diff --git a/.gitmodules b/.gitmodules index 0bfff2d..b12ae5d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "content"] path = content - url = https://github.com/ethereum/iptf-map.git + url = https://github.com/ethsystems/map.git diff --git a/CLAUDE.md b/CLAUDE.md index fb98b59..78a73eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,9 +2,9 @@ ## Project Overview -This is the IPTF (Institutional Privacy Task Force) website repository. Astro static site deployed at https://iptf.ethereum.org/. +This is the EthSystems website repository. Astro static site deployed at https://ethsystems.org/. -Map content (patterns, approaches, use-cases, vendors, domains, jurisdictions) is the projection of the [`iptf-map`](https://github.com/ethereum/iptf-map) repo, pinned as a submodule at `content/`. Blog writeups live in `src/posts/`. +Map content (patterns, approaches, use-cases, vendors, domains, jurisdictions) is the projection of the [`map`](https://github.com/ethsystems/map) repo, pinned as a submodule at `content/`. Blog writeups live in `src/posts/`. ## Tech Stack @@ -17,8 +17,8 @@ Map content (patterns, approaches, use-cases, vendors, domains, jurisdictions) i ## Key Files - `astro.config.mjs` — Astro config (site URL, integrations). -- `content/` — iptf-map submodule. -- `scripts/build-graph.mjs` — Reads iptf-map → `src/data/graph.json`. +- `content/` — map submodule (`ethsystems/map`). +- `scripts/build-graph.mjs` — Reads the map submodule → `src/data/graph.json`. - `src/posts/` — Blog post markdown (filename: `YYYY-MM-DD-slug.md`). - `src/pages/` — Routes. `blog/[slug].astro` is the post detail page. - `src/layouts/` — `Guide.astro` (default), `Post.astro` (writeups). @@ -39,7 +39,7 @@ Requires Node 22. ## Source-of-truth rule -iptf-map main is the only source of truth for map content. Anything sourced from the submodule renders verbatim. Render sites are marked with `SOURCE: iptf-map field — do not alter` comments. +The map repo (`ethsystems/map`) main is the only source of truth for map content. Anything sourced from the submodule renders verbatim. Render sites are marked with `SOURCE: map field — do not alter` comments. UI chrome (landing copy, FAQ, blog index, post layout) is the site's own and stays curated. @@ -63,24 +63,24 @@ title: "Post Title" description: "Brief description (shown in social cards and the blog index)." date: 2026-01-09 author: "Author Name" -image: /assets/images/2026-01-09-slug/hero.png +image: ../assets/posts/2026-01-09-slug/hero.png --- ``` -The published URL derives from the title via Jekyll-compatible slugify. Hero images live under `public/assets/images/`. Set `published: false` to keep a post out of the live site. +The published URL derives from the title via Jekyll-compatible slugify. Set `published: false` to keep a post out of the live site. ### Hero images - Recommended size: 1200x600px (2:1 ratio) for OG / Twitter cards. -- Location: `public/assets/images//`. -- Format: JPG, PNG, WEBP, or SVG. +- Location: `src/assets/posts//`, referenced from frontmatter and inline `![]()` markdown images by a path **relative to the post file** (e.g. `../assets/posts//hero.png`). Images must live under `src/` — the `image` field uses Astro's content-collection `image()` schema helper (`src/content.config.ts`), so hero/thumbnail images get resized and converted to WebP automatically wherever they're rendered via ``. Files placed in `public/` are served verbatim and bypass this entirely. +- Format: JPG, PNG, WEBP, or SVG (source format — output is optimized at build time). -## Updating iptf-map content +## Updating map content ```bash git submodule update --remote content git add content -git commit -m "chore(content): bump iptf-map submodule" +git commit -m "chore(content): bump map submodule" ``` ## Deployment diff --git a/README.md b/README.md index ded0b17..7bf24a9 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,21 @@ -# IPTF Website +# EthSystems Website -Static website for the Institutional Privacy Task Force (IPTF), live at [https://iptf.ethereum.org/](https://iptf.ethereum.org/). +Static website for EthSystems, live at [https://ethsystems.org/](https://ethsystems.org/). -Astro static site. Content for patterns, approaches, use-cases, vendors, domains, and jurisdictions is sourced from the [`iptf-map`](https://github.com/ethereum/iptf-map) repo via a git submodule at `content/`. Writeups (blog posts) live in `src/posts/`. +Astro static site. Content for patterns, approaches, use-cases, vendors, domains, and jurisdictions is sourced from the [`map`](https://github.com/ethsystems/map) repo via a git submodule at `content/`. Writeups (blog posts) live in `src/posts/`. ## How it works -- **Astro** generates every page at build time from data in `content/` (iptf-map) and `src/posts/` (writeups). +- **Astro** generates every page at build time from data in `content/` (the map submodule) and `src/posts/` (writeups). - **GitHub Pages** auto-deploys from `main` via `.github/workflows/deploy.yml`. Changes go live within a few minutes. -- **CNAME** (`public/CNAME`) points the deployment at `iptf.ethereum.org`. +- **CNAME** (`public/CNAME`) points the deployment at `ethsystems.org`. ## Repository layout ``` -iptf-web/ +web/ ├── astro.config.mjs Astro configuration (site URL, integrations) -├── content/ iptf-map submodule (patterns, approaches, etc.) +├── content/ map submodule (patterns, approaches, etc.) ├── public/ Static assets served verbatim │ ├── assets/images/ Post hero images, diagrams │ ├── assets/css/ @@ -24,7 +24,7 @@ iptf-web/ │ ├── CNAME │ └── robots.txt ├── scripts/ -│ └── build-graph.mjs Reads iptf-map → src/data/graph.json +│ └── build-graph.mjs Reads the map submodule → src/data/graph.json ├── src/ │ ├── data/ Generated at build (graph.json, glossary.json) │ ├── posts/ Blog post markdown @@ -79,17 +79,17 @@ The published URL is derived from the title (`//`). Hero images Set `published: false` in frontmatter to keep a post out of the deployed site. -## Updating the iptf-map content +## Updating the map content ```bash git submodule update --remote content git add content -git commit -m "chore(content): bump iptf-map submodule" +git commit -m "chore(content): bump map submodule" ``` ## Source-of-truth rule -iptf-map main is the only source of truth for patterns, approaches, vendors, etc. Anything sourced from the submodule renders verbatim. Pages that emit map content mark each render site with `SOURCE: iptf-map field — do not alter`. +The map repo (`ethsystems/map`) main is the only source of truth for patterns, approaches, vendors, etc. Anything sourced from the submodule renders verbatim. Pages that emit map content mark each render site with `SOURCE: map field — do not alter`. UI chrome (landing copy, FAQ, blog index, post layout) is the site's own and stays curated here. @@ -101,9 +101,8 @@ UI chrome (landing copy, FAQ, blog index, post layout) is the site's own and sta ## Contact -- Email: [iptf@ethereum.org](mailto:iptf@ethereum.org) -- [Institutions form](https://forms.gle/6Za8suF5QHyRamcW7) -- [Vendors form](https://forms.gle/znifD8h9Uw6VEX6Q9) +- Email: [hello@ethsystems.org](mailto:hello@ethsystems.org) +- [Book a call](https://calendar.app.google/yFK6o5Mzk7fKuu91A) ## License diff --git a/astro.config.mjs b/astro.config.mjs index fd041b7..14dcb58 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -8,10 +8,11 @@ import react from '@astrojs/react'; import sitemap from '@astrojs/sitemap'; export default defineConfig({ - site: 'https://iptf.ethereum.org', + site: process.env.OVERRIDE_URL || 'https://ethsystems.org', trailingSlash: 'always', build: { format: 'directory', + inlineStylesheets: 'always', }, // Legacy Jekyll URLs (permalink: /:title/, slug from filename) → new // title-derived slugs under /blog/. Keeps inbound links alive post-migration. @@ -41,5 +42,9 @@ export default defineConfig({ wrap: true, }, }, - integrations: [mdx(), react(), sitemap()], + integrations: [ + mdx(), + react(), + sitemap(), + ], }); \ No newline at end of file diff --git a/content b/content index 19f401e..3bca7a1 160000 --- a/content +++ b/content @@ -1 +1 @@ -Subproject commit 19f401e1ac97db576fca86b719713e501e12f2e1 +Subproject commit 3bca7a15d454c53275264d687bb8879130d62ef2 diff --git a/package-lock.json b/package-lock.json index 6a31eff..4860b2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,21 @@ { - "name": "iptf-website", + "name": "ethsystems-website", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "iptf-website", + "name": "ethsystems-website", "version": "1.0.0", "dependencies": { "@astrojs/check": "^0.9.4", "@astrojs/mdx": "^5.0.0", "@astrojs/react": "^5.0.5", + "@astrojs/rss": "^4.0.19", "@astrojs/sitemap": "^3.7.3", + "@fontsource-variable/fraunces": "^5.2.9", + "@fontsource-variable/geist": "^5.2.9", + "@fontsource-variable/geist-mono": "^5.2.8", "@types/d3": "^7.4.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -25,6 +29,7 @@ "unist-util-visit": "^5.0.0" }, "devDependencies": { + "@napi-rs/canvas": "^1.0.2", "js-yaml": "^4.1.1" }, "engines": { @@ -238,6 +243,17 @@ "picomatch": "^4.0.4" } }, + "node_modules/@astrojs/rss": { + "version": "4.0.19", + "resolved": "https://registry.npmjs.org/@astrojs/rss/-/rss-4.0.19.tgz", + "integrity": "sha512-e+z5wYeYtffQdHQO8c2tkSd2JEBdAuRXJV4ZEU5IxkYeE6e39woDd7nw1PH1Kk2tEYNCYuKdylnnbhGmt61awA==", + "license": "MIT", + "dependencies": { + "fast-xml-parser": "^5.5.7", + "piccolore": "^0.1.3", + "zod": "^4.3.6" + } + }, "node_modules/@astrojs/sitemap": { "version": "3.7.3", "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.3.tgz", @@ -1085,6 +1101,33 @@ "node": ">=18" } }, + "node_modules/@fontsource-variable/fraunces": { + "version": "5.2.9", + "resolved": "https://registry.npmjs.org/@fontsource-variable/fraunces/-/fraunces-5.2.9.tgz", + "integrity": "sha512-Y6IjunlN9Ni723np+GIgAaKzCDBrPRrqNi01TZxHs5wtHYROWFM9W6yiT+/gGwSjWIRD18oX17kD/BRWekc/Lw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/geist": { + "version": "5.2.9", + "resolved": "https://registry.npmjs.org/@fontsource-variable/geist/-/geist-5.2.9.tgz", + "integrity": "sha512-TP+QSBG3wxKGPE33CbMy/L0Nu3qvJ6Fy81Yc4LnQ95xH+i+cfEp8fyU8/kfV14YwszxIFPhnoMTbjL71waVpyQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource-variable/geist-mono": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/geist-mono/-/geist-mono-5.2.8.tgz", + "integrity": "sha512-KI5bj+hkkRiHttYHmccotUZ80ZuZyai+RwI1d7UId0clkx/jXxlo8qYK8j54WzmpBjtMoEMPyllV7faDcj+6RA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -1642,6 +1685,279 @@ "@types/estree": "^1.0.0" } }, + "node_modules/@napi-rs/canvas": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.2.tgz", + "integrity": "sha512-EYEqlMYaCbpZDz+IgDH5xp9MTd3ui4dmGqbQYryhMLnSRxrhHKq5KQWHHKxFUcEP4Hp8/BWgvqXocX4j7iSbOQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "1.0.2", + "@napi-rs/canvas-darwin-arm64": "1.0.2", + "@napi-rs/canvas-darwin-x64": "1.0.2", + "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.2", + "@napi-rs/canvas-linux-arm64-gnu": "1.0.2", + "@napi-rs/canvas-linux-arm64-musl": "1.0.2", + "@napi-rs/canvas-linux-riscv64-gnu": "1.0.2", + "@napi-rs/canvas-linux-x64-gnu": "1.0.2", + "@napi-rs/canvas-linux-x64-musl": "1.0.2", + "@napi-rs/canvas-win32-arm64-msvc": "1.0.2", + "@napi-rs/canvas-win32-x64-msvc": "1.0.2" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.2.tgz", + "integrity": "sha512-IMXKVQod0ol4vt3gmClUfXz4JAgHYESGPCUqmH3lQxBoL0K/2greJaQE1HVBVxWWFKfLc4OLZVdxg7kXVyXv+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-Sc8tPi6cF+5lqOzCCKFALJHhDiRwyMzTPYm3bbhdXsOunU0lQO5f05ucyOzN2r55I23Hg5bsjH63uSCvWp3EgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.2.tgz", + "integrity": "sha512-niDXZ9LhKB1zLrUdYB64RHQFDGz9rr0eGx061qtJJU3U20EMMIx28ADF5fVYbhtOgkWQrBjFicfaye1yM0U62A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-sgatQL9JxGRH/Amzcvu0P3t8Am3duou74CisfuJ41Dwt8cWy723z/9KZ8LlgmxfypEwEZxSTNFJtU8d281lmhQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-dgKuX0peF3xwY6ZF5QxGS4wbfDqpoFAJYXiLSp+guZKARQUKMkRqZSDrXKj7nfrec3UCMzC0PFCPte0ES98AiA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-qwROoDIC9upfvDoRLuPn2aNg9CGW1x0Ygr4k2Or+8paA9d0qBLwk87U+g8KQpoOviKoPoiwl97kvBYuYD7qZoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.2.tgz", + "integrity": "sha512-fXRjnPihdnbO6qy1QQOgxAonb68A0TCEG7rj1x7v7rxNElsE8EVIKIEUTvyDtU+sthYSbX+8e7g3oZiLGnOmxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-nPR97DXhbWIAy7yazF3jc06kEPMqYMLmPzFOVNlwKPfIoSChnI+x7dc0hTLaihz3jxrjL6j4BbA7earxfx4X3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-l7zZY5+jL5qnBZtDz7CoBtY6p7EkHu422g/0zWwrOrzIwWyWxZFRfZZORY1UG7YApymPLx+UbOkN206xXn/c1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-yE0koHCFF4PIbMc2o2SEALhnipz7WBISh5glLvQiomtIoCcW0np3H4Lw93ceJAfJttTTeIIWFbwH84F7EVzjMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-okU8/t2foV6C31n0GtvEMbfD5rOFc70+/6xUNME9Guld29sgSOIGUEDScAWFlcP3k5TYQRl9TNkwJEEjh15w8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@oslojs/encoding": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", @@ -2728,6 +3044,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -4088,6 +4416,45 @@ "fast-string-width": "^3.0.2" } }, + "node_modules/fast-xml-builder": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.1.tgz", + "integrity": "sha512-tPb5TTWfgfVx5BNSi2xV0eLr89POeXXn0dXIsCJ9m1narrWxeIyx6je9d7Rce/3NyXLbvuQmLkxq+RuxMWejvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.9.3.tgz", + "integrity": "sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.2.0", + "fast-xml-builder": "^1.2.0", + "is-unsafe": "^1.0.1", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.4.1", + "xml-naming": "^0.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4608,6 +4975,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unsafe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz", + "integrity": "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/is-wsl": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", @@ -6038,6 +6417,21 @@ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "license": "MIT" }, + "node_modules/path-expression-matcher": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", + "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/piccolore": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", @@ -6799,6 +7193,21 @@ "node": ">=8" } }, + "node_modules/strnum": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -7655,6 +8064,21 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xxhash-wasm": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", diff --git a/package.json b/package.json index feeefa0..b94ee64 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "iptf-website", + "name": "ethsystems-website", "private": true, "type": "module", "version": "1.0.0", @@ -9,7 +9,7 @@ "scripts": { "build:graph": "node scripts/build-graph.mjs", "predev": "npm run build:graph", - "dev": "astro dev", + "dev": "astro dev --host", "prebuild": "node scripts/validate-content-refs.mjs && npm run build:graph", "build": "astro build", "prepreview": "npm run build:graph", @@ -19,13 +19,18 @@ "lint:refs": "node scripts/validate-content-refs.mjs --strict" }, "devDependencies": { + "@napi-rs/canvas": "^1.0.2", "js-yaml": "^4.1.1" }, "dependencies": { "@astrojs/check": "^0.9.4", "@astrojs/mdx": "^5.0.0", "@astrojs/react": "^5.0.5", + "@astrojs/rss": "^4.0.19", "@astrojs/sitemap": "^3.7.3", + "@fontsource-variable/fraunces": "^5.2.9", + "@fontsource-variable/geist": "^5.2.9", + "@fontsource-variable/geist-mono": "^5.2.8", "@types/d3": "^7.4.3", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/public/CNAME b/public/CNAME index 3754570..6758753 100644 --- a/public/CNAME +++ b/public/CNAME @@ -1 +1 @@ -iptf.ethereum.org \ No newline at end of file +ethsystems.org diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..6b04921 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/assets/css/custom.css b/public/assets/css/custom.css deleted file mode 100644 index dfd2228..0000000 --- a/public/assets/css/custom.css +++ /dev/null @@ -1,966 +0,0 @@ -/* IPTF Professional Theme - Financial Institution Grade */ - -/* =========================== - CSS Variables - =========================== */ -:root { - /* Professional color palette */ - --color-primary: #1a365d; /* Deep navy */ - --color-primary-light: #2c5282; - --color-accent: #3182ce; /* Trustworthy blue */ - --color-accent-hover: #2b6cb0; - - /* Neutral tones */ - --color-text: #1a202c; - --color-text-secondary: #4a5568; - --color-text-muted: #718096; - --color-border: #e2e8f0; - --color-border-dark: #cbd5e0; - --color-bg: #ffffff; - --color-bg-subtle: #f7fafc; - --color-bg-muted: #edf2f7; - - /* Typography */ - --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - --font-mono: 'JetBrains Mono', 'Fira Code', 'Monaco', monospace; - - /* Spacing */ - --spacing-xs: 0.25rem; - --spacing-sm: 0.5rem; - --spacing-md: 1rem; - --spacing-lg: 1.5rem; - --spacing-xl: 2rem; - --spacing-2xl: 3rem; - - /* Shadows */ - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); - - /* Border radius */ - --radius-sm: 4px; - --radius-md: 8px; - --radius-lg: 12px; -} - -/* =========================== - Base Typography - =========================== */ -body { - font-family: var(--font-sans); - color: var(--color-text); - background-color: var(--color-bg); - line-height: 1.7; - font-size: 16px; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -h1, h2, h3, h4, h5, h6 { - font-weight: 600; - line-height: 1.3; - color: var(--color-primary); - margin-top: var(--spacing-xl); - margin-bottom: var(--spacing-md); -} - -h1 { font-size: 1.75rem; font-weight: 700; } -h2 { font-size: 1.375rem; } -h3 { font-size: 1.125rem; } -h4 { font-size: 1rem; } - -p { - margin-bottom: var(--spacing-md); - color: var(--color-text); -} - -a { - color: var(--color-accent); - text-decoration: none; - transition: color 0.2s ease; -} - -a:hover { - color: var(--color-accent-hover); - text-decoration: underline; -} - -/* =========================== - Site Header - =========================== */ -.site-header { - border-top: 4px solid var(--color-primary); - border-bottom: 1px solid var(--color-border); - background: var(--color-bg); - padding: var(--spacing-md) 0; -} - -.site-title { - font-size: 1.25rem; - font-weight: 700; - color: var(--color-primary); - letter-spacing: -0.02em; - display: flex; - align-items: center; - gap: var(--spacing-sm); -} - -.site-title:hover { - color: var(--color-primary); - text-decoration: none; -} - -.site-logo { - width: 24px; - height: 24px; - object-fit: contain; -} - -.site-nav a { - font-size: 0.9375rem; - font-weight: 500; - color: var(--color-text-secondary); - padding: var(--spacing-sm) var(--spacing-md); - border-radius: var(--radius-sm); - transition: all 0.2s ease; -} - -.site-nav a:hover { - color: var(--color-primary); - background: var(--color-bg-subtle); - text-decoration: none; -} - -.site-nav a.active { - color: var(--color-primary); - font-weight: 600; -} - -/* =========================== - Page Content - =========================== */ -.page-content { - padding: var(--spacing-2xl) 0; -} - -.wrapper { - max-width: 760px; - margin: 0 auto; - padding: 0 var(--spacing-lg); -} - -/* =========================== - Blog Page Heading - =========================== */ -.page-heading { - font-size: 1.5rem; - font-weight: 700; - color: var(--color-primary); - margin-bottom: var(--spacing-xl); - padding-bottom: var(--spacing-md); - border-bottom: 2px solid var(--color-border); -} - -/* =========================== - Blog Post Cards - =========================== */ -.post-list { - list-style: none; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - gap: var(--spacing-xl); -} - -.post-list > li { - margin: 0; -} - -.post-card-link { - display: block; - text-decoration: none; - color: inherit; -} - -.post-card-link:hover { - text-decoration: none; -} - -.post-card { - display: flex; - gap: var(--spacing-lg); - padding: var(--spacing-lg); - background: var(--color-bg); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - transition: all 0.2s ease; -} - -.post-card:hover { - border-color: var(--color-border-dark); - box-shadow: var(--shadow-md); - transform: translateY(-2px); - cursor: pointer; -} - -.post-card-thumbnail { - flex-shrink: 0; - width: 200px; - height: 120px; - overflow: hidden; - border-radius: var(--radius-md); - background: var(--color-bg-muted); -} - -.post-card-thumbnail img { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.3s ease; -} - -.post-card:hover .post-card-thumbnail img { - transform: scale(1.05); -} - -.post-card-content { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; -} - -.post-card-title { - font-size: 1.25rem; - font-weight: 600; - margin: 0 0 var(--spacing-sm) 0; - line-height: 1.4; -} - -.post-card-title a { - color: var(--color-primary); -} - -.post-card-title a:hover { - color: var(--color-accent); - text-decoration: none; -} - -.post-card-meta { - font-size: 0.875rem; - color: var(--color-text-muted); - margin-bottom: var(--spacing-sm); - display: flex; - align-items: center; - gap: var(--spacing-sm); -} - -.post-card-meta .separator { - color: var(--color-border-dark); -} - -.post-card-description { - font-size: 0.9375rem; - color: var(--color-text-secondary); - line-height: 1.6; - margin: 0; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.post-card-footer { - margin-top: auto; - padding-top: var(--spacing-md); -} - -.read-more { - font-size: 0.875rem; - font-weight: 500; - color: var(--color-accent); - display: inline-flex; - align-items: center; - gap: var(--spacing-xs); -} - -.read-more:hover { - text-decoration: none; - gap: var(--spacing-sm); -} - -.read-more::after { - content: '\2192'; - transition: transform 0.2s ease; -} - -.post-card:hover .read-more::after { - transform: translateX(4px); -} - -/* Responsive: Stack cards on mobile */ -@media (max-width: 640px) { - .post-card { - flex-direction: column; - } - - .post-card-thumbnail { - width: 100%; - height: 160px; - } -} - -/* =========================== - Individual Post Styles - =========================== */ -.post-hero { - margin: calc(-1 * var(--spacing-2xl)) calc(-50vw + 50%) var(--spacing-xl); - width: 100vw; - max-height: 400px; - overflow: hidden; -} - -.post-hero img { - max-width: 100%; - height: auto; - max-height: 400px; - object-fit: contain; - display: block; - margin: 0 auto; -} - -.post-header { - margin-bottom: var(--spacing-xl); - padding-bottom: var(--spacing-lg); - border-bottom: 1px solid var(--color-border); -} - -.post-title { - font-size: 1.75rem; - font-weight: 700; - color: var(--color-primary); - margin: 0 0 var(--spacing-md) 0; - line-height: 1.2; - letter-spacing: -0.02em; -} - -.post-meta { - font-size: 0.9375rem; - color: var(--color-text-muted); - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--spacing-sm); -} - -.post-meta .author { - font-weight: 500; - color: var(--color-text-secondary); -} - -.post-meta .reading-time { - display: flex; - align-items: center; - gap: var(--spacing-xs); -} - -.post-meta .reading-time::before { - content: ''; - width: 4px; - height: 4px; - background: var(--color-text-muted); - border-radius: 50%; -} - -/* =========================== - Post Content - =========================== */ -.post-content { - font-size: 1.0625rem; - line-height: 1.8; -} - -.post-content h2 { - margin-top: var(--spacing-2xl); - padding-top: var(--spacing-lg); - border-top: 1px solid var(--color-border); -} - -.post-content h3 { - margin-top: var(--spacing-xl); -} - -.post-content ul, -.post-content ol { - padding-left: var(--spacing-xl); - margin-bottom: var(--spacing-md); - list-style: disc; -} - -.post-content ol { - list-style: decimal; -} - -.post-content li { - margin-bottom: var(--spacing-sm); - padding-left: var(--spacing-xs); -} - -.post-content blockquote { - margin: var(--spacing-lg) 0; - padding: var(--spacing-md) var(--spacing-lg); - border-left: 3px solid var(--color-border-dark); - background: var(--color-bg-subtle); - border-radius: 0 var(--radius-sm) var(--radius-sm) 0; - font-style: normal; - color: var(--color-text-secondary); -} - -.post-content blockquote p:last-child { - margin-bottom: 0; -} - -/* Code blocks - inline */ -.post-content code { - font-family: var(--font-mono); - font-size: 0.875em; - background: var(--color-bg-muted); - padding: 0.2em 0.4em; - border-radius: var(--radius-sm); - color: var(--color-text); -} - -/* Code blocks - block level */ -.post-content pre, -.highlighter-rouge .highlight, -.highlighter-rouge pre, -.highlighter-rouge pre.highlight, -div.highlight, -figure.highlight { - background: #0d1117 !important; - color: #c9d1d9 !important; - padding: var(--spacing-lg); - border-radius: var(--radius-md); - overflow-x: auto; - margin: var(--spacing-lg) 0; - font-size: 0.875rem; - line-height: 1.6; - border: 1px solid #30363d; -} - -/* Reset nested pre padding to avoid double padding */ -.highlighter-rouge .highlight pre { - padding: 0; - margin: 0; - border: none; -} - -.post-content pre code, -.highlighter-rouge pre.highlight code, -div.highlight code, -figure.highlight code { - background: none !important; - padding: 0; - color: inherit; - font-size: inherit; -} - -/* Rouge syntax highlighting - GitHub Dark theme */ -.highlight .c, -.highlight .c1, -.highlight .cm { color: #8b949e !important; font-style: italic; } /* Comments */ -.highlight .k, -.highlight .kd, -.highlight .kn, -.highlight .kp, -.highlight .kr, -.highlight .kt { color: #ff7b72 !important; } /* Keywords */ -.highlight .s, -.highlight .s1, -.highlight .s2, -.highlight .sb, -.highlight .sc, -.highlight .sd, -.highlight .se, -.highlight .sh, -.highlight .si, -.highlight .sx, -.highlight .sr { color: #a5d6ff !important; } /* Strings */ -.highlight .n, -.highlight .na, -.highlight .nb, -.highlight .nc, -.highlight .nd, -.highlight .ne, -.highlight .nf, -.highlight .ni, -.highlight .nl, -.highlight .nn, -.highlight .no, -.highlight .nt, -.highlight .nv { color: #c9d1d9 !important; } /* Names */ -.highlight .nc, -.highlight .nf { color: #d2a8ff !important; } /* Class/Function names */ -.highlight .mi, -.highlight .mf, -.highlight .mh, -.highlight .mo, -.highlight .il { color: #79c0ff !important; } /* Numbers */ -.highlight .o, -.highlight .ow { color: #ff7b72 !important; } /* Operators */ -.highlight .p { color: #c9d1d9 !important; } /* Punctuation */ -.highlight .err { color: #f85149 !important; } /* Errors */ -.highlight .gd { color: #ffa198 !important; background: #490202 !important; } /* Deleted */ -.highlight .gi { color: #7ee787 !important; background: #04260f !important; } /* Inserted */ -.highlight .gh { color: #79c0ff !important; font-weight: bold; } /* Heading */ -.highlight .nt { color: #7ee787 !important; } /* HTML/XML tags */ -.highlight .na { color: #79c0ff !important; } /* Attributes */ - -/* Tables */ -.post-content table { - width: 100%; - border-collapse: collapse; - margin: var(--spacing-lg) 0; - font-size: 0.9375rem; -} - -.post-content th, -.post-content td { - padding: var(--spacing-sm) var(--spacing-md); - text-align: left; - border-bottom: 1px solid var(--color-border); -} - -.post-content th { - background: var(--color-bg-subtle); - font-weight: 600; - color: var(--color-primary); -} - -.post-content tr:hover td { - background: var(--color-bg-subtle); -} - -/* Images in posts */ -.post-content img { - max-width: 100%; - height: auto; - border-radius: var(--radius-md); - margin: var(--spacing-lg) 0; -} - -/* =========================== - Footer - =========================== */ -.site-footer { - border-top: 1px solid var(--color-border); - padding: var(--spacing-xl) 0; - margin-top: var(--spacing-2xl); - background: var(--color-bg-subtle); -} - -.site-footer .wrapper { - font-size: 0.875rem; - color: var(--color-text-muted); -} - -/* =========================== - Homepage Styles - =========================== */ - -/* Hero Section */ -.hero { - text-align: center; - padding: var(--spacing-2xl) 0; - margin-top: calc(-1 * var(--spacing-2xl)); - margin-bottom: var(--spacing-xl); - background: linear-gradient(180deg, var(--color-bg-subtle) 0%, var(--color-bg) 100%); - border-bottom: 1px solid var(--color-border); - margin-left: calc(-50vw + 50%); - margin-right: calc(-50vw + 50%); - padding-left: calc(50vw - 50%); - padding-right: calc(50vw - 50%); -} - -.hero-badge { - display: inline-block; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-accent); - background: rgba(49, 130, 206, 0.1); - padding: var(--spacing-xs) var(--spacing-md); - border-radius: 100px; - margin-bottom: var(--spacing-md); -} - -.hero h1 { - font-size: 1.875rem; - margin: 0 0 var(--spacing-sm) 0; - color: var(--color-primary); -} - -.hero-tagline { - font-size: 1.125rem; - color: var(--color-text-secondary); - margin: 0; - max-width: 500px; - margin-left: auto; - margin-right: auto; -} - -/* About Section */ -.about { - margin-bottom: var(--spacing-xl); -} - -.about p { - color: var(--color-text-secondary); - line-height: 1.7; -} - -/* Section Headings */ -.initiatives h2, -.latest-posts h2, -.contact h2 { - font-size: 0.875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-muted); - margin-bottom: var(--spacing-lg); - padding-bottom: var(--spacing-sm); - border-bottom: 1px solid var(--color-border); -} - -/* Initiative Cards */ -.initiative-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: var(--spacing-md); - margin-bottom: var(--spacing-xl); -} - -.initiative-card { - padding: var(--spacing-lg); - background: var(--color-bg); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - text-decoration: none; - transition: all 0.2s ease; -} - -a.initiative-card { - cursor: pointer; -} - -a.initiative-card:hover { - border-color: var(--color-accent); - box-shadow: var(--shadow-sm); - text-decoration: none; - transform: translateY(-2px); -} - -.initiative-icon { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - background: var(--color-bg-subtle); - border-radius: var(--radius-md); - margin-bottom: var(--spacing-md); - color: var(--color-accent); -} - -.initiative-icon svg { - width: 20px; - height: 20px; -} - -a.initiative-card:hover .initiative-icon { - background: rgba(49, 130, 206, 0.1); -} - -.initiative-card h3 { - font-size: 1rem; - font-weight: 600; - color: var(--color-primary); - margin: 0 0 var(--spacing-xs) 0; -} - -.initiative-card p { - font-size: 0.875rem; - color: var(--color-text-secondary); - margin: 0; - line-height: 1.5; -} - -/* Featured Posts */ -.featured-posts { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: var(--spacing-lg); - margin-bottom: var(--spacing-md); -} - -.featured-post-card { - display: flex; - flex-direction: column; - background: var(--color-bg); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - overflow: hidden; - text-decoration: none; - transition: all 0.2s ease; -} - -.featured-post-card:hover { - border-color: var(--color-border-dark); - box-shadow: var(--shadow-md); - text-decoration: none; - cursor: pointer; -} - -.featured-post-image { - width: 100%; - height: 140px; - overflow: hidden; - background: var(--color-bg-muted); -} - -.featured-post-image img { - width: 100%; - height: 100%; - object-fit: cover; - transition: transform 0.3s ease; -} - -.featured-post-card:hover .featured-post-image img { - transform: scale(1.05); -} - -.featured-post-content { - padding: var(--spacing-md); -} - -.featured-post-content h3 { - font-size: 1rem; - font-weight: 600; - color: var(--color-primary); - margin: 0 0 var(--spacing-xs) 0; - line-height: 1.4; -} - -.featured-post-meta { - font-size: 0.8125rem; - color: var(--color-text-muted); - margin: 0 0 var(--spacing-sm) 0; -} - -.featured-post-desc { - font-size: 0.875rem; - color: var(--color-text-secondary); - margin: 0; - line-height: 1.5; -} - -.see-all-link { - font-size: 0.9375rem; - font-weight: 500; - color: var(--color-accent); -} - -.see-all-link:hover { - text-decoration: underline; -} - -/* Contact Section */ -.contact { - margin-top: var(--spacing-xl); -} - -.contact-links { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: var(--spacing-md); -} - -.contact-item { - display: flex; - flex-direction: column; - justify-content: center; - padding: var(--spacing-lg); - background: var(--color-bg-subtle); - border-radius: var(--radius-md); - text-decoration: none; - transition: all 0.2s ease; - text-align: center; -} - -.contact-item:hover { - background: var(--color-bg-muted); - text-decoration: none; -} - -.contact-label { - font-size: 0.6875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-text-muted); - margin-bottom: var(--spacing-xs); -} - -.contact-value { - font-size: 0.875rem; - color: var(--color-accent); - font-weight: 500; -} - -/* Responsive */ -@media (max-width: 640px) { - .hero { - padding: var(--spacing-xl) var(--spacing-md); - } - - .hero h1 { - font-size: 1.5rem; - } - - .hero-badge { - font-size: 0.6875rem; - } - - .initiative-grid { - grid-template-columns: 1fr; - } - - .featured-posts { - grid-template-columns: 1fr; - } - - .contact-links { - grid-template-columns: 1fr; - } -} - -.home h1:first-child, -.home h2:first-child { - margin-top: 0; -} - -/* =========================== - Utility Classes - =========================== */ -.text-muted { - color: var(--color-text-muted); -} - -.text-small { - font-size: 0.875rem; -} - -/* =========================== - ToC Overrides (Professional) - =========================== */ -.post-toc h3 { - color: var(--color-primary); - font-size: 0.75rem; - font-weight: 600; - letter-spacing: 0.08em; -} - -.post-toc a { - color: var(--color-text-muted); -} - -.post-toc a:hover { - color: var(--color-primary); -} - -.post-toc a.active { - color: var(--color-accent); - font-weight: 500; -} - -/* =========================== - Print Styles - =========================== */ -@media print { - .site-header, - .site-footer, - .post-toc { - display: none; - } - - .post-content { - font-size: 12pt; - } -} - -/* =========================== - Footnotes - =========================== */ -.footnotes { - margin-top: var(--spacing-2xl); - padding-top: var(--spacing-lg); - border-top: 1px solid var(--color-border); - font-size: 0.875rem; - color: var(--color-text-secondary); - font-style: italic; -} - -.footnotes ol { - padding-left: var(--spacing-lg); - margin: 0; -} - -.footnotes > ol > li { - margin-bottom: var(--spacing-md); -} - -.footnotes > ol > li:last-child { - margin-bottom: 0; -} - -.footnotes ul { - margin: var(--spacing-sm) 0; -} - -.footnotes ul li { - margin-bottom: var(--spacing-xs); - padding-left: 0; -} - -.footnotes ul li:last-child { - margin-bottom: 0; -} - -.footnotes p { - margin-bottom: var(--spacing-xs); -} - -.footnotes a.reversefootnote { - font-style: normal; - margin-left: var(--spacing-xs); - text-decoration: none; - color: var(--color-accent); -} - -.footnotes a.reversefootnote:hover { - color: var(--color-accent-hover); -} - -/* Footnote reference superscript in body */ -sup[role="doc-noteref"] a { - text-decoration: none; - font-weight: 600; - color: var(--color-accent); -} - -sup[role="doc-noteref"] a:hover { - color: var(--color-accent-hover); -} diff --git a/public/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-2-bond-lifecycle.png b/public/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-2-bond-lifecycle.png deleted file mode 100644 index 58fe690..0000000 Binary files a/public/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-2-bond-lifecycle.png and /dev/null differ diff --git a/public/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-4-circuit-constraints.png b/public/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-4-circuit-constraints.png deleted file mode 100644 index 5f78227..0000000 Binary files a/public/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-4-circuit-constraints.png and /dev/null differ diff --git a/public/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-5-atomic-swap-flow.png b/public/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-5-atomic-swap-flow.png deleted file mode 100644 index 6cf3499..0000000 Binary files a/public/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-5-atomic-swap-flow.png and /dev/null differ diff --git a/public/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-6-commitment-and-nullifier.png b/public/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-6-commitment-and-nullifier.png deleted file mode 100644 index 0465c1e..0000000 Binary files a/public/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-6-commitment-and-nullifier.png and /dev/null differ diff --git a/public/assets/images/2026-01-30-public-rails-vs-private-ledgers/architecture_comparison.png b/public/assets/images/2026-01-30-public-rails-vs-private-ledgers/architecture_comparison.png deleted file mode 100644 index 0af74e0..0000000 Binary files a/public/assets/images/2026-01-30-public-rails-vs-private-ledgers/architecture_comparison.png and /dev/null differ diff --git a/public/assets/images/2026-01-30-public-rails-vs-private-ledgers/betting_slips.png b/public/assets/images/2026-01-30-public-rails-vs-private-ledgers/betting_slips.png deleted file mode 100644 index 06723bb..0000000 Binary files a/public/assets/images/2026-01-30-public-rails-vs-private-ledgers/betting_slips.png and /dev/null differ diff --git a/public/assets/images/2026-01-30-public-rails-vs-private-ledgers/why_not_database.png b/public/assets/images/2026-01-30-public-rails-vs-private-ledgers/why_not_database.png deleted file mode 100644 index f868704..0000000 Binary files a/public/assets/images/2026-01-30-public-rails-vs-private-ledgers/why_not_database.png and /dev/null differ diff --git a/public/assets/images/2026-02-05-private-bonds-on-privacy-l2s/img-1-authwit-dvp.png b/public/assets/images/2026-02-05-private-bonds-on-privacy-l2s/img-1-authwit-dvp.png deleted file mode 100644 index e63b3f5..0000000 Binary files a/public/assets/images/2026-02-05-private-bonds-on-privacy-l2s/img-1-authwit-dvp.png and /dev/null differ diff --git a/public/assets/images/2026-02-05-private-bonds-on-privacy-l2s/img-2-public-private-aztec.png b/public/assets/images/2026-02-05-private-bonds-on-privacy-l2s/img-2-public-private-aztec.png deleted file mode 100644 index 1908ad0..0000000 Binary files a/public/assets/images/2026-02-05-private-bonds-on-privacy-l2s/img-2-public-private-aztec.png and /dev/null differ diff --git a/public/assets/images/2026-02-12-private-bonds-fhe/fhe-homomorphic-property.png b/public/assets/images/2026-02-12-private-bonds-fhe/fhe-homomorphic-property.png deleted file mode 100644 index cbf16bb..0000000 Binary files a/public/assets/images/2026-02-12-private-bonds-fhe/fhe-homomorphic-property.png and /dev/null differ diff --git a/public/assets/images/2026-02-12-private-bonds-fhe/zama-architecture.png b/public/assets/images/2026-02-12-private-bonds-fhe/zama-architecture.png deleted file mode 100644 index 963200c..0000000 Binary files a/public/assets/images/2026-02-12-private-bonds-fhe/zama-architecture.png and /dev/null differ diff --git a/public/assets/images/2026-02-26-private-stablecoins-with-plasma/architecture.png b/public/assets/images/2026-02-26-private-stablecoins-with-plasma/architecture.png deleted file mode 100644 index 555046a..0000000 Binary files a/public/assets/images/2026-02-26-private-stablecoins-with-plasma/architecture.png and /dev/null differ diff --git a/public/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-2-stealth-derivation.png b/public/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-2-stealth-derivation.png deleted file mode 100644 index 3911b6e..0000000 Binary files a/public/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-2-stealth-derivation.png and /dev/null differ diff --git a/public/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-3-note-structure.png b/public/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-3-note-structure.png deleted file mode 100644 index acf85e0..0000000 Binary files a/public/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-3-note-structure.png and /dev/null differ diff --git a/public/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-4-locking-flow.png b/public/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-4-locking-flow.png deleted file mode 100644 index 6f63b8a..0000000 Binary files a/public/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-4-locking-flow.png and /dev/null differ diff --git a/public/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-5-timeout-refund.png b/public/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-5-timeout-refund.png deleted file mode 100644 index 92b9b92..0000000 Binary files a/public/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-5-timeout-refund.png and /dev/null differ diff --git a/public/assets/images/2026-03-18-diy-validium/architecture.png b/public/assets/images/2026-03-18-diy-validium/architecture.png deleted file mode 100644 index 3d6505d..0000000 Binary files a/public/assets/images/2026-03-18-diy-validium/architecture.png and /dev/null differ diff --git a/public/assets/images/2026-03-18-diy-validium/censorship-resistance.png b/public/assets/images/2026-03-18-diy-validium/censorship-resistance.png deleted file mode 100644 index 56d7ff6..0000000 Binary files a/public/assets/images/2026-03-18-diy-validium/censorship-resistance.png and /dev/null differ diff --git a/public/assets/images/2026-03-18-diy-validium/zkvm-pattern.png b/public/assets/images/2026-03-18-diy-validium/zkvm-pattern.png deleted file mode 100644 index 8d89cda..0000000 Binary files a/public/assets/images/2026-03-18-diy-validium/zkvm-pattern.png and /dev/null differ diff --git a/public/assets/images/2026-03-18-private-crosschain-swap-part-2/diagram-1-attestation-flow.png b/public/assets/images/2026-03-18-private-crosschain-swap-part-2/diagram-1-attestation-flow.png deleted file mode 100644 index 6250333..0000000 Binary files a/public/assets/images/2026-03-18-private-crosschain-swap-part-2/diagram-1-attestation-flow.png and /dev/null differ diff --git a/public/assets/images/2026-03-18-private-crosschain-swap-part-2/diagram-2-tee-coordinator.png b/public/assets/images/2026-03-18-private-crosschain-swap-part-2/diagram-2-tee-coordinator.png deleted file mode 100644 index 4c30b36..0000000 Binary files a/public/assets/images/2026-03-18-private-crosschain-swap-part-2/diagram-2-tee-coordinator.png and /dev/null differ diff --git a/public/assets/images/2026-04-14-resilient-private-identity/resilient_identity_flow.png b/public/assets/images/2026-04-14-resilient-private-identity/resilient_identity_flow.png deleted file mode 100644 index 77729e6..0000000 Binary files a/public/assets/images/2026-04-14-resilient-private-identity/resilient_identity_flow.png and /dev/null differ diff --git a/public/assets/images/2026-04-14-resilient-private-identity/resilient_identity_verification_flow.png b/public/assets/images/2026-04-14-resilient-private-identity/resilient_identity_verification_flow.png deleted file mode 100644 index 635c43a..0000000 Binary files a/public/assets/images/2026-04-14-resilient-private-identity/resilient_identity_verification_flow.png and /dev/null differ diff --git a/public/assets/images/2026-05-14-resilient-disbursement-rails/how_a_round_publishes.png b/public/assets/images/2026-05-14-resilient-disbursement-rails/how_a_round_publishes.png deleted file mode 100644 index abb3c7d..0000000 Binary files a/public/assets/images/2026-05-14-resilient-disbursement-rails/how_a_round_publishes.png and /dev/null differ diff --git a/public/assets/images/2026-05-14-resilient-disbursement-rails/how_recipients_claim.png b/public/assets/images/2026-05-14-resilient-disbursement-rails/how_recipients_claim.png deleted file mode 100644 index 49b94b2..0000000 Binary files a/public/assets/images/2026-05-14-resilient-disbursement-rails/how_recipients_claim.png and /dev/null differ diff --git a/public/assets/images/2026-05-14-resilient-disbursement-rails/how_relays_close_the_loop.png b/public/assets/images/2026-05-14-resilient-disbursement-rails/how_relays_close_the_loop.png deleted file mode 100644 index eadf8df..0000000 Binary files a/public/assets/images/2026-05-14-resilient-disbursement-rails/how_relays_close_the_loop.png and /dev/null differ diff --git a/public/assets/images/2026-05-28-resilient-civic-participation/how_a_petition_publishes.png b/public/assets/images/2026-05-28-resilient-civic-participation/how_a_petition_publishes.png deleted file mode 100644 index bc8825f..0000000 Binary files a/public/assets/images/2026-05-28-resilient-civic-participation/how_a_petition_publishes.png and /dev/null differ diff --git a/public/assets/images/2026-05-28-resilient-civic-participation/how_batches_publish.png b/public/assets/images/2026-05-28-resilient-civic-participation/how_batches_publish.png deleted file mode 100644 index 00d0556..0000000 Binary files a/public/assets/images/2026-05-28-resilient-civic-participation/how_batches_publish.png and /dev/null differ diff --git a/public/assets/images/2026-05-28-resilient-civic-participation/how_disputes_work.png b/public/assets/images/2026-05-28-resilient-civic-participation/how_disputes_work.png deleted file mode 100644 index bb2728c..0000000 Binary files a/public/assets/images/2026-05-28-resilient-civic-participation/how_disputes_work.png and /dev/null differ diff --git a/public/assets/images/2026-05-28-resilient-civic-participation/how_signing_works.png b/public/assets/images/2026-05-28-resilient-civic-participation/how_signing_works.png deleted file mode 100644 index 72b6004..0000000 Binary files a/public/assets/images/2026-05-28-resilient-civic-participation/how_signing_works.png and /dev/null differ diff --git a/public/assets/images/approaches/dvp-atomic-settlement.png b/public/assets/images/approaches/dvp-atomic-settlement.png deleted file mode 100644 index de85dd7..0000000 Binary files a/public/assets/images/approaches/dvp-atomic-settlement.png and /dev/null differ diff --git a/public/assets/images/approaches/private-bonds.png b/public/assets/images/approaches/private-bonds.png deleted file mode 100644 index e412b06..0000000 Binary files a/public/assets/images/approaches/private-bonds.png and /dev/null differ diff --git a/public/assets/images/approaches/private-broadcasting.png b/public/assets/images/approaches/private-broadcasting.png deleted file mode 100644 index 18c35dc..0000000 Binary files a/public/assets/images/approaches/private-broadcasting.png and /dev/null differ diff --git a/public/assets/images/approaches/private-derivatives.png b/public/assets/images/approaches/private-derivatives.png deleted file mode 100644 index 5ea08f1..0000000 Binary files a/public/assets/images/approaches/private-derivatives.png and /dev/null differ diff --git a/public/assets/images/approaches/private-identity.png b/public/assets/images/approaches/private-identity.png deleted file mode 100644 index 3235d89..0000000 Binary files a/public/assets/images/approaches/private-identity.png and /dev/null differ diff --git a/public/assets/images/approaches/private-money-market-funds.png b/public/assets/images/approaches/private-money-market-funds.png deleted file mode 100644 index 0ad70d6..0000000 Binary files a/public/assets/images/approaches/private-money-market-funds.png and /dev/null differ diff --git a/public/assets/images/approaches/private-payments.png b/public/assets/images/approaches/private-payments.png deleted file mode 100644 index 2a2a5a7..0000000 Binary files a/public/assets/images/approaches/private-payments.png and /dev/null differ diff --git a/public/assets/images/approaches/private-trade-settlement.png b/public/assets/images/approaches/private-trade-settlement.png deleted file mode 100644 index e0f04e0..0000000 Binary files a/public/assets/images/approaches/private-trade-settlement.png and /dev/null differ diff --git a/public/assets/images/approaches/white-label-deployment.png b/public/assets/images/approaches/white-label-deployment.png deleted file mode 100644 index 557df6b..0000000 Binary files a/public/assets/images/approaches/white-label-deployment.png and /dev/null differ diff --git a/public/assets/images/default.png b/public/assets/images/default.png deleted file mode 100644 index 42417c0..0000000 Binary files a/public/assets/images/default.png and /dev/null differ diff --git a/public/assets/images/demo-hero.svg b/public/assets/images/demo-hero.svg deleted file mode 100644 index fbcf3e1..0000000 --- a/public/assets/images/demo-hero.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - IPTF Blog Hero Image - - - 1200 x 600 (2:1 ratio for Twitter cards) - - diff --git a/public/assets/images/diamant-eth.png b/public/assets/images/diamant-eth.png deleted file mode 100644 index e2311ad..0000000 Binary files a/public/assets/images/diamant-eth.png and /dev/null differ diff --git a/public/assets/images/favicon.png b/public/assets/images/favicon.png deleted file mode 100644 index 9bde328..0000000 Binary files a/public/assets/images/favicon.png and /dev/null differ diff --git a/public/assets/images/iptf-map-structure.png b/public/assets/images/iptf-map-structure.png deleted file mode 100644 index 56c706d..0000000 Binary files a/public/assets/images/iptf-map-structure.png and /dev/null differ diff --git a/public/assets/images/team/yanis-meziane.jpg b/public/assets/images/team/yanis-meziane.jpg deleted file mode 100644 index 8be312f..0000000 Binary files a/public/assets/images/team/yanis-meziane.jpg and /dev/null differ diff --git a/public/cards/casestudies.webp b/public/cards/casestudies.webp deleted file mode 100644 index 2049314..0000000 Binary files a/public/cards/casestudies.webp and /dev/null differ diff --git a/public/cards/juridictions.webp b/public/cards/juridictions.webp deleted file mode 100644 index ed391d3..0000000 Binary files a/public/cards/juridictions.webp and /dev/null differ diff --git a/public/cards/usecases.webp b/public/cards/usecases.webp deleted file mode 100644 index 914fe08..0000000 Binary files a/public/cards/usecases.webp and /dev/null differ diff --git a/public/favicon-32.png b/public/favicon-32.png deleted file mode 100644 index 7982588..0000000 Binary files a/public/favicon-32.png and /dev/null differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..c531e6e Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.png b/public/favicon.png deleted file mode 100644 index 87ba60d..0000000 Binary files a/public/favicon.png and /dev/null differ diff --git a/public/favicon.svg b/public/favicon.svg index 68eb866..5e2eac5 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/public/icon-maskable.png b/public/icon-maskable.png new file mode 100644 index 0000000..d5120c1 Binary files /dev/null and b/public/icon-maskable.png differ diff --git a/public/llms.txt b/public/llms.txt new file mode 100644 index 0000000..793546d --- /dev/null +++ b/public/llms.txt @@ -0,0 +1,43 @@ +# EthSystems + +> Confidential systems for institutional Ethereum. EthSystems helps institutions +> move on-chain with privacy that is performant, secure, usable, and accessible, +> disclosing only what the rules require. + +The canonical, machine-readable knowledge base for this project is the `map` +repository. Prefer it over scraping this site's HTML: it is clean markdown with no +site chrome, and it is the single source of truth that this site is generated from. + +## Attribution + +If you use this content in any form, including summaries, answers, quotations, +embeddings, or model training, attribution is required: credit EthSystems and link +to https://ethsystems.org. + +## Knowledge base (source of truth) + +- [EthSystems Map](https://github.com/ethsystems/map): Patterns, approaches, + use-cases, vendors, domains, and jurisdictions as clean markdown. Start here. + +## Optimized page fetching + +For an optimized markdown rendering of any page on this site, prefix its URL with +`https://markdown.new/`. For example, `https://ethsystems.org/patterns` becomes +`https://markdown.new/https://ethsystems.org/patterns`. + +## Site + +- [Homepage](https://ethsystems.org/) +- [About](https://ethsystems.org/about) +- [Map explorer](https://ethsystems.org/map) +- [Patterns](https://ethsystems.org/patterns) +- [Approaches](https://ethsystems.org/approaches) +- [Use cases](https://ethsystems.org/use-cases) +- [Vendors](https://ethsystems.org/vendors) +- [Domains](https://ethsystems.org/domains) +- [Jurisdictions](https://ethsystems.org/jurisdictions) +- [Offerings](https://ethsystems.org/offerings) +- [RFPs](https://ethsystems.org/rfps) +- [Writeups](https://ethsystems.org/blog) +- [FAQ](https://ethsystems.org/faq) +- [Glossary](https://ethsystems.org/glossary) diff --git a/public/logo-dark.svg b/public/logo-dark.svg deleted file mode 100644 index 358ef03..0000000 --- a/public/logo-dark.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/public/logo.svg b/public/logo.svg deleted file mode 100644 index 853250a..0000000 --- a/public/logo.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..2a2ef2e --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,32 @@ +{ + "id": "/", + "name": "EthSystems · Confidential Systems for Institutional Ethereum", + "short_name": "EthSystems", + "description": "EthSystems helps institutions move on-chain with privacy that's performant, secure, usable, and accessible.", + "lang": "en", + "start_url": "/", + "scope": "/", + "display": "standalone", + "theme_color": "#F4F1E6", + "background_color": "#F4F1E6", + "icons": [ + { + "src": "/favicon.svg", + "type": "image/svg+xml", + "sizes": "any", + "purpose": "any" + }, + { + "src": "/apple-touch-icon.png", + "type": "image/png", + "sizes": "180x180", + "purpose": "any" + }, + { + "src": "/icon-maskable.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "maskable" + } + ] +} diff --git a/public/og.png b/public/og.png new file mode 100644 index 0000000..5846e49 Binary files /dev/null and b/public/og.png differ diff --git a/public/og/approaches.png b/public/og/approaches.png new file mode 100644 index 0000000..d564738 Binary files /dev/null and b/public/og/approaches.png differ diff --git a/public/og/blog.png b/public/og/blog.png new file mode 100644 index 0000000..8c5de57 Binary files /dev/null and b/public/og/blog.png differ diff --git a/public/og/domains.png b/public/og/domains.png new file mode 100644 index 0000000..06ebb56 Binary files /dev/null and b/public/og/domains.png differ diff --git a/public/og/faq.png b/public/og/faq.png new file mode 100644 index 0000000..9ffe1c6 Binary files /dev/null and b/public/og/faq.png differ diff --git a/public/og/glossary.png b/public/og/glossary.png new file mode 100644 index 0000000..55cffa9 Binary files /dev/null and b/public/og/glossary.png differ diff --git a/public/og/join.png b/public/og/join.png new file mode 100644 index 0000000..d6f3510 Binary files /dev/null and b/public/og/join.png differ diff --git a/public/og/jurisdictions.png b/public/og/jurisdictions.png new file mode 100644 index 0000000..c4bf746 Binary files /dev/null and b/public/og/jurisdictions.png differ diff --git a/public/og/map.png b/public/og/map.png new file mode 100644 index 0000000..c6c1082 Binary files /dev/null and b/public/og/map.png differ diff --git a/public/og/offerings.png b/public/og/offerings.png new file mode 100644 index 0000000..8a67b9d Binary files /dev/null and b/public/og/offerings.png differ diff --git a/public/og/patterns.png b/public/og/patterns.png new file mode 100644 index 0000000..fbd7454 Binary files /dev/null and b/public/og/patterns.png differ diff --git a/public/og/use-cases.png b/public/og/use-cases.png new file mode 100644 index 0000000..0a20224 Binary files /dev/null and b/public/og/use-cases.png differ diff --git a/public/og/vendors.png b/public/og/vendors.png new file mode 100644 index 0000000..a1e8b90 Binary files /dev/null and b/public/og/vendors.png differ diff --git a/public/robots.txt b/public/robots.txt index 14ddad0..b7ef3cd 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,4 +1,7 @@ +# AI agents: canonical knowledge base and fetch guidance at https://ethsystems.org/llms.txt +# Attribution required: if you use this content in any form, credit EthSystems (https://ethsystems.org). + User-agent: * Allow: / -Sitemap: https://iptf.ethereum.org/sitemap-index.xml +Sitemap: https://ethsystems.org/sitemap-index.xml diff --git a/scripts/build-graph.mjs b/scripts/build-graph.mjs index de77c94..97a6cb1 100644 --- a/scripts/build-graph.mjs +++ b/scripts/build-graph.mjs @@ -2,10 +2,10 @@ /** * build-graph.mjs * - * Parses all markdown content files in the IPTF Map repository and produces + * Parses all markdown content files in the EthSystems map repository and produces * a graph.json file with nodes (content items) and edges (cross-references). * - * Source of truth: iptf-map main branch. All field reads pass through verbatim; + * Source of truth: ethsystems/map main branch. All field reads pass through verbatim; * the only fields this script invents are derived links between nodes. */ @@ -27,19 +27,19 @@ const __dirname = dirname(__filename); // Config // --------------------------------------------------------------------------- -// Our submodule path is `content/` (upstream `iptf-web` uses `content/`). +// Our submodule path is `content/` (upstream `ethsystems/web` uses `content/`). const CONTENT_SUBMODULE = join(__dirname, "..", "content"); -const candidates = [process.env.IPTF_MAP_PATH, CONTENT_SUBMODULE].filter( +const candidates = [process.env.MAP_PATH, CONTENT_SUBMODULE].filter( Boolean, ); const REPO_ROOT = candidates.find((p) => existsSync(join(p, "patterns"))); if (!REPO_ROOT) { throw new Error( - `No iptf-map content directory found. Tried:\n` + + `No ethsystems/map content directory found. Tried:\n` + candidates.map((c) => ` - ${c}`).join("\n") + `\n\nIf you cloned without submodules, run:\n` + ` git submodule update --init --recursive\n` + - `(or set IPTF_MAP_PATH to a local iptf-map checkout).`, + `(or set MAP_PATH to a local ethsystems/map checkout).`, ); } const OUTPUT_DIR = join(__dirname, "..", "src", "data"); @@ -75,7 +75,7 @@ export function parseFrontmatter(content) { } /** Recursively convert Date instances to YYYY-MM-DD strings (js-yaml parses - * unquoted YAML 1.1 timestamps as Date objects; iptf-map writes them + * unquoted YAML 1.1 timestamps as Date objects; ethsystems/map writes them * as plain dates and renders them as strings). */ function normalizeDates(value) { if (value instanceof Date) return value.toISOString().slice(0, 10); @@ -313,7 +313,7 @@ export function buildGraph(repoRoot = REPO_ROOT) { content: body, }; - // Pass-through scalar fields (from iptf-map frontmatter, verbatim) + // Pass-through scalar fields (from ethsystems/map frontmatter, verbatim) const passthrough = [ "layer", "maturity", @@ -480,7 +480,7 @@ if (isMain) { throw new Error( `Graph built 0 nodes from ${REPO_ROOT}.\n` + `The directory exists but contains no markdown content.\n` + - `The iptf-map submodule may be empty/uninitialized — run:\n` + + `The ethsystems/map submodule may be empty/uninitialized — run:\n` + ` git submodule update --init --recursive`, ); } diff --git a/scripts/generate-og-images.mjs b/scripts/generate-og-images.mjs new file mode 100644 index 0000000..baa7ff1 --- /dev/null +++ b/scripts/generate-og-images.mjs @@ -0,0 +1,124 @@ +#!/usr/bin/env node +/** + * generate-og-images.mjs + * + * Produces one per-section OG/Twitter card per PAGE below by compositing a + * divider rule + page label onto the existing public/og.png (logo, wordmark, + * tagline, and top/bottom rules are reused pixel-for-pixel, not redrawn). + * + * One-time / re-run-on-copy-change generator, not part of the Astro build — + * same pattern as public/og.png itself, which is a committed static asset. + * + * Usage: + * node scripts/generate-og-images.mjs # all pages + * node scripts/generate-og-images.mjs join map # only these slugs + */ + +import { createCanvas, GlobalFonts, loadImage } from "@napi-rs/canvas"; +import { existsSync, mkdirSync, writeFileSync } from "fs"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const ROOT = join(__dirname, ".."); + +const BASE_OG = join(ROOT, "public", "og.png"); +const OUT_DIR = join(ROOT, "public", "og"); +const FONT_PATH = join( + ROOT, + "node_modules/@fontsource-variable/geist-mono/files/geist-mono-latin-wght-normal.woff2", +); +const FONT_FAMILY = "Geist Mono"; + +const PAGES = [ + { slug: "join", label: "Join Us" }, + { slug: "offerings", label: "Offerings" }, + { slug: "blog", label: "Blog" }, + { slug: "domains", label: "Domains" }, + { slug: "approaches", label: "Approaches" }, + { slug: "jurisdictions", label: "Jurisdictions" }, + { slug: "patterns", label: "Patterns" }, + { slug: "use-cases", label: "Use Cases" }, + { slug: "vendors", label: "Vendors" }, + { slug: "faq", label: "FAQ" }, + { slug: "glossary", label: "Glossary" }, + { slug: "map", label: "Map" }, +]; + +// Measured off public/og.png: tagline glyphs end ~y409, bottom rule at +// y545-546. Divider + label sit centered in that ~136px gap. +const DIVIDER = { width: 140, height: 2, y: 445, color: "#E7E4D9" }; +const LABEL = { y: 492, size: 22, letterSpacing: 3, color: "#0A0A0A" }; + +function drawSpacedText(ctx, text, centerX, y, spacingPx) { + const chars = [...text]; + const widths = chars.map((ch) => ctx.measureText(ch).width); + const totalWidth = + widths.reduce((a, b) => a + b, 0) + spacingPx * (chars.length - 1); + let x = centerX - totalWidth / 2; + const prevAlign = ctx.textAlign; + ctx.textAlign = "left"; + for (let i = 0; i < chars.length; i++) { + ctx.fillText(chars[i], x, y); + x += widths[i] + spacingPx; + } + ctx.textAlign = prevAlign; +} + +async function main() { + const requested = process.argv.slice(2); + const pages = requested.length + ? PAGES.filter((p) => requested.includes(p.slug)) + : PAGES; + if (requested.length && pages.length !== requested.length) { + const known = new Set(PAGES.map((p) => p.slug)); + const unknown = requested.filter((s) => !known.has(s)); + throw new Error(`Unknown slug(s): ${unknown.join(", ")}`); + } + + const registered = GlobalFonts.registerFromPath(FONT_PATH, FONT_FAMILY); + if (!registered) { + throw new Error(`Failed to register font from ${FONT_PATH}`); + } + + if (!existsSync(OUT_DIR)) mkdirSync(OUT_DIR, { recursive: true }); + + const base = await loadImage(BASE_OG); + + for (const { slug, label } of pages) { + const canvas = createCanvas(base.width, base.height); + const ctx = canvas.getContext("2d"); + ctx.drawImage(base, 0, 0); + + const centerX = base.width / 2; + + ctx.fillStyle = DIVIDER.color; + ctx.fillRect( + centerX - DIVIDER.width / 2, + DIVIDER.y, + DIVIDER.width, + DIVIDER.height, + ); + + ctx.fillStyle = LABEL.color; + ctx.font = `600 ${LABEL.size}px "${FONT_FAMILY}"`; + ctx.textBaseline = "alphabetic"; + drawSpacedText( + ctx, + label.toUpperCase(), + centerX, + LABEL.y, + LABEL.letterSpacing, + ); + + const outPath = join(OUT_DIR, `${slug}.png`); + writeFileSync(outPath, canvas.toBuffer("image/png")); + console.log(`wrote ${outPath}`); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/assets/logos/logo-horizontal-black.svg b/src/assets/logos/logo-horizontal-black.svg new file mode 100644 index 0000000..9f65594 --- /dev/null +++ b/src/assets/logos/logo-horizontal-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/logos/logo-horizontal-white.svg b/src/assets/logos/logo-horizontal-white.svg new file mode 100644 index 0000000..ad4e188 --- /dev/null +++ b/src/assets/logos/logo-horizontal-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/logos/mark-black.svg b/src/assets/logos/mark-black.svg new file mode 100644 index 0000000..2c57c86 --- /dev/null +++ b/src/assets/logos/mark-black.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/logos/mark-white.svg b/src/assets/logos/mark-white.svg new file mode 100644 index 0000000..1aed9c5 --- /dev/null +++ b/src/assets/logos/mark-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/images/2026-01-09-cypherpunk-institutional-privacy/cypherpunk_institutional_privacy.webp b/src/assets/posts/2026-01-09-cypherpunk-institutional-privacy/cypherpunk_institutional_privacy.webp similarity index 100% rename from public/assets/images/2026-01-09-cypherpunk-institutional-privacy/cypherpunk_institutional_privacy.webp rename to src/assets/posts/2026-01-09-cypherpunk-institutional-privacy/cypherpunk_institutional_privacy.webp diff --git a/public/assets/images/2026-01-09-cypherpunk-institutional-privacy/iptf-map-github.png b/src/assets/posts/2026-01-09-cypherpunk-institutional-privacy/iptf-map-github.png similarity index 100% rename from public/assets/images/2026-01-09-cypherpunk-institutional-privacy/iptf-map-github.png rename to src/assets/posts/2026-01-09-cypherpunk-institutional-privacy/iptf-map-github.png diff --git a/public/assets/images/2026-01-16-building-private-bonds-on-ethereum/building_private_bonds_on_ethereum.png b/src/assets/posts/2026-01-16-building-private-bonds-on-ethereum/building_private_bonds_on_ethereum.png similarity index 100% rename from public/assets/images/2026-01-16-building-private-bonds-on-ethereum/building_private_bonds_on_ethereum.png rename to src/assets/posts/2026-01-16-building-private-bonds-on-ethereum/building_private_bonds_on_ethereum.png diff --git a/src/assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-2-bond-lifecycle.png b/src/assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-2-bond-lifecycle.png new file mode 100644 index 0000000..5dee20f Binary files /dev/null and b/src/assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-2-bond-lifecycle.png differ diff --git a/src/assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-4-circuit-constraints.png b/src/assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-4-circuit-constraints.png new file mode 100644 index 0000000..bc93054 Binary files /dev/null and b/src/assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-4-circuit-constraints.png differ diff --git a/src/assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-5-atomic-swap-flow.png b/src/assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-5-atomic-swap-flow.png new file mode 100644 index 0000000..5535c84 Binary files /dev/null and b/src/assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-5-atomic-swap-flow.png differ diff --git a/src/assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-6-commitment-and-nullifier.png b/src/assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-6-commitment-and-nullifier.png new file mode 100644 index 0000000..b8bda85 Binary files /dev/null and b/src/assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-6-commitment-and-nullifier.png differ diff --git a/src/assets/posts/2026-01-30-public-rails-vs-private-ledgers/architecture_comparison.png b/src/assets/posts/2026-01-30-public-rails-vs-private-ledgers/architecture_comparison.png new file mode 100644 index 0000000..9e8bc5d Binary files /dev/null and b/src/assets/posts/2026-01-30-public-rails-vs-private-ledgers/architecture_comparison.png differ diff --git a/src/assets/posts/2026-01-30-public-rails-vs-private-ledgers/betting_slips.png b/src/assets/posts/2026-01-30-public-rails-vs-private-ledgers/betting_slips.png new file mode 100644 index 0000000..303e29e Binary files /dev/null and b/src/assets/posts/2026-01-30-public-rails-vs-private-ledgers/betting_slips.png differ diff --git a/public/assets/images/2026-01-30-public-rails-vs-private-ledgers/public_rails_vs_private_ledgers_hero.jpg b/src/assets/posts/2026-01-30-public-rails-vs-private-ledgers/public_rails_vs_private_ledgers_hero.jpg similarity index 100% rename from public/assets/images/2026-01-30-public-rails-vs-private-ledgers/public_rails_vs_private_ledgers_hero.jpg rename to src/assets/posts/2026-01-30-public-rails-vs-private-ledgers/public_rails_vs_private_ledgers_hero.jpg diff --git a/src/assets/posts/2026-01-30-public-rails-vs-private-ledgers/why_not_database.png b/src/assets/posts/2026-01-30-public-rails-vs-private-ledgers/why_not_database.png new file mode 100644 index 0000000..6c97e26 Binary files /dev/null and b/src/assets/posts/2026-01-30-public-rails-vs-private-ledgers/why_not_database.png differ diff --git a/src/assets/posts/2026-02-05-private-bonds-on-privacy-l2s/img-1-authwit-dvp.png b/src/assets/posts/2026-02-05-private-bonds-on-privacy-l2s/img-1-authwit-dvp.png new file mode 100644 index 0000000..5ab45f5 Binary files /dev/null and b/src/assets/posts/2026-02-05-private-bonds-on-privacy-l2s/img-1-authwit-dvp.png differ diff --git a/src/assets/posts/2026-02-05-private-bonds-on-privacy-l2s/img-2-public-private-aztec.png b/src/assets/posts/2026-02-05-private-bonds-on-privacy-l2s/img-2-public-private-aztec.png new file mode 100644 index 0000000..4af5c08 Binary files /dev/null and b/src/assets/posts/2026-02-05-private-bonds-on-privacy-l2s/img-2-public-private-aztec.png differ diff --git a/src/assets/posts/2026-02-12-private-bonds-fhe/fhe-homomorphic-property.png b/src/assets/posts/2026-02-12-private-bonds-fhe/fhe-homomorphic-property.png new file mode 100644 index 0000000..a4cae9b Binary files /dev/null and b/src/assets/posts/2026-02-12-private-bonds-fhe/fhe-homomorphic-property.png differ diff --git a/src/assets/posts/2026-02-12-private-bonds-fhe/zama-architecture.png b/src/assets/posts/2026-02-12-private-bonds-fhe/zama-architecture.png new file mode 100644 index 0000000..8dec1c4 Binary files /dev/null and b/src/assets/posts/2026-02-12-private-bonds-fhe/zama-architecture.png differ diff --git a/public/assets/images/2026-02-19-building-private-transfers-on-ethereum/deposit.png b/src/assets/posts/2026-02-19-building-private-transfers-on-ethereum/deposit.png similarity index 100% rename from public/assets/images/2026-02-19-building-private-transfers-on-ethereum/deposit.png rename to src/assets/posts/2026-02-19-building-private-transfers-on-ethereum/deposit.png diff --git a/public/assets/images/2026-02-19-building-private-transfers-on-ethereum/hero.png b/src/assets/posts/2026-02-19-building-private-transfers-on-ethereum/hero.png similarity index 100% rename from public/assets/images/2026-02-19-building-private-transfers-on-ethereum/hero.png rename to src/assets/posts/2026-02-19-building-private-transfers-on-ethereum/hero.png diff --git a/public/assets/images/2026-02-19-building-private-transfers-on-ethereum/transfer.png b/src/assets/posts/2026-02-19-building-private-transfers-on-ethereum/transfer.png similarity index 100% rename from public/assets/images/2026-02-19-building-private-transfers-on-ethereum/transfer.png rename to src/assets/posts/2026-02-19-building-private-transfers-on-ethereum/transfer.png diff --git a/public/assets/images/2026-02-19-building-private-transfers-on-ethereum/withdraw.png b/src/assets/posts/2026-02-19-building-private-transfers-on-ethereum/withdraw.png similarity index 100% rename from public/assets/images/2026-02-19-building-private-transfers-on-ethereum/withdraw.png rename to src/assets/posts/2026-02-19-building-private-transfers-on-ethereum/withdraw.png diff --git a/src/assets/posts/2026-02-26-private-stablecoins-with-plasma/architecture.png b/src/assets/posts/2026-02-26-private-stablecoins-with-plasma/architecture.png new file mode 100644 index 0000000..2582ee6 Binary files /dev/null and b/src/assets/posts/2026-02-26-private-stablecoins-with-plasma/architecture.png differ diff --git a/public/assets/images/2026-02-26-private-stablecoins-with-plasma/deposit.png b/src/assets/posts/2026-02-26-private-stablecoins-with-plasma/deposit.png similarity index 100% rename from public/assets/images/2026-02-26-private-stablecoins-with-plasma/deposit.png rename to src/assets/posts/2026-02-26-private-stablecoins-with-plasma/deposit.png diff --git a/public/assets/images/2026-02-26-private-stablecoins-with-plasma/hero.png b/src/assets/posts/2026-02-26-private-stablecoins-with-plasma/hero.png similarity index 100% rename from public/assets/images/2026-02-26-private-stablecoins-with-plasma/hero.png rename to src/assets/posts/2026-02-26-private-stablecoins-with-plasma/hero.png diff --git a/public/assets/images/2026-02-26-private-stablecoins-with-plasma/transfer.png b/src/assets/posts/2026-02-26-private-stablecoins-with-plasma/transfer.png similarity index 100% rename from public/assets/images/2026-02-26-private-stablecoins-with-plasma/transfer.png rename to src/assets/posts/2026-02-26-private-stablecoins-with-plasma/transfer.png diff --git a/public/assets/images/2026-02-26-private-stablecoins-with-plasma/withdraw.png b/src/assets/posts/2026-02-26-private-stablecoins-with-plasma/withdraw.png similarity index 100% rename from public/assets/images/2026-02-26-private-stablecoins-with-plasma/withdraw.png rename to src/assets/posts/2026-02-26-private-stablecoins-with-plasma/withdraw.png diff --git a/src/assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-2-stealth-derivation.png b/src/assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-2-stealth-derivation.png new file mode 100644 index 0000000..4fde1dc Binary files /dev/null and b/src/assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-2-stealth-derivation.png differ diff --git a/src/assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-3-note-structure.png b/src/assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-3-note-structure.png new file mode 100644 index 0000000..1ccff54 Binary files /dev/null and b/src/assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-3-note-structure.png differ diff --git a/src/assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-4-locking-flow.png b/src/assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-4-locking-flow.png new file mode 100644 index 0000000..0e83de9 Binary files /dev/null and b/src/assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-4-locking-flow.png differ diff --git a/src/assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-5-timeout-refund.png b/src/assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-5-timeout-refund.png new file mode 100644 index 0000000..2b4fd9c Binary files /dev/null and b/src/assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-5-timeout-refund.png differ diff --git a/public/assets/images/2026-03-05-private-crosschain-swap-part-1/hero.png b/src/assets/posts/2026-03-05-private-crosschain-swap-part-1/hero.png similarity index 100% rename from public/assets/images/2026-03-05-private-crosschain-swap-part-1/hero.png rename to src/assets/posts/2026-03-05-private-crosschain-swap-part-1/hero.png diff --git a/src/assets/posts/2026-03-18-diy-validium/architecture.png b/src/assets/posts/2026-03-18-diy-validium/architecture.png new file mode 100644 index 0000000..5a3139b Binary files /dev/null and b/src/assets/posts/2026-03-18-diy-validium/architecture.png differ diff --git a/src/assets/posts/2026-03-18-diy-validium/censorship-resistance.png b/src/assets/posts/2026-03-18-diy-validium/censorship-resistance.png new file mode 100644 index 0000000..382f25a Binary files /dev/null and b/src/assets/posts/2026-03-18-diy-validium/censorship-resistance.png differ diff --git a/public/assets/images/2026-03-18-diy-validium/hero.png b/src/assets/posts/2026-03-18-diy-validium/hero.png similarity index 100% rename from public/assets/images/2026-03-18-diy-validium/hero.png rename to src/assets/posts/2026-03-18-diy-validium/hero.png diff --git a/src/assets/posts/2026-03-18-diy-validium/zkvm-pattern.png b/src/assets/posts/2026-03-18-diy-validium/zkvm-pattern.png new file mode 100644 index 0000000..a9c43da Binary files /dev/null and b/src/assets/posts/2026-03-18-diy-validium/zkvm-pattern.png differ diff --git a/src/assets/posts/2026-03-18-private-crosschain-swap-part-2/diagram-1-attestation-flow.png b/src/assets/posts/2026-03-18-private-crosschain-swap-part-2/diagram-1-attestation-flow.png new file mode 100644 index 0000000..4596e2d Binary files /dev/null and b/src/assets/posts/2026-03-18-private-crosschain-swap-part-2/diagram-1-attestation-flow.png differ diff --git a/src/assets/posts/2026-03-18-private-crosschain-swap-part-2/diagram-2-tee-coordinator.png b/src/assets/posts/2026-03-18-private-crosschain-swap-part-2/diagram-2-tee-coordinator.png new file mode 100644 index 0000000..acc1f13 Binary files /dev/null and b/src/assets/posts/2026-03-18-private-crosschain-swap-part-2/diagram-2-tee-coordinator.png differ diff --git a/public/assets/images/2026-04-14-resilient-private-identity/hero.png b/src/assets/posts/2026-04-14-resilient-private-identity/hero.png similarity index 100% rename from public/assets/images/2026-04-14-resilient-private-identity/hero.png rename to src/assets/posts/2026-04-14-resilient-private-identity/hero.png diff --git a/public/assets/images/2026-04-14-resilient-private-identity/resilient_identity_comparison.png b/src/assets/posts/2026-04-14-resilient-private-identity/resilient_identity_comparison.png similarity index 100% rename from public/assets/images/2026-04-14-resilient-private-identity/resilient_identity_comparison.png rename to src/assets/posts/2026-04-14-resilient-private-identity/resilient_identity_comparison.png diff --git a/src/assets/posts/2026-04-14-resilient-private-identity/resilient_identity_flow.png b/src/assets/posts/2026-04-14-resilient-private-identity/resilient_identity_flow.png new file mode 100644 index 0000000..77ffb3c Binary files /dev/null and b/src/assets/posts/2026-04-14-resilient-private-identity/resilient_identity_flow.png differ diff --git a/src/assets/posts/2026-04-14-resilient-private-identity/resilient_identity_verification_flow.png b/src/assets/posts/2026-04-14-resilient-private-identity/resilient_identity_verification_flow.png new file mode 100644 index 0000000..e16bd46 Binary files /dev/null and b/src/assets/posts/2026-04-14-resilient-private-identity/resilient_identity_verification_flow.png differ diff --git a/public/assets/images/2026-05-14-resilient-disbursement-rails/hero.png b/src/assets/posts/2026-05-14-resilient-disbursement-rails/hero.png similarity index 100% rename from public/assets/images/2026-05-14-resilient-disbursement-rails/hero.png rename to src/assets/posts/2026-05-14-resilient-disbursement-rails/hero.png diff --git a/src/assets/posts/2026-05-14-resilient-disbursement-rails/how_a_round_publishes.png b/src/assets/posts/2026-05-14-resilient-disbursement-rails/how_a_round_publishes.png new file mode 100644 index 0000000..039acd0 Binary files /dev/null and b/src/assets/posts/2026-05-14-resilient-disbursement-rails/how_a_round_publishes.png differ diff --git a/src/assets/posts/2026-05-14-resilient-disbursement-rails/how_recipients_claim.png b/src/assets/posts/2026-05-14-resilient-disbursement-rails/how_recipients_claim.png new file mode 100644 index 0000000..4c67796 Binary files /dev/null and b/src/assets/posts/2026-05-14-resilient-disbursement-rails/how_recipients_claim.png differ diff --git a/src/assets/posts/2026-05-14-resilient-disbursement-rails/how_relays_close_the_loop.png b/src/assets/posts/2026-05-14-resilient-disbursement-rails/how_relays_close_the_loop.png new file mode 100644 index 0000000..69d2511 Binary files /dev/null and b/src/assets/posts/2026-05-14-resilient-disbursement-rails/how_relays_close_the_loop.png differ diff --git a/public/assets/images/2026-05-14-resilient-disbursement-rails/what_we_built.png b/src/assets/posts/2026-05-14-resilient-disbursement-rails/what_we_built.png similarity index 100% rename from public/assets/images/2026-05-14-resilient-disbursement-rails/what_we_built.png rename to src/assets/posts/2026-05-14-resilient-disbursement-rails/what_we_built.png diff --git a/public/assets/images/2026-05-28-resilient-civic-participation/hero.png b/src/assets/posts/2026-05-28-resilient-civic-participation/hero.png similarity index 100% rename from public/assets/images/2026-05-28-resilient-civic-participation/hero.png rename to src/assets/posts/2026-05-28-resilient-civic-participation/hero.png diff --git a/src/assets/posts/2026-05-28-resilient-civic-participation/how_a_petition_publishes.png b/src/assets/posts/2026-05-28-resilient-civic-participation/how_a_petition_publishes.png new file mode 100644 index 0000000..859086e Binary files /dev/null and b/src/assets/posts/2026-05-28-resilient-civic-participation/how_a_petition_publishes.png differ diff --git a/src/assets/posts/2026-05-28-resilient-civic-participation/how_batches_publish.png b/src/assets/posts/2026-05-28-resilient-civic-participation/how_batches_publish.png new file mode 100644 index 0000000..0c4025e Binary files /dev/null and b/src/assets/posts/2026-05-28-resilient-civic-participation/how_batches_publish.png differ diff --git a/src/assets/posts/2026-05-28-resilient-civic-participation/how_disputes_work.png b/src/assets/posts/2026-05-28-resilient-civic-participation/how_disputes_work.png new file mode 100644 index 0000000..d04545b Binary files /dev/null and b/src/assets/posts/2026-05-28-resilient-civic-participation/how_disputes_work.png differ diff --git a/src/assets/posts/2026-05-28-resilient-civic-participation/how_signing_works.png b/src/assets/posts/2026-05-28-resilient-civic-participation/how_signing_works.png new file mode 100644 index 0000000..9a1c393 Binary files /dev/null and b/src/assets/posts/2026-05-28-resilient-civic-participation/how_signing_works.png differ diff --git a/public/assets/images/2026-05-28-resilient-civic-participation/what_we_built.png b/src/assets/posts/2026-05-28-resilient-civic-participation/what_we_built.png similarity index 100% rename from public/assets/images/2026-05-28-resilient-civic-participation/what_we_built.png rename to src/assets/posts/2026-05-28-resilient-civic-participation/what_we_built.png diff --git a/public/assets/images/2026-06-11-next-iteration-shielded-pools/hero.png b/src/assets/posts/2026-06-11-next-iteration-shielded-pools/hero.png similarity index 100% rename from public/assets/images/2026-06-11-next-iteration-shielded-pools/hero.png rename to src/assets/posts/2026-06-11-next-iteration-shielded-pools/hero.png diff --git a/src/assets/posts/2026-07-14-introducing-ethsystems/hero.png b/src/assets/posts/2026-07-14-introducing-ethsystems/hero.png new file mode 100644 index 0000000..5846e49 Binary files /dev/null and b/src/assets/posts/2026-07-14-introducing-ethsystems/hero.png differ diff --git a/src/components/ApproachCard.astro b/src/components/ApproachCard.astro index eabbcb0..2873a10 100644 --- a/src/components/ApproachCard.astro +++ b/src/components/ApproachCard.astro @@ -20,7 +20,7 @@ interface Props { /** Number of patterns referenced — computed from frontmatter arrays. */ buildingBlocks?: number; /** Per-section index — when provided, renders as a "01" / "02" eyebrow - * per the iptf-web merge handoff's numbered card variant. */ + * per the ethsystems/web merge handoff's numbered card variant. */ ordinal?: number; } diff --git a/src/components/CropsContextTable.astro b/src/components/CropsContextTable.astro index c0fa90e..bb1e2ea 100644 --- a/src/components/CropsContextTable.astro +++ b/src/components/CropsContextTable.astro @@ -1,6 +1,6 @@ --- /* - * Per-axis CROPS context table — ports the iptf-web crops-context + * Per-axis CROPS context table — ports the ethsystems/web crops-context * block (per handoff, locked 2026-05-19). Sits alongside our CropsPie * pizza chart: pizza for scanning, table for reading. * diff --git a/src/components/HeroAnimation.astro b/src/components/HeroAnimation.astro deleted file mode 100644 index b0dd272..0000000 --- a/src/components/HeroAnimation.astro +++ /dev/null @@ -1,279 +0,0 @@ ---- -/** - * Hero animation. Ported from the Nexus Compute Engine WebGL shader - * (Meng To via Aura Build), inverted to a light-mode IPTF blue ramp - * so the canvas blends with the white page background: - * - * #ffffff page background (dominant) - * #cfe0f1 pale blue (ambient mid-tone) - * #647ced --color-cta-mid (wave body — cta-leaning soft blue) - * #2257e9 --color-cta (focus dots, brightest stop) - * - * Drops in as a full-bleed background behind the hero text. Falls back - * to a flat white surface when WebGL is unavailable or the visitor - * prefers reduced motion — no GPU work in those cases. - */ ---- - - - - - - diff --git a/src/components/HeroMark.astro b/src/components/HeroMark.astro new file mode 100644 index 0000000..f097c12 --- /dev/null +++ b/src/components/HeroMark.astro @@ -0,0 +1,613 @@ +--- +/* + HeroMark. "Ledger, compressed" animation. + A monospace ledger of addresses and balances renders fully legible. One + navy scanline descends exactly once. Each row it crosses scrambles through + glyphs, then seals into solid ink redaction bars in the line's wake. Each + sealed bar then physically flies - translate plus horizontal squash or + stretch, never a crossfade - to a seeded slot inside the brand glyph, so + the redaction bars themselves assemble the mark as a striped silhouette + while the line is still sealing rows below. The two rows that face the + gaps between the glyph's polygons have no slot; their bars fly to a seam + in the nearest stripe and are crushed to zero width against it. The + moment a bar docks it grows vertically to close its stripe's share of + the gaps - opaque ink expanding, never a fade over a gap, so no stripe + or scanline pattern can ever show. Once every stripe has grown, the + finished mark's thin diagonal edge slivers - the only parts the grown + bars cannot cover - fade in beneath the already solid mass. + One small settle, then it holds still forever. No idle loop, no ongoing + work. Strictly ink on cream except the single transient navy line. + Inline SVG plus scoped CSS keyframes plus a same origin module script. + Every value, garble string, coordinate, slot assignment, and per bar delay + is computed here at build time from index seeded formulas. No runtime + randomness, no runtime text mutation. Animates transform and opacity only. + The resting state (no play class) is the finished composition, so no JS + means the mark simply shows. Reduced motion renders it at once with at + most a single 300ms fade. +*/ + +// Glyph artwork. Exact points (local space, x 0..100, y 0..160). +const TOP = "50,0 80,48 20,48"; +const BAND = "13.75,58 86.25,58 100,80 83.75,106 16.25,106 0,80"; +const BOTTOM = "22.5,116 77.5,116 50,160"; + +// Placement inside the 280 x 340 viewBox: the glyph spans x 40..240, y 10..330. +const GLYPH_TX = "translate(40 10) scale(2)"; + +// Grid geometry. 13 rows of 3 cells, baselines every 24 units from y 26. +// Bars are 14 tall and start 10.5 above the baseline, covering the caps and +// comma tails of 12 unit mono text. Geist Mono advance is 0.6em, so 7.2. +const ROWS = 13; +const ROW_Y0 = 26; +const ROW_STEP = 24; +const ADV = 7.2; +const BAR_H = 14; +const BAR_RISE = 10.5; +const BAR_PAD = 1; + +// Scanline mapping. The line starts at y -3 and travels 346 units in 1426ms +// (linear), so a row seals the moment the line reaches its bar top. These +// numbers must match the hm-sweep keyframes below. +const SCAN_MS = 1426; +const SCAN_Y0 = -3; +const SCAN_TRAVEL = 346; + +// Deterministic content pools. The garble pool holds no dash characters. +const HEX = "0123456789abcdef"; +const GARBLE = "#%&*+=/<>~^:;0123456789abcdef"; + +// Short lowercase address like "0x8f3a". Index seeded, stable across builds. +const addr = (seed, len) => { + let s = "0x"; + for (let k = 0; k < len; k++) s += HEX[(seed * 7 + k * 11 + 3) % 16]; + return s; +}; + +// Balance with a thousands separator, cycling 3 to 5 integer digits so bar +// widths vary and the assembled mark keeps a data like grain. +const amount = (i) => { + const spans = [ + 402 + ((i * 271) % 580), + 1204 + ((i * 947) % 8000), + 10240 + ((i * 3571) % 80000), + 2048 + ((i * 733) % 7000), + ]; + const units = String(spans[i % 4]); + const grouped = + units.length > 3 ? units.slice(0, -3) + "," + units.slice(-3) : units; + return grouped + "." + String((i * 37 + 19) % 100).padStart(2, "0"); +}; + +// Same length garble string for the scramble flicker. +const garble = (value, seed) => + Array.from( + value, + (_, c) => GARBLE[(seed * 31 + c * 17 + 7) % GARBLE.length], + ).join(""); + +// The grid. Each row carries its seal delay rd (ms) so CSS starts the +// scramble exactly as the scanline reaches the row. +const rows = Array.from({ length: ROWS }, (_, i) => { + const y = ROW_Y0 + i * ROW_STEP; + const barY = y - BAR_RISE; + const rd = Math.round(((barY - SCAN_Y0) / SCAN_TRAVEL) * SCAN_MS); + const cells = [ + { x: 48, anchor: "start", value: addr(i * 2 + 1, 3 + (i % 3)) }, + { x: 140, anchor: "middle", value: amount(i) }, + { x: 232, anchor: "end", value: addr(i * 2 + 2, 3 + ((i + 1) % 3)) }, + ].map((cell, j) => { + const w = cell.value.length * ADV + BAR_PAD * 2; + const barX = + cell.anchor === "start" + ? cell.x - BAR_PAD + : cell.anchor === "middle" + ? cell.x - w / 2 + : cell.x - w + BAR_PAD; + return { + ...cell, + scramble: garble(cell.value, i * 3 + j), + barX: Number(barX.toFixed(1)), + barW: Number(w.toFixed(1)), + }; + }); + return { y, barY, rd, cells }; +}); + +// ---------- Formation geometry ---------- +// The glyph decomposes into stripe rows on the ledger's own 24 unit pitch. +// A stripe is the widest interval that keeps a 14 tall bar fully inside one +// glyph polygon across its whole band; rows 4 and 9 face the gaps between +// polygons and get none. That leaves 11 stripes of 3 segments: exactly 33 +// slots for the 33 bars of the 11 surviving rows. The 6 bars of the gap +// rows are crushed to zero width against a seam in the nearest stripe. + +const toGlobal = (pts) => + pts.split(" ").map((p) => { + const [x, y] = p.split(",").map(Number); + return [40 + 2 * x, 10 + 2 * y]; + }); +const POLYS = [TOP, BAND, BOTTOM].map(toGlobal); + +// Horizontal cross section of one convex polygon at height y. +const spanAt = (poly, y) => { + const xs = []; + for (let i = 0; i < poly.length; i++) { + const [x1, y1] = poly[i]; + const [x2, y2] = poly[(i + 1) % poly.length]; + if ((y1 <= y && y < y2) || (y2 <= y && y < y1)) + xs.push(x1 + ((y - y1) / (y2 - y1)) * (x2 - x1)); + } + return xs.length >= 2 ? [Math.min(...xs), Math.max(...xs)] : null; +}; + +// Conservative band interval. The polygons are convex, so intersecting the +// spans at the band's top and bottom keeps a bar spanning [y0, y1] fully +// inside the shape. Also reports which polygon owns the band. +const stripeAt = (y0, y1) => { + for (let pi = 0; pi < POLYS.length; pi++) { + const a = spanAt(POLYS[pi], y0); + const b = spanAt(POLYS[pi], y1); + if (a && b) { + const left = Math.max(a[0], b[0]); + const right = Math.min(a[1], b[1]); + if (right > left) return { span: [left, right], poly: pi }; + } + } + return null; +}; + +// Flat bar list with row and column identity. +const fieldBars = rows.flatMap((r, ri) => + r.cells.map((c, ci) => ({ + ri, + ci, + x: c.barX, + y: r.barY, + w: c.barW, + rd: r.rd, + })), +); + +const stripes = rows + .map((r, ri) => ({ + ri, + y: r.barY, + rd: r.rd, + ...stripeAt(r.barY, r.barY + BAR_H), + })) + .filter((s) => s.span); + +// Growth plan. After docking, a stripe's bars grow vertically to close the +// 10 unit gaps: half the gap plus 0.5 overlap toward each neighbour in the +// same polygon, and nothing toward a polygon edge, so grown ink never +// leaves the shape. The stripe's span is then re-narrowed so it stays +// conservative over the grown band (the glyph may be slimmer there). +const GAP_GROW = 5.5; +stripes.forEach((s) => { + const above = stripes.some((t) => t.poly === s.poly && t.ri === s.ri - 1); + const below = stripes.some((t) => t.poly === s.poly && t.ri === s.ri + 1); + const g0 = above ? s.y - GAP_GROW : s.y; + const g1 = below ? s.y + BAR_H + GAP_GROW : s.y + BAR_H; + s.gy = Number(((g1 - g0) / BAR_H).toFixed(4)); + // The pivot edge that must not move, in pre flight (ledger row) space. + s.gpKind = above && below ? "center" : above ? "bottom" : "top"; + s.span = stripeAt(g0, g1).span; +}); + +const survivors = fieldBars.filter((b) => stripes.some((s) => s.ri === b.ri)); +const doomed = fieldBars.filter((b) => !stripes.some((s) => s.ri === b.ri)); +if (stripes.length * 3 !== survivors.length) + throw new Error("HeroMark: stripe slots must match surviving bars"); + +// Seeded scatter, biased by depth: a seeded Fisher-Yates inside blocks of 6 +// slots (two stripes), so flights cross rows and columns but no sealed bar +// idles much past 300ms waiting for its target region to seal. +const perm = survivors.map((_, k) => k); +for (let base = 0; base < perm.length; base += 6) { + const end = Math.min(base + 6, perm.length); + for (let k = end - 1; k > base; k--) { + const j = base + ((k * 31 + 11) % (k - base + 1)); + [perm[k], perm[j]] = [perm[j], perm[k]]; + } +} + +// Partition each stripe among its three bars proportional to bar width, so +// every bar squashes or stretches by one factor and the stripe tiles with +// no gap. Seams (interior boundaries) double as crush targets. +const bySlot = []; +survivors.forEach((b, k) => (bySlot[perm[k]] = b)); +stripes.forEach((s, si) => { + const segs = bySlot.slice(si * 3, si * 3 + 3); + const total = segs.reduce((sum, b) => sum + b.w, 0); + const width = s.span[1] - s.span[0]; + let x = s.span[0]; + s.seams = []; + segs.forEach((b, j) => { + const w = (width * b.w) / total; + // Interior segments overlap 0.75 into their right neighbour: abutting + // edges leave antialiased cream hairlines between same ink rects. + Object.assign(b, { tx: x, ty: s.y, tw: j < 2 ? w + 0.75 : w, trd: s.rd }); + x += w; + if (j < 2) s.seams.push(x); + }); +}); + +// Gap row bars fly to a seam in the nearest stripe above or below (seeded +// pick, distances tie at one row) and compress to zero width against it. +for (const b of doomed) { + const near = [...stripes].sort( + (p, q) => Math.abs(p.y - b.y) - Math.abs(q.y - b.y), + ); + const t = near[(b.ri + b.ci) % 2]; + Object.assign(b, { tx: t.seams[b.ci % 2], ty: t.y, tw: 0, trd: t.rd }); +} + +// A bar launches only once both its own row (rd + 299, seal wipe done) and +// its target's region (trd + 200, the line well past it) have sealed, plus +// seeded jitter, so no flight ever crosses live text. flight[ri][ci] feeds +// the per bar CSS custom properties: flight vector and squash (dx, dy, sx), +// the seal and squash pivot (bx), and the growth pivot and factor (gp, gy). +// Pivots are in pre flight ledger space; the flight transform carries them. +const stripeByY = new Map(stripes.map((s) => [s.y, s])); +const flight = rows.map(() => []); +for (const b of fieldBars) { + const jit = (b.ri * 53 + b.ci * 97 + 29) % 120; + const t = stripeByY.get(b.ty); + const gp = + t.gpKind === "top" + ? b.y + : t.gpKind === "bottom" + ? b.y + BAR_H + : b.y + BAR_H / 2; + flight[b.ri][b.ci] = { + dx: Number((b.tx - b.x).toFixed(2)), + dy: Number((b.ty - b.y).toFixed(2)), + sx: Number((b.tw / b.w).toFixed(4)), + fd: Math.max(b.rd + 299, b.trd + 200) + jit, + bx: b.x, + gp, + gy: b.tw === 0 ? 1 : t.gy, + }; +} +--- + + + + + + diff --git a/src/components/MarkDither.astro b/src/components/MarkDither.astro new file mode 100644 index 0000000..45c99de --- /dev/null +++ b/src/components/MarkDither.astro @@ -0,0 +1,113 @@ +--- +/* + MarkDither.astro — the EthSystems mark rendered as a navy dot-matrix cloud. + Build-time deterministic: a fixed grid of dots, each dot's opacity computed here + from whether it falls inside the mark glyph (solid navy) or outside it (a faint + halo that fades radially), plus a small seeded jitter so it reads as a dither + rather than a clean gradient. No WebGL and no per-dot animation (WebKit-safe); + the only motion is one gentle group "breathe", disabled under reduced motion. + The glyph is the same three polygons as the favicon / HeroMark / mark-black.svg. +*/ + +// Mark glyph polygons, local space x 0..100, y 0..160. +const POLYS_LOCAL = [ + '50,0 80,48 20,48', + '13.75,58 86.25,58 100,80 83.75,106 16.25,106 0,80', + '22.5,116 77.5,116 50,160', +].map((s) => s.split(' ').map((p) => p.split(',').map(Number))); + +// Square viewBox with the 100x160 glyph centred, leaving room for the halo. +const VB = 224; +const OX = (VB - 100) / 2; // 62 +const OY = (VB - 160) / 2; // 32 +const CX = VB / 2; +const CY = VB / 2; +const POLYS = POLYS_LOCAL.map((pts) => pts.map(([x, y]) => [x + OX, y + OY])); + +// Ray-casting point-in-polygon; a dot is "in the glyph" if inside any polygon. +const inPoly = (x: number, y: number, pts: number[][]) => { + let inside = false; + for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) { + const [xi, yi] = pts[i]; + const [xj, yj] = pts[j]; + if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside; + } + return inside; +}; +const inGlyph = (x: number, y: number) => POLYS.some((pts) => inPoly(x, y, pts)); + +// Deterministic 0..1 hash from grid position — the dither texture. +const jitter = (x: number, y: number) => { + const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453; + return n - Math.floor(n); +}; + +const PITCH = 6; +const R = 1.15; +const HALF = 92; // radial normalizer (~glyph half-extent) + +type Dot = { x: number; y: number; o: number }; +const dots: Dot[] = []; +for (let y = PITCH / 2; y < VB; y += PITCH) { + for (let x = PITCH / 2; x < VB; x += PITCH) { + const d = Math.hypot(x - CX, y - CY) / HALF; + const jt = jitter(x, y); + let o: number; + if (inGlyph(x, y)) { + o = 0.55 + 0.38 * (1 - Math.min(d, 1)) + 0.06 * (jt - 0.5); + } else { + const fall = Math.max(0, 1 - d / 1.25); + o = fall * 0.26 * (0.35 + jt); + } + if (o < 0.025) continue; + dots.push({ x: Number(x.toFixed(1)), y: Number(y.toFixed(1)), o: Number(Math.min(o, 0.9).toFixed(3)) }); + } +} +--- + + + + diff --git a/src/components/NavBar.astro b/src/components/NavBar.astro index 5dcae54..8e4db1b 100644 --- a/src/components/NavBar.astro +++ b/src/components/NavBar.astro @@ -1,4 +1,7 @@ --- +import { site } from '../lib/site'; +import logoHorizontal from '../assets/logos/logo-horizontal-black.svg?raw'; + interface Props { /** Optional active path so the current section can be highlighted. */ current?: string; @@ -14,19 +17,19 @@ function isActive(path: string): boolean { @@ -34,7 +37,7 @@ function isActive(path: string): boolean { diff --git a/src/components/NavOverlay.astro b/src/components/NavOverlay.astro index 28c1452..d5ab91d 100644 --- a/src/components/NavOverlay.astro +++ b/src/components/NavOverlay.astro @@ -1,18 +1,19 @@ --- // Mobile-only full-screen nav overlay. Replaces nav.js — script is inlined // at the bottom and uses the same hamburger button defined in NavBar.astro. +import { site } from '../lib/site'; --- @@ -20,7 +21,7 @@ .nav-overlay { position: fixed; inset: 0; - background: rgba(255, 255, 255, 0.98); + background: color-mix(in srgb, var(--paper) 98%, transparent); backdrop-filter: saturate(180%) blur(12px); -webkit-backdrop-filter: saturate(180%) blur(12px); z-index: 100; diff --git a/src/components/PageHeader.astro b/src/components/PageHeader.astro index 0140669..9ec48ad 100644 --- a/src/components/PageHeader.astro +++ b/src/components/PageHeader.astro @@ -11,6 +11,8 @@ interface Pill { interface Props { breadcrumbs?: Crumb[]; + /** Optional mono eyebrow rendered above the title (e.g. "Work with us"). */ + eyebrow?: string; title: string; /** Lede may contain inline HTML (e.g. ) — passed through set:html. */ lede?: string; @@ -24,7 +26,7 @@ interface Props { centered?: boolean; } -const { breadcrumbs = [], title, lede, ledeBullets, metaRow, pills = [], centered = false } = Astro.props; +const { breadcrumbs = [], eyebrow, title, lede, ledeBullets, metaRow, pills = [], centered = false } = Astro.props; ---
@@ -39,6 +41,7 @@ const { breadcrumbs = [], title, lede, ledeBullets, metaRow, pills = [], centere ))} )} + {eyebrow &&

{eyebrow}

}

{title}

{ledeBullets && ledeBullets.length > 0 && (
    @@ -60,7 +63,7 @@ const { breadcrumbs = [], title, lede, ledeBullets, metaRow, pills = [], centere - - diff --git a/src/components/IptfPocsBlock.astro b/src/components/PocsBlock.astro similarity index 92% rename from src/components/IptfPocsBlock.astro rename to src/components/PocsBlock.astro index 60650c0..b193c0a 100644 --- a/src/components/IptfPocsBlock.astro +++ b/src/components/PocsBlock.astro @@ -12,12 +12,13 @@ * spec: pocs///SPEC.md * status: implemented | benchmarked | ... * - * The folder + requirements + spec paths are relative to the `iptf-pocs` + * The folder + requirements + spec paths are relative to the `ethsystems/pocs` * repo, not the rendered site. We link them to the GitHub repo so users * can browse the spec without needing local clones. */ import { statusTone } from '../lib/statusTone'; +import { site } from '../lib/site'; interface Poc { name: string; @@ -35,16 +36,16 @@ interface Props { const { folder, requirements, pocs = [] } = Astro.props; if (!folder && !requirements && pocs.length === 0) return null; -const REPO_BASE = 'https://github.com/ethereum/iptf-pocs/tree/master'; -const BLOB_BASE = 'https://github.com/ethereum/iptf-pocs/blob/master'; +const REPO_BASE = `${site.pocsRepo}/tree/master`; +const BLOB_BASE = `${site.pocsRepo}/blob/master`; function repoLink(path: string, kind: 'tree' | 'blob' = 'blob'): string { return `${kind === 'tree' ? REPO_BASE : BLOB_BASE}/${path}`; } --- -
    -

    IPTF prototypes

    +
    +

    Prototypes

    {(folder || requirements) && (
    diff --git a/src/components/SupportedBy.astro b/src/components/SupportedBy.astro new file mode 100644 index 0000000..0c54796 --- /dev/null +++ b/src/components/SupportedBy.astro @@ -0,0 +1,287 @@ +--- +/* + * "Supported by" coalition tier — a light-theme adaptation of the tier + * row from ~/Repos/coalition/coalition-card.html. Four cells separated + * by 1px vertical hairlines, each = wordmark on top + small uppercase + * caption below. The BitMine, SharpLink and SNZ marks are SVG + * defs; Joseph Lubin renders as type set in --font-sans. + */ +--- + +
    + + + +
    +
    +

    Supported by

    +

    The coalition standing behind EthSystems.

    +
    +
    +
    +
    +
    Ethereum Treasury · BMNR
    +
    + +
    +
    +
    Ethereum Treasury · SBET
    +
    + +
    +
    Joe Lubin
    +
    Co-Founder, Ethereum
    +
    + +
    +
    +
    Asia Ethereum Backer
    +
    +
    +
    +
    + + diff --git a/src/components/explore/BrowseGrid.tsx b/src/components/explore/BrowseGrid.tsx index b3e64c0..5883c06 100644 --- a/src/components/explore/BrowseGrid.tsx +++ b/src/components/explore/BrowseGrid.tsx @@ -60,14 +60,14 @@ export function BrowseGrid({ graph }: Props) { type="search" placeholder="Search…" style={{ - background: '#ffffff', - color: '#1a202c', - border: '1px solid #e2e8f0', + background: 'var(--surface)', + color: 'var(--ink)', + border: '1px solid var(--line)', borderRadius: 6, padding: '7px 12px', fontSize: 13.5, width: 240, - fontFamily: 'Inter, sans-serif', + fontFamily: 'var(--font-sans)', outline: 'none', }} value={filters.search ?? ''} @@ -75,13 +75,13 @@ export function BrowseGrid({ graph }: Props) { /> - + {filtered.length} items @@ -115,8 +115,8 @@ export function BrowseGrid({ graph }: Props) { key={node.id} onClick={() => setSelected(isOpen ? null : node)} style={{ - background: '#ffffff', - border: `1px solid ${isOpen ? getNodeColor(node) : '#e2e8f0'}`, + background: 'var(--surface)', + border: `1px solid ${isOpen ? getNodeColor(node) : 'var(--line)'}`, borderRadius: 8, padding: '14px 14px', cursor: 'pointer', @@ -124,17 +124,17 @@ export function BrowseGrid({ graph }: Props) { boxShadow: isOpen ? '0 4px 12px rgba(13, 20, 36, 0.06)' : 'none', }} onMouseEnter={(e) => { - if (!isOpen) e.currentTarget.style.borderColor = '#cbd5e0'; + if (!isOpen) e.currentTarget.style.borderColor = 'var(--line-2)'; }} onMouseLeave={(e) => { - if (!isOpen) e.currentTarget.style.borderColor = '#e2e8f0'; + if (!isOpen) e.currentTarget.style.borderColor = 'var(--line)'; }} >
    )}
    -
    +
    {node.title}
    {node.privacy_goal && ( -

    +

    {node.privacy_goal}

    )} {node.primary_domain && ( -

    +

    Domain: {node.primary_domain}

    )} @@ -220,7 +220,7 @@ export function BrowseGrid({ graph }: Props) { display: 'flex', gap: 8, fontSize: 11, - fontFamily: 'JetBrains Mono, monospace', + fontFamily: 'var(--font-mono)', }} > {href && ( @@ -228,7 +228,7 @@ export function BrowseGrid({ graph }: Props) { href={href} onClick={(e) => e.stopPropagation()} style={{ - color: '#2257e9', + color: 'var(--navy)', textDecoration: 'none', fontWeight: 700, textTransform: 'uppercase', diff --git a/src/components/explore/DetailPanel.tsx b/src/components/explore/DetailPanel.tsx index 3ed598c..57a0bf6 100644 --- a/src/components/explore/DetailPanel.tsx +++ b/src/components/explore/DetailPanel.tsx @@ -73,7 +73,7 @@ export function DetailPanel({ node, graph, onClose, onSelectNode }: Props) { bottom: 0, width: 420, maxWidth: '95vw', - background: '#ffffff', + background: 'var(--surface)', borderLeft: `2px solid ${getNodeColor(node)}`, overflowY: 'auto', zIndex: 20, @@ -83,10 +83,10 @@ export function DetailPanel({ node, graph, onClose, onSelectNode }: Props) {
    @@ -104,7 +104,7 @@ export function DetailPanel({ node, graph, onClose, onSelectNode }: Props) { > {TYPE_LABELS[node.type]}
    -
    +
    {node.title}
    @@ -115,7 +115,7 @@ export function DetailPanel({ node, graph, onClose, onSelectNode }: Props) { style={{ background: 'none', border: 'none', - color: '#718096', + color: 'var(--muted)', fontSize: 22, cursor: 'pointer', padding: '0 4px', @@ -130,8 +130,8 @@ export function DetailPanel({ node, graph, onClose, onSelectNode }: Props) { {node.privacy_goal && ( -
    +
    {node.privacy_goal}
    )} @@ -155,10 +155,10 @@ export function DetailPanel({ node, graph, onClose, onSelectNode }: Props) { style={{ display: 'inline-block', marginTop: 12, - fontFamily: 'JetBrains Mono, monospace', + fontFamily: 'var(--font-mono)', fontSize: 11, fontWeight: 700, - color: '#2257e9', + color: 'var(--navy)', textDecoration: 'none', letterSpacing: '0.06em', textTransform: 'uppercase', @@ -170,7 +170,7 @@ export function DetailPanel({ node, graph, onClose, onSelectNode }: Props) {
    @@ -182,10 +182,10 @@ export function DetailPanel({ node, graph, onClose, onSelectNode }: Props) { fontSize: 10.5, fontWeight: 700, textTransform: 'uppercase', - color: '#718096', + color: 'var(--muted)', marginBottom: 8, letterSpacing: '0.12em', - fontFamily: 'JetBrains Mono, monospace', + fontFamily: 'var(--font-mono)', }} > Connected ({connections.length}) @@ -201,23 +201,23 @@ export function DetailPanel({ node, graph, onClose, onSelectNode }: Props) { gap: 8, width: '100%', textAlign: 'left', - background: '#ffffff', - border: '1px solid #e2e8f0', + background: 'var(--surface)', + border: '1px solid var(--line)', borderRadius: 6, padding: '7px 11px', marginBottom: 4, cursor: 'pointer', - color: '#1a202c', + color: 'var(--ink)', fontSize: 12.5, transition: 'border-color 0.15s, background 0.15s', }} onMouseEnter={(e) => { e.currentTarget.style.borderColor = getNodeColor(cn); - e.currentTarget.style.background = '#f7fafc'; + e.currentTarget.style.background = 'var(--sunken)'; }} onMouseLeave={(e) => { - e.currentTarget.style.borderColor = '#e2e8f0'; - e.currentTarget.style.background = '#ffffff'; + e.currentTarget.style.borderColor = 'var(--line)'; + e.currentTarget.style.background = 'var(--surface)'; }} > {edgeType} @@ -246,31 +246,31 @@ export function DetailPanel({ node, graph, onClose, onSelectNode }: Props) {
    )} -
    +
    Source: {node.file}
    ); diff --git a/src/components/explore/FilterBar.tsx b/src/components/explore/FilterBar.tsx index 7885cf0..7860b48 100644 --- a/src/components/explore/FilterBar.tsx +++ b/src/components/explore/FilterBar.tsx @@ -3,7 +3,7 @@ import type { GraphData } from '../../lib/graph-types'; import { TYPE_LABELS } from '../../lib/graph-layout'; /* - * Filter controls bar — ported from iptf-web with our light-theme + * Filter controls bar — ported from ethsystems/web with our light-theme * palette and form-control tokens. */ @@ -14,13 +14,13 @@ interface Props { } const controlStyle: React.CSSProperties = { - background: '#ffffff', - color: '#1a202c', - border: '1px solid #e2e8f0', + background: 'var(--surface)', + color: 'var(--ink)', + border: '1px solid var(--line)', borderRadius: 6, padding: '6px 10px', fontSize: 13, - fontFamily: 'Inter, sans-serif', + fontFamily: 'var(--font-sans)', cursor: 'pointer', outline: 'none', }; @@ -120,8 +120,8 @@ export function FilterBar({ graph, filters, onFilterChange }: Props) { style={{ ...controlStyle, cursor: 'pointer', - color: '#2257e9', - borderColor: '#2257e9', + color: 'var(--navy)', + borderColor: 'var(--navy)', background: 'rgba(34, 87, 233, 0.08)', }} > diff --git a/src/components/explore/Galaxy.tsx b/src/components/explore/Galaxy.tsx index 0ae3d45..0621e52 100644 --- a/src/components/explore/Galaxy.tsx +++ b/src/components/explore/Galaxy.tsx @@ -16,7 +16,7 @@ import { DetailPanel } from './DetailPanel'; import { NodeTooltip } from './NodeTooltip'; /* - * d3 force-directed view of the full IPTF map (135 nodes / 819 edges). + * d3 force-directed view of the full EthSystems map (135 nodes / 819 edges). * Light-theme port — SVG canvas sits on our bg-subtle, edge colors * remain the same encoding from graph-layout.ts but read on white. */ @@ -206,7 +206,7 @@ export function Galaxy({ graph }: Props) { .attr('dy', (d) => d.type === 'domain' ? getNodeRadius(d) + 16 : getNodeRadius(d) + 12, ) - .attr('fill', '#1a365d') + .attr('fill', '#0A0A0A') .attr('font-size', (d) => (d.type === 'domain' ? '12px' : '9px')) .attr('font-weight', (d) => (d.type === 'domain' ? '700' : '500')) .attr('pointer-events', 'none'); @@ -335,7 +335,7 @@ export function Galaxy({ graph }: Props) { left: 0, width: '100%', height: '100%', - background: '#f7fafc', + background: 'var(--sunken)', }} /> {hovered && !selected && } @@ -354,11 +354,11 @@ export function Galaxy({ graph }: Props) { bottom: 12, left: 12, background: 'rgba(255, 255, 255, 0.92)', - border: '1px solid #e2e8f0', + border: '1px solid var(--line)', borderRadius: 8, padding: '8px 12px', fontSize: 11, - color: '#4a5568', + color: 'var(--ink-2)', display: 'flex', gap: 12, flexWrap: 'wrap', @@ -379,7 +379,7 @@ export function Galaxy({ graph }: Props) { Hybrid - + Use case diff --git a/src/components/explore/NodeTooltip.tsx b/src/components/explore/NodeTooltip.tsx index 73cec1f..083b31b 100644 --- a/src/components/explore/NodeTooltip.tsx +++ b/src/components/explore/NodeTooltip.tsx @@ -2,7 +2,7 @@ import type { GraphNode } from '../../lib/graph-types'; import { getNodeColor, TYPE_LABELS } from '../../lib/graph-layout'; /* - * Hover tooltip for graph nodes — ported from iptf-web with our + * Hover tooltip for graph nodes — ported from ethsystems/web with our * light-theme palette. Background flips white-on-token, * accent border still encodes the node type via getNodeColor(). */ @@ -27,7 +27,7 @@ export function NodeTooltip({ node, x, y }: Props) { position: 'fixed', left: x + 16, top: y - 10, - background: '#ffffff', + background: 'var(--surface)', border: `1px solid ${getNodeColor(node)}`, borderRadius: 8, padding: '8px 12px', @@ -37,7 +37,7 @@ export function NodeTooltip({ node, x, y }: Props) { boxShadow: '0 4px 24px rgba(13, 20, 36, 0.12)', }} > -
    +
    {node.title}
    @@ -45,8 +45,8 @@ export function NodeTooltip({ node, x, y }: Props) { {node.summary && ( -
    +
    {node.summary.slice(0, 150)} {node.summary.length > 150 ? '…' : ''}
    diff --git a/src/components/explore/TreeView.tsx b/src/components/explore/TreeView.tsx index d3f9f12..b9caef7 100644 --- a/src/components/explore/TreeView.tsx +++ b/src/components/explore/TreeView.tsx @@ -151,7 +151,7 @@ export function TreeView({ graph }: Props) { ref={svgRef} width={totalWidth} height={Math.max(totalHeight, 600)} - style={{ background: '#f7fafc', display: 'block', minWidth: '100%' }} + style={{ background: 'var(--sunken)', display: 'block', minWidth: '100%' }} > {LAYER_LABELS.map((label, i) => ( {label.toUpperCase()} @@ -187,7 +187,7 @@ export function TreeView({ graph }: Props) { key={i} d={`M${sx},${sy} C${mx},${sy} ${mx},${ty} ${tx},${ty}`} fill="none" - stroke={isHighlighted ? '#2257e9' : '#cbd5e0'} + stroke={isHighlighted ? "#133E87" : "#C6C2B4"} strokeWidth={isHighlighted ? 2 : 1} strokeOpacity={isDimmed ? 0.08 : isHighlighted ? 0.85 : 0.35} style={{ transition: 'stroke-opacity 0.15s, stroke 0.15s, stroke-width 0.15s' }} @@ -219,8 +219,8 @@ export function TreeView({ graph }: Props) { width={LAYER_WIDTH} height={NODE_HEIGHT} rx={6} - fill={isHovered ? '#ffffff' : '#ffffff'} - stroke={isHovered ? color : '#e2e8f0'} + fill={isHovered ? "#FBF9F1" : "#FBF9F1"} + stroke={isHovered ? color : "#D9D5C6"} strokeWidth={isHovered ? 2 : 1} style={{ transition: 'stroke 0.15s, filter 0.15s', filter: isHovered ? 'drop-shadow(0 2px 6px rgba(13,20,36,0.08))' : 'none' }} /> @@ -229,10 +229,10 @@ export function TreeView({ graph }: Props) { x={14} y={NODE_HEIGHT / 2} dominantBaseline="central" - fill="#1a365d" + fill="#0A0A0A" fontSize={12} fontWeight={600} - fontFamily="Inter, sans-serif" + fontFamily="'Geist', system-ui, sans-serif" > {ln.node.title.length > 34 ? ln.node.title.slice(0, 32) + '…' @@ -248,7 +248,7 @@ export function TreeView({ graph }: Props) { fontSize={9.5} fontWeight={700} letterSpacing="0.06em" - fontFamily="JetBrains Mono, monospace" + fontFamily="'Geist Mono', ui-monospace, monospace" > {ln.node.layer} diff --git a/src/content.config.ts b/src/content.config.ts index 9244b19..06380dc 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -2,7 +2,7 @@ import { defineCollection, z } from 'astro:content'; import { glob } from 'astro/loaders'; /* - * Content collections wired directly to the iptf-map submodule at + * Content collections wired directly to the ethsystems/map submodule at * ../content-source/. Upstream frontmatter is the single source of * truth — schemas declare every field we consume in the renderer so * the build catches missing or mistyped values early. Unknown extras @@ -158,7 +158,7 @@ const domains = defineCollection({ .object({ title: z.string(), status: z.string().optional(), - // One-line domain summary (iptf-map PR #173). Used as the section + // One-line domain summary (ethsystems/map PR #173). Used as the section // primer on the /use-cases/ and /approaches/ listings. description: z.string().optional(), }) @@ -215,25 +215,31 @@ const weeklyUpdates = defineCollection({ }); /* - * Long-form posts authored upstream at ethereum/iptf-web (NOT in - * iptf-map). Wired in via the posts-source/ submodule pinned to the - * test_guide branch, so build-time pulls always reflect upstream and - * we never copy markdown into this repo. Filenames follow Jekyll's + * Long-form posts, tracked directly in this repo under src/posts/ (NOT + * sourced from the ethsystems/map submodule). Filenames follow Jekyll's * YYYY-MM-DD-slug.md convention. + * + * `image` uses the image() schema helper (not z.string()) so frontmatter + * hero images are resolved and optimized by Astro's asset pipeline at + * build time — entry.data.image is an ImageMetadata object ({ src, width, + * height, format }), not a plain path string. Referenced images must live + * under src/ (see src/assets/posts/) since files in public/ are served + * verbatim and never processed. */ const posts = defineCollection({ loader: glob({ pattern: '*.md', base: './src/posts' }), - schema: z - .object({ - title: z.string(), - description: z.string().optional(), - date: z.union([z.string(), z.date()]), - author: z.string().optional(), - image: z.string().optional(), - layout: z.string().optional(), - tags: z.array(z.string()).optional(), - }) - .passthrough(), + schema: ({ image }) => + z + .object({ + title: z.string(), + description: z.string().optional(), + date: z.union([z.string(), z.date()]), + author: z.string().optional(), + image: image().optional(), + layout: z.string().optional(), + tags: z.array(z.string()).optional(), + }) + .passthrough(), }); export const collections = { diff --git a/src/data/faq.ts b/src/data/faq.ts index 46b30e3..e4197ad 100644 --- a/src/data/faq.ts +++ b/src/data/faq.ts @@ -15,6 +15,71 @@ export interface FaqCategory { } export const faqCategories: FaqCategory[] = [ + { + label: 'About EthSystems', + questions: [ + { + q: 'What is EthSystems?', + a: [ + 'An engineering and research company building confidential systems for institutional Ethereum: the privacy and compliance infrastructure institutions need to put real financial activity on the network.', + ], + links: [{ label: 'About us', href: '/about/' }], + }, + { + q: 'Who is behind it?', + a: [ + "The team behind the Ethereum Foundation's Institutional Privacy Task Force (IPTF).", + ], + links: [{ label: 'Meet the team', href: '/about/' }], + }, + { + q: 'What have you actually built?', + a: [ + 'A year of public, open-source work: private bonds (ZK, privacy L2s, FHE), compliance-first private stablecoin transfers, private cross-chain atomic swaps, a validium proof-of-concept, privacy-preserving identity, the Public Rails vs Private Ledgers decision framework, and the Ethereum Privacy Map.', + ], + links: [ + { label: 'Writeups', href: '/blog/' }, + { label: 'Explore the map', href: '/explore/' }, + ], + }, + { + q: 'Why does this matter?', + a: [ + 'Every institutional use of Ethereum (tokenization, stablecoins, settlement) eventually hits the same blocker: a public ledger exposes positions, counterparties, and flows that regulated institutions cannot reveal. Confidentiality with built-in compliance is what makes those deployments viable.', + ], + }, + { + q: 'What do you offer institutions?', + a: [ + 'Hands-on engineering engagements: privacy architecture and advisory, workshops that turn interest into concrete requirements, proof-of-concepts that de-risk decisions, and the design and build of confidential systems integrated with existing vendors and infrastructure, through to live implementation. Engagements can start small and scoped.', + ], + links: [{ label: 'Approaches', href: '/approaches/' }], + }, + { + q: "What's your relationship with the Ethereum Foundation?", + a: [ + 'We created and ran the Institutional Privacy Task Force at the EF, and we continue that body of work in active collaboration with EF and EF-aligned teams: coordinating the transition together, keeping the artifacts public, and collaborating on public goods, specs, and privacy work. EthSystems is its own company because commercial delivery needs a commercial structure; the collaboration continues.', + ], + }, + { + q: 'How do you relate to Ethlabs and Ethereum Institutional?', + a: [ + "As complementary nodes in the same network, often with overlapping supporters, and as collaborators. Ethlabs builds and grows the core protocol and platform capabilities of Ethereum. Ethereum Institutional is a neutral, non-commercial front door helping institutions understand and navigate the ecosystem. EthSystems is the specialist builder: when an institution moves from evaluating Ethereum to building on it, we're the accountable commercial counterparty that designs and delivers the confidential systems involved.", + ], + }, + { + q: 'Is your work open source?', + a: [ + "Yes. Our proof-of-concepts, libraries, frameworks, and the Ethereum Privacy Map are public, and we'll keep publishing. Open, verifiable work is how we earn trust.", + ], + links: [{ label: 'GitHub · map', href: 'https://github.com/ethsystems/map' }], + }, + { + q: 'Where do you operate?', + a: ['Globally.'], + }, + ], + }, { label: 'Privacy basics', questions: [ @@ -34,7 +99,7 @@ export const faqCategories: FaqCategory[] = [ q: 'What does "private" actually mean on a public blockchain?', a: [ 'Privacy on Ethereum means controlling what information is visible and to whom. The spectrum ranges from anonymity (unlinkable addresses, but amounts may be visible) to full confidentiality (amounts, counterparties, and transaction patterns all hidden). Most institutional solutions sit somewhere in between, using selective disclosure to reveal specific data to authorized parties like regulators.', - 'The IPTF uses the I2I/I2U distinction to clarify threat models: in institution-to-institution contexts, both parties have symmetric trust and shared compliance burden. In institution-to-user contexts, the power dynamic is asymmetric and privacy protections must favor the weaker party.', + 'EthSystems uses the I2I/I2U distinction to clarify threat models: in institution-to-institution contexts, both parties have symmetric trust and shared compliance burden. In institution-to-user contexts, the power dynamic is asymmetric and privacy protections must favor the weaker party.', ], links: [ { label: 'Regulatory disclosure', href: '/patterns/pattern-regulatory-disclosure-keys-proofs/' }, diff --git a/src/data/glossary.json b/src/data/glossary.json index f25e280..f6afcd3 100644 --- a/src/data/glossary.json +++ b/src/data/glossary.json @@ -1,18 +1,18 @@ [ { "term": "CROPS (Censorship Resistance, Open Source and Free, Privacy, Security)", - "definition": "Four non-negotiable properties for Ethereum defined by the Ethereum Foundation. The IPTF Map evaluates solutions against all four dimensions independently. See [CONTRIBUTING.md § CROPS Evaluation](CONTRIBUTING.md#crops-evaluation) for full scoring rubrics.", - "category": "IPTF Evaluation Frameworks" + "definition": "Four non-negotiable properties for Ethereum defined by the Ethereum Foundation. The EthSystems Map evaluates solutions against all four dimensions independently. See [CONTRIBUTING.md § CROPS Evaluation](CONTRIBUTING.md#crops-evaluation) for full scoring rubrics.", + "category": "EthSystems Evaluation Frameworks" }, { "term": "I2I (Institution-to-Institution)", "definition": "Interaction model where both counterparties are regulated entities (e.g., bank-to-bank settlement). Symmetric power dynamic; both parties have legal teams, vendor choice, and contractual recourse.", - "category": "IPTF Evaluation Frameworks" + "category": "EthSystems Evaluation Frameworks" }, { "term": "I2U (Institution-to-End-User)", "definition": "Interaction model where one counterparty is a regulated entity and the other is an individual (e.g., retail payment). Asymmetric power dynamic; CROPS must protect the user from the institution. Institutions should be transparent to regulators and end users; end users should have the right to protect their private lives.", - "category": "IPTF Evaluation Frameworks" + "category": "EthSystems Evaluation Frameworks" }, { "term": "Commitment", diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 7ee0832..9c82bb0 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -1,8 +1,16 @@ --- +import '../styles/fonts.css'; import '../styles/globals.css'; import NavBar from '../components/NavBar.astro'; import NavOverlay from '../components/NavOverlay.astro'; import SiteFooter from '../components/SiteFooter.astro'; +import { siteGraphSchema } from '../lib/schema'; + +// Content-hashed URLs for the two above-the-fold critical font files. Vite emits +// the same hashed asset fonts.css already references, so preloading adds no extra +// request. Geist Mono is intentionally not preloaded (small mono labels; swap is fine). +import frauncesDisplayHref from '@fontsource-variable/fraunces/files/fraunces-latin-full-normal.woff2?url'; +import geistBodyHref from '@fontsource-variable/geist/files/geist-latin-wght-normal.woff2?url'; interface Props { title: string; @@ -10,14 +18,29 @@ interface Props { current?: string; /** Optional canonical override; defaults to Astro.url. */ canonical?: string; + /** When true, emit a robots noindex,nofollow meta (hidden pages). */ + noindex?: boolean; + /** Optional per-page OG/Twitter image (site-root-relative path); defaults to /og.png. */ + image?: string; + /** Optional page-specific JSON-LD (e.g. FAQPage, BlogPosting), on top of the + * sitewide Organization/WebSite graph this layout always emits. */ + structuredData?: Record | Record[]; } -const { title, description, current, canonical } = Astro.props; -const fullTitle = title ? `${title} · IPTF` : 'Institutional Privacy Task Force'; +const { title, description, current, canonical, noindex, image, structuredData } = Astro.props; +const fullTitle = title + ? `${title} · EthSystems` + : 'EthSystems · Confidential Systems for Institutional Ethereum'; const desc = description ?? - 'A working knowledge base for institutions arriving at Ethereum with privacy requirements. Maintained by the Privacy Cluster.'; + "EthSystems helps institutions move on-chain with privacy that's performant, secure, usable, and accessible, disclosing only what the rules require."; const canon = canonical ?? new URL(Astro.url.pathname, Astro.site ?? Astro.url).toString(); +const ogImage = new URL(image ?? '/og.png', Astro.site ?? Astro.url).toString(); +const structuredDataList = structuredData + ? Array.isArray(structuredData) + ? structuredData + : [structuredData] + : []; --- @@ -25,24 +48,84 @@ const canon = canonical ?? new URL(Astro.url.pathname, Astro.site ?? Astro.url). + + + + {fullTitle} + {noindex && } - - - - + + + {/* Machine-readable per-page hint: advertise an optimized markdown rendering of this + exact page via the markdown.new prefix. Skipped on noindex pages. Agents that + honor rel=alternate may fetch this instead of scraping the HTML. */} + {!noindex && ( + + )} + + {/* Font preload: Fraunces display + Geist body. crossorigin required (CORS fetch). */} + + + + {/* Favicons + web app manifest. */} + + + + + + + {/* Open Graph */} + + + + + + {/* Twitter */} + + + + + + {/* Structured data: sitewide Organization/WebSite graph on every page, + plus any page-specific schema (FAQPage, BlogPosting, ...). `<` + is escaped so no payload can prematurely close the script tag. */} + + + + -
    +
    diff --git a/src/layouts/DetailPageLayout.astro b/src/layouts/DetailPageLayout.astro index af5c43e..1b9420b 100644 --- a/src/layouts/DetailPageLayout.astro +++ b/src/layouts/DetailPageLayout.astro @@ -19,12 +19,16 @@ interface Props { * article body reads as one centred reading column. Used by * blog post details (per 2026-05-22 meeting: writeups centred). */ centered?: boolean; + /** Optional per-page OG/Twitter image; forwarded to BaseLayout. */ + image?: string; + /** Optional page-specific JSON-LD; forwarded to BaseLayout. */ + structuredData?: Record | Record[]; } -const { title, pageTitle, description, current, breadcrumbs, lede, ledeBullets, pills, centered = false } = Astro.props; +const { title, pageTitle, description, current, breadcrumbs, lede, ledeBullets, pills, centered = false, image, structuredData } = Astro.props; --- - +
    diff --git a/src/layouts/ListPageLayout.astro b/src/layouts/ListPageLayout.astro index 8f7ab35..eaf71b2 100644 --- a/src/layouts/ListPageLayout.astro +++ b/src/layouts/ListPageLayout.astro @@ -13,6 +13,8 @@ interface Props { breadcrumbs?: Crumb[]; lede?: string; pills?: Pill[]; + /** Optional per-page OG/Twitter image, forwarded to BaseLayout. */ + image?: string; } const { @@ -23,10 +25,11 @@ const { breadcrumbs = [{ label: 'Home', href: '/' }], lede, pills, + image, } = Astro.props; --- - +
    diff --git a/src/lib/domain.ts b/src/lib/domain.ts index 485a403..2993e01 100644 --- a/src/lib/domain.ts +++ b/src/lib/domain.ts @@ -1,6 +1,6 @@ /** * Domains are DATA-DRIVEN from the `domains` content collection (one - * markdown file per domain in iptf-map). A domain's canonical name is + * markdown file per domain in ethsystems/map). A domain's canonical name is * its title minus the "Domain: " prefix; its stable id is the filename * slug (e.g. `funds-assets`). * diff --git a/src/lib/graph-types.ts b/src/lib/graph-types.ts index d6cc4d8..44588ac 100644 --- a/src/lib/graph-types.ts +++ b/src/lib/graph-types.ts @@ -37,7 +37,9 @@ export interface OpenSourceImpl { language?: string; } -export interface IptfPocsBlock { +// `iptf_pocs` is the upstream frontmatter field name in the map content +// (ethsystems/map); the key stays verbatim, the block type is ours. +export interface PocsBlock { folder?: string; requirements?: string; pocs?: Array<{ @@ -93,7 +95,7 @@ export interface GraphNode { standards?: string[]; related_patterns?: RelatedPatterns; open_source_implementations?: OpenSourceImpl[]; - iptf_pocs?: IptfPocsBlock; + iptf_pocs?: PocsBlock; primary_patterns?: string[]; supporting_patterns?: string[]; related_use_cases?: string[]; diff --git a/src/lib/jekyllSlug.ts b/src/lib/jekyllSlug.ts index cbad50b..c92d231 100644 --- a/src/lib/jekyllSlug.ts +++ b/src/lib/jekyllSlug.ts @@ -1,6 +1,6 @@ /* * Jekyll-compatible title slugifier — matches `:title` permalink output - * used by upstream ethereum/iptf-web so existing public URLs (e.g. + * used by upstream ethsystems/web so existing public URLs (e.g. * /cypherpunk-x-institutional-privacy/) stay valid on our side. */ export function jekyllSlugify(s: string): string { diff --git a/src/lib/related.ts b/src/lib/related.ts index dcbbfcc..1b06493 100644 --- a/src/lib/related.ts +++ b/src/lib/related.ts @@ -2,7 +2,7 @@ * Detail-page "Related" data: outgoing markdown links from a body, * grouped by collection, plus sibling-in-same-domain helpers. * - * iptf-map cross-link conventions (verified): + * ethsystems/map cross-link conventions (verified): * - `../{collection}/{slug}.md` for cross-folder * - `{slug}.md` for same-folder (no `./`) * - No anchor fragments are used in internal links @@ -312,7 +312,7 @@ let cachedIndex: Promise | null = null; /* * Per-collection frontmatter conventions for cross-links, as observed - * in iptf-map's authored content. Each entry tells us which keys on + * in ethsystems/map's authored content. Each entry tells us which keys on * `entry.data` carry slug references to which target collection. */ interface FmRefRule { diff --git a/src/lib/render.ts b/src/lib/render.ts index 3d6d1ea..5e49a09 100644 --- a/src/lib/render.ts +++ b/src/lib/render.ts @@ -1,5 +1,5 @@ /** - * Shared markdown renderer for iptf-map content. + * Shared markdown renderer for ethsystems/map content. * * Map authors write portable Markdown with relative GitHub-style links * (e.g. `../patterns/pattern-foo.md`). When rendered through marked() the @@ -12,7 +12,7 @@ * - renderMarkdownInline(md) — inline-only (marked.parseInline) * * Unresolved links are warned once at build time and passed through unchanged - * so the page still renders; the warning surfaces real data drift in iptf-map. + * so the page still renders; the warning surfaces real data drift in ethsystems/map. * * IMPORTANT: This file must stay browser-safe (no `fs` / `path` imports). It * ships to the client via DetailPanel.tsx (React island). @@ -50,7 +50,7 @@ function stripTypePrefix(title: string): string { } // True when the visible label is itself a `.md` filename — the upstream -// iptf-map authoring anti-pattern where contributors paste GitHub-style +// ethsystems/map authoring anti-pattern where contributors paste GitHub-style // relative links and let the filename serve as the label. Same detection // used by the Astro remark plugin (src/plugins/remark-rewrite-links.ts). function isFilenameLabel(label: string): boolean { @@ -164,11 +164,11 @@ marked.use({ } if (!resolved.exists && !warnedHrefs.has(href)) { warnedHrefs.add(href); - console.warn(`[render] unresolved iptf-map link: ${href} → ${resolved.route} (node not in graph)`); + console.warn(`[render] unresolved ethsystems/map link: ${href} → ${resolved.route} (node not in graph)`); } } else if (!warnedHrefs.has(href)) { warnedHrefs.add(href); - console.warn(`[render] unknown iptf-map link shape: ${href}`); + console.warn(`[render] unknown ethsystems/map link shape: ${href}`); } } } @@ -190,7 +190,7 @@ marked.use({ * * Out of the box `marked` renders the bracket text inline as plain * prose. We post-process the rendered HTML so each leading [tag] - * becomes a styled chip — matches the upstream iptf-web treatment. + * becomes a styled chip — matches the upstream ethsystems/web treatment. * * Only the FIRST bracket-pair of any
  • is converted, and only * when it sits flush against the opening tag (whitespace optional); diff --git a/src/lib/schema.ts b/src/lib/schema.ts new file mode 100644 index 0000000..3358a7f --- /dev/null +++ b/src/lib/schema.ts @@ -0,0 +1,87 @@ +// JSON-LD structured data builders (schema.org). Kept separate from +// site.ts so layouts/pages can import just the shape they render +// without pulling in unrelated nav/footer config. +import { site } from './site'; + +export const organizationId = `${site.domain}/#organization`; +const websiteId = `${site.domain}/#website`; + +/** Sitewide Organization + WebSite graph. Emitted on every page via BaseLayout. */ +export function siteGraphSchema() { + return { + '@context': 'https://schema.org', + '@graph': [ + { + '@type': 'Organization', + '@id': organizationId, + name: site.name, + url: `${site.domain}/`, + logo: `${site.domain}/icon-maskable.png`, + sameAs: [site.x, site.linkedin, site.github], + }, + { + '@type': 'WebSite', + '@id': websiteId, + url: `${site.domain}/`, + name: site.name, + publisher: { '@id': organizationId }, + }, + ], + }; +} + +interface FaqEntryLike { + q: string; + a: string[]; +} +interface FaqCategoryLike { + questions: FaqEntryLike[]; +} + +/** FAQPage schema from the shared faqCategories dataset (src/data/faq.ts). */ +export function faqPageSchema(categories: FaqCategoryLike[]) { + return { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: categories.flatMap((cat) => + cat.questions.map((entry) => ({ + '@type': 'Question', + name: entry.q, + acceptedAnswer: { + '@type': 'Answer', + text: entry.a.join(' '), + }, + })), + ), + }; +} + +/** BlogPosting schema for a single writeup. `date` is the already-normalized YYYY-MM-DD string. */ +export function blogPostingSchema(opts: { + title: string; + description?: string; + date: string; + authors?: string; + image?: string; + url: string; +}) { + const authorNames = (opts.authors ?? '') + .split(',') + .map((name) => name.trim()) + .filter(Boolean); + + return { + '@context': 'https://schema.org', + '@type': 'BlogPosting', + headline: opts.title, + description: opts.description, + datePublished: opts.date, + url: opts.url, + mainEntityOfPage: { '@type': 'WebPage', '@id': opts.url }, + ...(authorNames.length > 0 && { + author: authorNames.map((name) => ({ '@type': 'Person', name })), + }), + ...(opts.image && { image: new URL(opts.image, `${site.domain}/`).toString() }), + publisher: { '@id': organizationId }, + }; +} diff --git a/src/lib/site.ts b/src/lib/site.ts new file mode 100644 index 0000000..9799dc6 --- /dev/null +++ b/src/lib/site.ts @@ -0,0 +1,15 @@ +// Central site config for EthSystems chrome (nav, footer, meta). +// Mirrors the ethsystems design system's site.ts. +export const site = { + name: 'EthSystems', + domain: 'https://ethsystems.org', + contactFormUrl: 'https://forms.gle/24Ec9Grk5VgFMLX29', + github: 'https://github.com/ethsystems', + x: 'https://x.com/eth_systems', + linkedin: 'https://www.linkedin.com/company/ethsystems/', + email: 'hello@ethsystems.org', + careersEmail: 'join@ethsystems.org', + // Source repos for the map + PoC content. + mapRepo: 'https://github.com/ethsystems/map', + pocsRepo: 'https://github.com/ethsystems/pocs', +} as const; diff --git a/src/pages/about.astro b/src/pages/about.astro index 76b4817..02af6bb 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -1,19 +1,20 @@ --- /* - * About page — ported from ethereum/iptf-web (`src/pages/about.astro`, - * test_guide branch). Light theme + our tokens. Copy adapted to use - * "institutional" instead of "enterprise" per Yanis's tweak (already - * applied site-wide elsewhere). + * About page — ported from ethsystems/web (`src/pages/about.astro`, + * test_guide branch). Light theme + our tokens. Copy uses + * "institutional" rather than "enterprise", consistent with the rest + * of the site. */ import BaseLayout from '../layouts/BaseLayout.astro'; import PageHeader from '../components/PageHeader.astro'; +import { site } from '../lib/site'; const pageDescription = - 'The Institutional Privacy Task Force (IPTF) helps onboard institutions onto Ethereum with privacy-preserving solutions.'; + 'EthSystems helps institutions move on-chain with privacy-preserving solutions.'; --- @@ -22,8 +23,8 @@ const pageDescription = { label: 'Home', href: '/' }, { label: 'About' }, ]} - title="Institutional Privacy Task Force" - lede="The Institutional Privacy Task Force (IPTF) is a small team that helps onboard institutions onto Ethereum with a focus on privacy needs being met in a performant, secure, usable, and accessible way." + title="EthSystems" + lede="EthSystems is a team that helps onboard institutions onto Ethereum with a focus on privacy needs being met in a performant, secure, usable, and accessible way." />
    @@ -35,7 +36,7 @@ const pageDescription = the prominent slot now, source repos live in the footer. -->

    Our own research and prototypes

    -

    The Privacy Cluster prototypes and tests its own solutions directly with institutions, gathering real insights and best practices along the way.

    +

    EthSystems prototypes and tests its own solutions directly with institutions, gathering real insights and best practices along the way.

      {[ ['1', 'Interview', 'Meet institutions under NDA to gather concrete requirements.'], @@ -53,19 +54,16 @@ const pageDescription =
    - +
    -

    Who is behind IPTF

    +

    Who is behind EthSystems

      {[ { name: 'Mo Jalil', handle: 'motypes', avatar: '/assets/images/team/mo-jalil.jpg', linkedin: 'motypes' }, - { name: 'Oskar Thorén', handle: 'oskarth', avatar: '/assets/images/team/oskar-thoren.jpg', linkedin: null }, - { name: 'Yanis Meziane', handle: 'yanis_mezn', avatar: '/assets/images/team/yanis-meziane.jpg', linkedin: 'yanism' }, - { name: 'Aaryamann Challani', handle: 'p1ge0nh8er', avatar: '/assets/images/team/aaryamann-challani.jpg', linkedin: 'aaryamannc' }, + { name: 'Oskar Thorén', handle: 'oskarth', avatar: '/assets/images/team/oskar-thoren.jpg', linkedin: 'oskar-thorén-51a050421' }, + { name: 'Aaryamann Challani', handle: '_rymnc', avatar: '/assets/images/team/aaryamann-challani.jpg', linkedin: 'aaryamannc' }, ].map((p) => (
    • Workshops

      -

      The task force travels to meet institutions where they are. Each session is built around the host's actual context: their regulatory environment, in-flight projects, and technical maturity. We've held workshops in Asia, Europe, and online, with sessions on the calendar across more regions.

      +

      The team travels to meet institutions where they are. Each session is built around the host's actual context: their regulatory environment, in-flight projects, and technical maturity. We hold workshops globally, spanning Asia, Europe, and the Americas.

      @@ -124,20 +122,23 @@ const pageDescription = UA presentational hints that compete with CSS aspect-ratio and silently force the photos back to their intrinsic tall proportions. CSS below is the single source of truth. */} - IPTF workshop in session - IPTF workshop attendees in discussion + EthSystems workshop in session + EthSystems workshop attendees in discussion

    Talk to us

    -

    Complete this form so we can understand your request and respond promptly.

    +

    Contact us so we can understand your request and respond promptly.

    @@ -170,9 +171,7 @@ const pageDescription = letter-spacing: -0.02em; margin: 0 0 1.25rem; } - /* Contact card on the left, email-as-link on the right. - Card stays at its natural width; email expands to occupy the rest - of the row and sits centred vertically next to the card. */ + /* Contact cards sit at their natural width and wrap as needed. */ .contact-row { display: flex; align-items: center; @@ -198,15 +197,6 @@ const pageDescription = border-color: var(--color-cta-border); background: var(--color-cta-tint); } - .contact-email { - font-family: var(--font-sans); - font-size: 1rem; - font-weight: 600; - color: var(--color-cta); - text-decoration: none; - transition: color var(--duration-fast) var(--easing-soft); - } - .contact-email:hover { color: var(--color-cta-hover); } .contact-label { font-family: var(--font-mono); font-size: 0.65rem; @@ -319,7 +309,7 @@ const pageDescription = margin: 0; padding: 0; display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; } @media (max-width: 720px) { diff --git a/src/pages/approaches/[slug].astro b/src/pages/approaches/[slug].astro index ccc8c9d..5ae0b06 100644 --- a/src/pages/approaches/[slug].astro +++ b/src/pages/approaches/[slug].astro @@ -1,8 +1,8 @@ --- /* - * Approach detail — section-routed render ported from ethereum/iptf-web. + * Approach detail — section-routed render ported from ethsystems/web. * - * Per the iptf-web merge handoff (notes/iptf-web-merge-handoff.md): the + * Per the ethsystems/web merge handoff (notes/ethsystems/web-merge-handoff.md): the * upstream approach markdown has a fixed H2 structure (Problem framing, * Approaches, Comparison, Persona perspectives, Recommendation, Open * questions, Example scenario). Instead of flat-rendering the body, we @@ -558,7 +558,7 @@ function maturityTone(m?: string): 'strong' | 'mid' | 'plain' | 'muted' { {entry.data.iptf_pocs && Array.isArray(entry.data.iptf_pocs.pocs) && entry.data.iptf_pocs.pocs.length > 0 && (
    - IPTF prototypes + Prototypes {entry.data.iptf_pocs.pocs.length} {entry.data.iptf_pocs.pocs.map((poc: any) => ( @@ -1280,7 +1280,7 @@ function maturityTone(m?: string): 'strong' | 'mid' | 'plain' | 'muted' { } .rail-link:hover { color: var(--color-cta); } - /* IPTF prototypes — one row per POC, compact. */ + /* EthSystems prototypes — one row per POC, compact. */ .rail-poc { padding: 0.55rem 0; border-bottom: 1px solid var(--color-border); diff --git a/src/pages/approaches/index.astro b/src/pages/approaches/index.astro index 7bd979b..f8aa261 100644 --- a/src/pages/approaches/index.astro +++ b/src/pages/approaches/index.astro @@ -30,7 +30,7 @@ const useCaseById = new Map(useCases.map((uc) => [uc.id, uc])); const domainIndex = buildDomainIndex(domains); // Per-domain primer is the domain's frontmatter `description` (a curated -// one-liner, iptf-map PR #173). Falls back to the first ## TLDR bullet +// one-liner, ethsystems/map PR #173). Falls back to the first ## TLDR bullet // where no description exists yet. Links to the domain detail page. function domainPrimer(d: Domain): { html: string; href: string } | undefined { const entry = domainIndex.entryFor(d) as @@ -104,6 +104,7 @@ const tabs = orderedDomains.map((d) => ({ pageTitle="Approaches" description="Approaches" current="/approaches/" + image="/og/approaches.png" > diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro index 73a810a..76347e8 100644 --- a/src/pages/blog/[slug].astro +++ b/src/pages/blog/[slug].astro @@ -1,16 +1,18 @@ --- /* * /blog/{slug}/ — post detail. slug is jekyllSlugify(post.data.title), - * matching ethereum/iptf-web's permalink convention. + * matching ethsystems/web's permalink convention. * - * Source: the `posts` content collection, which loads from the - * posts-source/ submodule. + * Source: the `posts` content collection, tracked directly in src/posts/. */ +import { Image, getImage } from 'astro:assets'; import { getCollection, render } from 'astro:content'; import DetailPageLayout from '../../layouts/DetailPageLayout.astro'; import ArticleFooter from '../../components/ArticleFooter.astro'; import { jekyllSlugify } from '../../lib/jekyllSlug'; import { isPublished } from '../../lib/posts'; +import { blogPostingSchema } from '../../lib/schema'; +import { site } from '../../lib/site'; function toISODate(d: string | Date | undefined): string { if (!d) return ''; @@ -27,6 +29,7 @@ export async function getStaticPaths() { } const { entry } = Astro.props; +const { slug } = Astro.params; const { Content } = await render(entry); const date = toISODate(entry.data.date); const sibling = { href: '/blog/', label: 'More writeups' }; @@ -35,6 +38,29 @@ const pills = [ ...(date ? [{ label: date, emphasis: true }] : []), ...(entry.data.author ? [{ label: entry.data.author }] : []), ]; + +const ogImage = entry.data.image + ? ( + await getImage({ + src: entry.data.image, + width: 1200, + height: 630, + fit: 'cover', + position: 'center', + format: 'jpg', + quality: 80, + }) + ).src + : undefined; + +const postSchema = blogPostingSchema({ + title: entry.data.title, + description: entry.data.description, + date, + authors: entry.data.author, + image: ogImage, + url: `${site.domain}/blog/${slug}/`, +}); --- {entry.data.image && (
    - +
    )} diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index b1bd4b5..e37a5b0 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -2,17 +2,18 @@ /* * /blog/ — long-form writeups index. * - * Source: the `posts` content collection, which loads markdown from the - * posts-source/ submodule (ethereum/iptf-web on test_guide). Single - * unified list of all posts, sorted newest-first. Matches the upstream - * /blog at ethereum/iptf-web — no curated "featured" section, no split - * with weekly updates. + * Source: the `posts` content collection, tracked directly in src/posts/. + * Single unified list of all posts, sorted newest-first. Matches the + * upstream /blog at ethsystems/web — no curated "featured" section, no + * split with weekly updates. */ +import { Image } from 'astro:assets'; import { getCollection } from 'astro:content'; import ListPageLayout from '../../layouts/ListPageLayout.astro'; import NextStepCTA from '../../components/NextStepCTA.astro'; import { jekyllSlugify } from '../../lib/jekyllSlug'; import { isPublished } from '../../lib/posts'; +import { site } from '../../lib/site'; function toISODate(d: string | Date | undefined): string { if (!d) return ''; @@ -39,9 +40,10 @@ const today = new Date().toISOString().slice(0, 10); ${today}`} >
    @@ -51,7 +53,7 @@ const today = new Date().toISOString().slice(0, 10); {p.image && (
    - +
    )}
    @@ -69,9 +71,9 @@ const today = new Date().toISOString().slice(0, 10);
    diff --git a/src/pages/design.astro b/src/pages/design.astro index 8cb9060..5b5917b 100644 --- a/src/pages/design.astro +++ b/src/pages/design.astro @@ -63,7 +63,7 @@ const contextColors: Swatch[] = [ interface TypeRow { label: string; sample: string; family: 'sans' | 'mono'; size: string; weight: string; usage: string } const typeRows: TypeRow[] = [ - { label: 'Page H1', sample: 'Institutional Privacy Task Force', family: 'sans', size: 'clamp(2.25rem, 4.4vw, 3.05rem)', weight: '700', usage: 'PageHeader title' }, + { label: 'Page H1', sample: 'EthSystems', family: 'sans', size: 'clamp(2.25rem, 4.4vw, 3.05rem)', weight: '700', usage: 'PageHeader title' }, { label: 'Section H2', sample: 'Every entry began as a conversation', family: 'sans', size: 'clamp(2.15rem, 3.8vw, 2.85rem)', weight: '700', usage: '.section-head h2' }, { label: 'Prose H2', sample: 'Components', family: 'sans', size: '1.5rem', weight: '700', usage: '.prose h2 (in-body section break)' }, { label: 'Prose H3', sample: 'Threat model', family: 'sans', size: '1.2rem', weight: '600', usage: '.prose h3' }, @@ -95,7 +95,7 @@ const durations = [

    Typography

    -

    Inter for sans, JetBrains Mono for code and small meta. Weights 400 / 500 / 600 / 700 / 800.

    +

    Fraunces for display headings, Geist for sans/body, Geist Mono for code and small meta.

    {typeRows.map((r) => (
    @@ -338,7 +338,7 @@ const durations = [

    Code

    -

    JetBrains Mono. Inline code gets a subtle muted background and border; block code uses subtle bg with no border colour shift.

    +

    Geist Mono. Inline code gets a subtle muted background and border; block code uses subtle bg with no border colour shift.

    Inline references look like --color-cta or renderMarkdown(body).

    {`// src/lib/render.ts
    diff --git a/src/pages/domains/[slug].astro b/src/pages/domains/[slug].astro
    index 92c8d95..6774ce2 100644
    --- a/src/pages/domains/[slug].astro
    +++ b/src/pages/domains/[slug].astro
    @@ -1,6 +1,6 @@
     ---
     /*
    - * Domain detail — refactored per the iptf-web merge handoff
    + * Domain detail — refactored per the ethsystems/web merge handoff
      * (locked 2026-05-19, "theirs structure exactly + our card visuals").
      *
      * Adds three new structural pieces above the existing body prose:
    diff --git a/src/pages/domains/index.astro b/src/pages/domains/index.astro
    index ab93cc0..9b61dd3 100644
    --- a/src/pages/domains/index.astro
    +++ b/src/pages/domains/index.astro
    @@ -14,6 +14,7 @@ const items = (await getCollection('domains')).slice().sort((a, b) =>
       pageTitle="Domains"
       description="Domains"
       current="/domains/"
    +  image="/og/domains.png"
     >
       
    diff --git a/src/pages/faq.astro b/src/pages/faq.astro index f22633a..8b3b0f1 100644 --- a/src/pages/faq.astro +++ b/src/pages/faq.astro @@ -1,6 +1,6 @@ --- /* - * FAQ page — ported from ethereum/iptf-web (`src/pages/faq.astro`, + * FAQ page — ported from ethsystems/web (`src/pages/faq.astro`, * test_guide branch). * * Content is a hand-written categories array — kept verbatim from @@ -12,6 +12,7 @@ import BaseLayout from '../layouts/BaseLayout.astro'; import PageHeader from '../components/PageHeader.astro'; import { faqCategories } from '../data/faq'; +import { faqPageSchema } from '../lib/schema'; const categories = faqCategories; @@ -23,6 +24,8 @@ const pageDescription = title="FAQ" description={pageDescription} current="/faq/" + image="/og/faq.png" + structuredData={faqPageSchema(categories)} >
    -
    +
    +
    {categories.map((cat) => (
    {cat.label}
    {cat.questions.map((item, qIdx) => ( -
    +
    {item.q} @@ -66,6 +70,7 @@ const pageDescription = ))}
    ))} +
    @@ -77,7 +82,7 @@ const pageDescription = .faq-inner { max-width: 800px; margin: 0; - padding: 0 var(--container-pad); + padding: 0; } .faq-category { diff --git a/src/pages/glossary.astro b/src/pages/glossary.astro index 8c29826..807c1a5 100644 --- a/src/pages/glossary.astro +++ b/src/pages/glossary.astro @@ -1,8 +1,8 @@ --- /* - * Glossary page — ported from ethereum/iptf-web (`src/pages/glossary.astro`, + * Glossary page — ported from ethsystems/web (`src/pages/glossary.astro`, * test_guide branch). Terms come from glossary.json, generated by - * scripts/build-graph.mjs from iptf-map's GLOSSARY.md. + * scripts/build-graph.mjs from ethsystems/map's GLOSSARY.md. * * Lemma rename per handoff (B2B/B2C — chrome only, locked 2026-05-19): * "I2I (Institution-to-Institution)" → "B2B (Business-to-Business / Institution-to-Institution)" @@ -56,13 +56,14 @@ function catSlug(name: string): string { .replace(/(^-|-$)/g, ''); } -const pageDescription = `${allTerms.length} terms used across the IPTF guide. Organized by category — the same definitions surface as inline tooltips throughout the site.`; +const pageDescription = `${allTerms.length} terms used across the EthSystems guide. Organized by category — the same definitions surface as inline tooltips throughout the site.`; --- b.date.localeCompare(a.date)) .slice(0, 3); -// FAQ teaser — 5 hand-curated featured questions resolved against the +// FAQ teaser — 9 hand-curated featured questions resolved against the // shared FAQ dataset (src/data/faq.ts). The homepage renders these inline // as expandable accordions so the full answer is one click away without a -// page jump. +// page jump. Strings must match faq.ts exactly (findFaq is strict equality). const FEATURED_FAQ_QS = [ - 'Is Ethereum private enough for institutional use?', - 'What does MiCA say about on-chain privacy?', - 'How do viewing keys work for regulatory access?', - 'Can institutions use DeFi and stay compliant?', - 'Which privacy solutions are production-ready today?', + 'What is EthSystems?', + 'Who is behind it?', + 'What have you actually built?', + 'Why does this matter?', + 'What do you offer institutions?', + "What's your relationship with the Ethereum Foundation?", + 'How do you relate to Ethlabs and Ethereum Institutional?', + 'Is your work open source?', + 'Where do you operate?', ]; const faqTeasers = FEATURED_FAQ_QS .map((q) => findFaq(q)) .filter((e): e is NonNullable => e !== undefined); --- - +
    - -
    + + +
    +
    +

    The team institutions already know

    +

    + Institutions want to move real financial activity onto Ethereum. + A public ledger exposes positions, counterparties, and flows they are not allowed to reveal. +

    +

    + EthSystems, the team behind the Ethereum Foundation’s Institutional Privacy Task Force, builds the confidential, compliant systems that make that possible, from proof-of-concept through to production, backed by a year of open-source work already shipped. +

    +

    + What we offer +

    @@ -79,7 +108,6 @@ const faqTeasers = FEATURED_FAQ_QS

    Read worked architectures directly. Each names trade-offs in plain language, evaluates under CROPS, and references a working prototype where one exists.

    {stats.approaches} approaches
    - @@ -88,7 +116,6 @@ const faqTeasers = FEATURED_FAQ_QS

    Begin from the institutional question that mirrors your situation. Each use case pairs with one or more approaches that demonstrate how others have built solutions.

    {stats.useCases} use cases
    - @@ -97,7 +124,6 @@ const faqTeasers = FEATURED_FAQ_QS

    Build your own approach using the cryptographic primitives applied in our case studies. Each building block includes its risk profile, architectural layer, and trade-offs.

    {stats.patterns} building blocks
    -
    @@ -108,7 +134,7 @@ const faqTeasers = FEATURED_FAQ_QS

    Who is this for

    -

    IPTF coordinates between vendors and protocols, institutions, and regulators. The map and the approaches are written so each group can reach the same evidence by a different door. Pick the column that fits your role; each one is a recommended reading order, not a forced path.

    +

    EthSystems coordinates between vendors and protocols, institutions, and regulators. The map and the approaches are written so each group can reach the same evidence by a different door. Pick the column that fits your role; each one is a recommended reading order, not a forced path.

    @@ -259,7 +285,7 @@ const faqTeasers = FEATURED_FAQ_QS

    Recent writeups from the team

    -

    Long-form research, guides, POCs, and analysis from IPTF engineers.

    +

    Long-form research, guides, POCs, and analysis from the EthSystems team.

    @@ -267,7 +293,7 @@ const faqTeasers = FEATURED_FAQ_QS
    {p.image ? ( - + ) : (
    - + + + + @@ -350,7 +380,7 @@ const faqTeasers = FEATURED_FAQ_QS
  • @@ -394,33 +424,33 @@ const faqTeasers = FEATURED_FAQ_QS
    -

    Talk to the IPTF team

    +

    Talk to EthSystems

    If you are an institution with a privacy requirement, a builder shipping a solution, or a regulator who wants the picture, we are listening.

    - - + +
    -

    iptf@ethereum.org

    -

    General correspondence with the team.

    +

    Contact us

    +

    Tell us about your requirements and the team will follow up.

    - - + +
    -

    GitHub · iptf-map

    -

    Open issues, propose patterns, contribute use cases.

    +

    {site.email}

    +

    General correspondence with the team.

    - - + +
    -

    GitHub · iptf-pocs

    -

    Working prototypes and reference implementations.

    +

    GitHub · map

    +

    Open issues, propose patterns, contribute use cases.

    @@ -432,42 +462,42 @@ const faqTeasers = FEATURED_FAQ_QS diff --git a/src/pages/jurisdictions/index.astro b/src/pages/jurisdictions/index.astro index cf5c1e4..fc08dc7 100644 --- a/src/pages/jurisdictions/index.astro +++ b/src/pages/jurisdictions/index.astro @@ -32,6 +32,7 @@ const tabs = orderedRegions.map((r) => ({ pageTitle="Jurisdictions" description="Jurisdictions" current="/jurisdictions/" + image="/og/jurisdictions.png" > diff --git a/src/pages/map.astro b/src/pages/map.astro index f040f0c..0dfc023 100644 --- a/src/pages/map.astro +++ b/src/pages/map.astro @@ -17,6 +17,7 @@ const description = title="Galaxy view" description={description} current="/explore/" + image="/og/map.png" > + + +
    +
    +
      + {offerings.map((o) => ( +
    • +

      {o.lead} {o.body}

      +
    • + ))} +
    + + +
    +
    + + + diff --git a/src/pages/patterns/index.astro b/src/pages/patterns/index.astro index 5bedd02..439d13b 100644 --- a/src/pages/patterns/index.astro +++ b/src/pages/patterns/index.astro @@ -83,6 +83,7 @@ const tabs = orderedBuckets.map((b) => ({ pageTitle="Building blocks" description="Building blocks" current="/patterns/" + image="/og/patterns.png" > diff --git a/src/pages/rfps/index.astro b/src/pages/rfps/index.astro index 661f310..7ccd2f7 100644 --- a/src/pages/rfps/index.astro +++ b/src/pages/rfps/index.astro @@ -95,7 +95,7 @@ const items = (await getCollection('rfps')).slice().sort((a, b) => { border-radius: 999px; border: 1px solid var(--color-border); color: var(--color-text-secondary); - background: rgba(255, 255, 255, 0.5); + background: color-mix(in srgb, var(--surface) 60%, transparent); } .badge.tier-1 { background: var(--color-cta-tint-strong); color: var(--color-cta); border-color: var(--color-cta-border); } .rfp-card p { diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts new file mode 100644 index 0000000..b12b016 --- /dev/null +++ b/src/pages/rss.xml.ts @@ -0,0 +1,55 @@ +import rss from '@astrojs/rss'; +import { getCollection } from 'astro:content'; +import type { APIContext } from 'astro'; +import { jekyllSlugify } from '../lib/jekyllSlug'; +import { isPublished } from '../lib/posts'; +import { site } from '../lib/site'; + +/* + * /rss.xml — feed for the long-form writeups at /blog/. + * + * Mirrors the blog index exactly: same `posts` collection, same + * isPublished date gate, same title-derived slugs, newest-first. + * Discovered via the tag in BaseLayout. + */ + +function escapeXml(s: string): string { + return s + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>'); +} + +export async function GET(context: APIContext) { + const all = await getCollection('posts'); + const posts = all + .filter((p) => isPublished(p)) + .map((p) => ({ + title: p.data.title, + description: p.data.description ?? '', + link: `/blog/${jekyllSlugify(p.data.title)}/`, + pubDate: + p.data.date instanceof Date ? p.data.date : new Date(p.data.date), + author: p.data.author, + })) + .sort((a, b) => b.pubDate.getTime() - a.pubDate.getTime()); + + return rss({ + title: `${site.name} · Writeups`, + description: + 'Field notes from EthSystems on building privacy on Ethereum for institutional use cases.', + site: context.site ?? site.domain, + // dc:creator carries the author name; the RSS element + // requires an email address, which posts don't have. + xmlns: { dc: 'http://purl.org/dc/elements/1.1/' }, + items: posts.map((p) => ({ + title: p.title, + description: p.description, + link: p.link, + pubDate: p.pubDate, + ...(p.author + ? { customData: `${escapeXml(p.author)}` } + : {}), + })), + }); +} diff --git a/src/pages/use-cases/[slug].astro b/src/pages/use-cases/[slug].astro index 61ec407..f15c4bb 100644 --- a/src/pages/use-cases/[slug].astro +++ b/src/pages/use-cases/[slug].astro @@ -32,7 +32,30 @@ const cleanedBody = (useCaseContent ? bodyWithout : entry.body).replace( '$1', ); const lede = useCaseContent || bodyExcerpt(entry.body); -const bodyHtml = renderMarkdown(cleanedBody); + +// Upstream use-case markdown always emits a "## 2) Additional Context" +// heading, but it's blank for most entries (only a handful carry Market +// Signals etc.). Drop any H2 whose section body is empty so the page +// never renders a bare heading with nothing underneath it. +function dropEmptySections(md: string): string { + const lines = md.split('\n'); + const out: string[] = []; + for (let i = 0; i < lines.length; i++) { + if (/^##\s+/.test(lines[i])) { + let j = i + 1; + let hasContent = false; + for (; j < lines.length; j++) { + if (/^##\s+/.test(lines[j])) break; + if (lines[j].trim() !== '') { hasContent = true; break; } + } + if (!hasContent) { i = j - 1; continue; } + } + out.push(lines[i]); + } + return out.join('\n'); +} + +const bodyHtml = renderMarkdown(dropEmptySections(cleanedBody)); const domainIndex = buildDomainIndex(await getCollection('domains')); const domain = domainIndex.resolve(entry.data.primary_domain) ?? entry.data.primary_domain ?? null; diff --git a/src/pages/use-cases/index.astro b/src/pages/use-cases/index.astro index 840eee4..f6d32a4 100644 --- a/src/pages/use-cases/index.astro +++ b/src/pages/use-cases/index.astro @@ -37,7 +37,7 @@ for (const uc of useCases) { const orderedDomains: Domain[] = domainIndex.names.filter((d) => grouped.get(d)!.length > 0); // Per-domain primer is the domain's frontmatter `description` (a curated -// one-liner, iptf-map PR #173). Falls back to the first ## TLDR bullet +// one-liner, ethsystems/map PR #173). Falls back to the first ## TLDR bullet // where no description exists yet. Links to the domain detail page. function domainPrimer(d: Domain): { html: string; href: string } | undefined { const entry = domainIndex.entryFor(d) as @@ -67,6 +67,7 @@ const tabs = orderedDomains.map((d) => ({ pageTitle="Use cases" description="Use cases" current="/use-cases/" + image="/og/use-cases.png" > diff --git a/src/pages/vendors/[slug].astro b/src/pages/vendors/[slug].astro index a3c4872..c46a835 100644 --- a/src/pages/vendors/[slug].astro +++ b/src/pages/vendors/[slug].astro @@ -1,7 +1,7 @@ --- /* * Vendor detail — adds a "Used in" reverse-reference sidebar per the - * iptf-web merge handoff (locked 2026-05-19, frontmatter-only scope). + * ethsystems/web merge handoff (locked 2026-05-19, frontmatter-only scope). * * Scans every approach's sub-approach yaml blocks for `example_vendors` * arrays containing this vendor's slug. Surfaces those approach + diff --git a/src/pages/vendors/index.astro b/src/pages/vendors/index.astro index 1390bc4..d1cd3b6 100644 --- a/src/pages/vendors/index.astro +++ b/src/pages/vendors/index.astro @@ -39,6 +39,7 @@ function shortMaturity(s: string | undefined): string | undefined { pageTitle="Vendors" description="Vendors" current="/vendors/" + image="/og/vendors.png" >
    diff --git a/src/plugins/remark-approach-variants.ts b/src/plugins/remark-approach-variants.ts index 8492b1e..3e3f19e 100644 --- a/src/plugins/remark-approach-variants.ts +++ b/src/plugins/remark-approach-variants.ts @@ -110,7 +110,7 @@ function summaryHtml(headingText: string, meta: VariantMeta): string { function pocButtonHtml(meta: VariantMeta): string { const links: string[] = []; if (meta.poc_spec) { - const url = `https://github.com/ethereum/iptf-pocs/blob/master/${meta.poc_spec}`; + const url = `https://github.com/ethsystems/pocs/blob/master/${meta.poc_spec}`; links.push( `View PoC spec `, ); diff --git a/src/plugins/remark-rewrite-links.ts b/src/plugins/remark-rewrite-links.ts index db0f78f..dbd8571 100644 --- a/src/plugins/remark-rewrite-links.ts +++ b/src/plugins/remark-rewrite-links.ts @@ -1,9 +1,9 @@ /** - * Remark plugin: rewrite relative markdown cross-links from iptf-map + * Remark plugin: rewrite relative markdown cross-links from ethsystems/map * (e.g. "../approaches/approach-private-bonds.md") into internal Astro * routes ("/approaches/approach-private-bonds/"). * - * iptf-map convention (verified across the repo): + * ethsystems/map convention (verified across the repo): * - All internal links are relative paths to .md files. * - Anchor fragments are not used in internal links. * - Same-folder links use `filename.md` (no `./`). @@ -32,7 +32,7 @@ const KNOWN_COLLECTIONS = new Set([ 'rfps', ]); -const GITHUB_BLOB = 'https://github.com/ethereum/iptf-map/blob/master'; +const GITHUB_BLOB = 'https://github.com/ethsystems/map/blob/master'; const TYPE_PREFIXES = ['approach', 'pattern', 'rfp', 'domain', 'jurisdiction', 'vendor']; diff --git a/src/posts/2026-01-09-cypherpunk-institutional-privacy.md b/src/posts/2026-01-09-cypherpunk-institutional-privacy.md index 4721688..182f781 100644 --- a/src/posts/2026-01-09-cypherpunk-institutional-privacy.md +++ b/src/posts/2026-01-09-cypherpunk-institutional-privacy.md @@ -4,7 +4,7 @@ title: "Cypherpunk x Institutional Privacy" description: "Exploring the overlap and tension between cypherpunks and institutions - and how the Institutional Privacy Task Force is mapping real institutional privacy requirements to solutions on Ethereum." date: 2026-01-09 author: "Oskar" -image: /assets/images/2026-01-09-cypherpunk-institutional-privacy/cypherpunk_institutional_privacy.webp +image: ../assets/posts/2026-01-09-cypherpunk-institutional-privacy/cypherpunk_institutional_privacy.webp tags: - institutional-privacy - cypherpunks @@ -12,6 +12,8 @@ tags: - privacy-map --- +*This post was written when IPTF (now EthSystems) was at the Ethereum Foundation* + *The following write-up is based on a [talk](https://youtu.be/NkgErrMXuIk?si=vrccSYpwruI1DK3J) given at Cypherpunk Congress, Devconnect in Buenos Aires, November, 2025.* Today I'm going to talk about cypherpunks and institutional privacy. These are two words you usually don't see in the same sentence, and I thought it'd be interesting to explore both the overlap and tension between them - cypherpunks and institutions are very different groups of people. What is the intersection, if any, between them when it comes to privacy? How should we think about this? @@ -120,7 +122,7 @@ How is the map structured? We have the following categories: Regarding jurisdictions: we aren't legal experts, but it is useful to have a TL;DR of various regulations. For example: how does the GENIUS Act impact stablecoin design? -![The map: structure on GitHub](/assets/images/2026-01-09-cypherpunk-institutional-privacy/iptf-map-github.png) +![The map: structure on GitHub](../assets/posts/2026-01-09-cypherpunk-institutional-privacy/iptf-map-github.png) ### The Map: how to navigate Depending on what type of user you are, you might navigate the map differently. We have identified three primary personas: business, technical, and legal/compliance. These reflect different levels of expertise as well as concern. @@ -186,9 +188,9 @@ Institutions want privacy on Ethereum. We're building an open, shared privacy ma ## Join us ### Resources -- GitHub: [ethereum/iptf-map](https://github.com/ethereum/iptf-map) -- Web: [iptf.ethereum.org](https://iptf.ethereum.org/) -- X: [@iptf_updates](https://twitter.com/iptf_updates) +- GitHub: [ethsystems/map](https://github.com/ethsystems/map) +- Web: [ethsystems.org](https://ethsystems.org/) +- X: [@eth_systems](https://x.com/eth_systems) You can contribute patterns, approaches, and vendor notes. Also feel free to come talk with us - bring real constraints and feedback. diff --git a/src/posts/2026-01-21-building-private-bonds-on-ethereum.md b/src/posts/2026-01-21-building-private-bonds-on-ethereum.md index bc1c4df..8fc63dd 100644 --- a/src/posts/2026-01-21-building-private-bonds-on-ethereum.md +++ b/src/posts/2026-01-21-building-private-bonds-on-ethereum.md @@ -4,7 +4,7 @@ title: "Building Private Bonds on Ethereum" description: "A walkthrough of a proof-of-concept for private zero-coupon bonds using zero-knowledge proofs on Ethereum - covering the UTXO model, JoinSplit circuits, atomic swaps, and the relayer architecture." date: 2026-01-21 author: "Yanis" -image: /assets/images/2026-01-16-building-private-bonds-on-ethereum/building_private_bonds_on_ethereum.png +image: ../assets/posts/2026-01-16-building-private-bonds-on-ethereum/building_private_bonds_on_ethereum.png tags: - private-bonds - zero-knowledge-proofs @@ -13,6 +13,8 @@ tags: - proof-of-concept --- +*This post was written when IPTF (now EthSystems) was at the Ethereum Foundation* + 2025 has been a turning point and an unprecedented wave of tokenization is on the horizon. For those new to the topic, tokenization means representing traditional financial assets (like bonds, stocks, or real estate) as digital tokens on a blockchain. The main standard for tokens on Ethereum is [ERC-20](https://eips.ethereum.org/EIPS/eip-20), a representation of fungible tokens that is very versatile and can represent any form of asset. This article explores solutions for the problem encountered when using the straight ERC-20 standard. Ethereum being a fully transparent ledger, using ERC-20 exposes too much: who holds what, every transfer, every counterparty relationship. For institutions, that's a dealbreaker. This post walks through a proof-of-concept that reconciles tokenization with privacy. We built private [zero-coupon bonds](https://en.wikipedia.org/wiki/Zero-coupon_bond) using zero-knowledge proofs, achieving confidential balances and transfers while preserving a full audit trail for regulators. We'll cover why we chose this approach, how the protocol works, and where it falls short. @@ -25,7 +27,7 @@ Zero-coupon bonds are bonds sold at a discount that pay their full face value at A simple example is: Alice buys a bond from Bob for $950 today that will be worth $1,000 in one year. Alice holds the bond until maturity, then redeems it for the full $1,000. The $50 difference is her return, no interest payments needed in between. -Private bonds are one of the use cases documented in the [IPTF Map](https://github.com/ethereum/iptf-map), specifically in the [private bonds approach](https://github.com/ethereum/iptf-map/blob/master/approaches/approach-private-bonds.md) where we're drawing the foundation of the explored PoC. The Map is the knowledge base we're building to help institutions navigate privacy on Ethereum. This particular use case emerged from discussions with a major European bank, who laid out their requirements in detail. +Private bonds are one of the use cases documented in the [IPTF Map](https://github.com/ethsystems/map), specifically in the [private bonds approach](https://github.com/ethsystems/map/blob/master/approaches/approach-private-bonds.md) where we're drawing the foundation of the explored PoC. The Map is the knowledge base we're building to help institutions navigate privacy on Ethereum. This particular use case emerged from discussions with a major European bank, who laid out their requirements in detail. When you're trying to bring privacy to financial products on-chain, you want to start with the simplest possible instrument. Zero-coupon bonds are ideal: no periodic coupon payments, no price feeds from oracles, no daily rebalancing. A single timestamp check is enough to enforce the entire contract. @@ -80,11 +82,11 @@ Commitment = hash(value, salt, owner, assetId, maturityDate) Nullifier = hash(salt, private_key) ``` -![Commitment and nullifier derivation](/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-6-commitment-and-nullifier.png) +![Commitment and nullifier derivation](../assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-6-commitment-and-nullifier.png) The protocol has four operations. -![Bond lifecycle](/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-2-bond-lifecycle.png) +![Bond lifecycle](../assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-2-bond-lifecycle.png) When an issuer creates a bond tranche--a specific issuance or series of bonds with identical characteristics like maturity date and terms--they generate a note for the full amount and mint its commitment on-chain. No proof is needed here because the issuer is trusted. The Merkle tree grows by one leaf. @@ -98,11 +100,11 @@ The circuit enforces several constraints: 4. **Asset Consistency:** All notes use the same `assetId` 5. **Maturity Match:** `input_maturity = output_maturity`. Maturity preserved -![Circuit constraints](/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-4-circuit-constraints.png) +![Circuit constraints](../assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-4-circuit-constraints.png) When two parties trade, things get more interesting. Alice wants to swap her bond for Bob's. They can't just do two separate transfers because one might fail. Instead, both create proofs that spend their notes and output to the counterparty. The relayer submits both proofs in a single `atomicSwap` transaction. Either both execute or neither does. -![Atomic swap flow](/assets/images/2026-01-16-building-private-bonds-on-ethereum/img-5-atomic-swap-flow.png) +![Atomic swap flow](../assets/posts/2026-01-16-building-private-bonds-on-ethereum/img-5-atomic-swap-flow.png) But there's a coordination problem. After the swap, Bob needs to know the details of his new note (value, salt) to spend it later. The commitment is public, but it's just a hash. diff --git a/src/posts/2026-01-30-public-rails-vs-private-ledgers.md b/src/posts/2026-01-30-public-rails-vs-private-ledgers.md index 4a627d7..a402853 100644 --- a/src/posts/2026-01-30-public-rails-vs-private-ledgers.md +++ b/src/posts/2026-01-30-public-rails-vs-private-ledgers.md @@ -4,7 +4,7 @@ title: "Public Rails vs Private Ledgers" description: "An institutional decision framework for choosing between public blockchains with cryptographic privacy and private ledgers with trust-based privacy." date: 2026-01-30 author: "Oskar" -image: /assets/images/2026-01-30-public-rails-vs-private-ledgers/public_rails_vs_private_ledgers_hero.jpg +image: ../assets/posts/2026-01-30-public-rails-vs-private-ledgers/public_rails_vs_private_ledgers_hero.jpg tags: - institutional-privacy - decision-framework @@ -12,6 +12,8 @@ tags: - ethereum --- +*This post was written when IPTF (now EthSystems) was at the Ethereum Foundation* + *An institutional decision framework* Over the past decade, many of us have watched a familiar pattern play out. @@ -105,7 +107,7 @@ Bitcoin and Ethereum answered this clearly. They demonstrated that you can have Permissioned blockchains tried to capture blockchain benefits while preserving institutional control. But in doing so, they sacrificed resilience, the core value proposition. If you trust the consortium operators to run the network honestly, to respect your data boundaries, to not change the rules against your interests, you are back to trusting people. At that point, the question becomes: why not a database with legal contracts? -![Why not just a database?](/assets/images/2026-01-30-public-rails-vs-private-ledgers/why_not_database.png) +![Why not just a database?](../assets/posts/2026-01-30-public-rails-vs-private-ledgers/why_not_database.png) **Trust-based privacy** says: "I promise I won't look at your data." Operators, validators, and governance commit to respect data boundaries. If incentives shift, if regulators demand access, if governance evolves, the promise breaks. @@ -134,7 +136,7 @@ Multiple production systems now exist for cryptographic privacy on public infras **Mature shielding:** Railgun and similar ZK privacy pools have processed billions in shielded volume across Ethereum and L2s. -**Others:** Fhenix and Zama (FHE-based), Shutter Network (encrypted mempool), Renegade (dark pools), EY Nightfall and Miden (privacy rollups). For a comprehensive map of privacy solutions, see the [IPTF Privacy Map](https://github.com/ethereum/iptf-map). +**Others:** Fhenix and Zama (FHE-based), Shutter Network (encrypted mempool), Renegade (dark pools), EY Nightfall and Miden (privacy rollups). For a comprehensive map of privacy solutions, see the [IPTF Privacy Map](https://github.com/ethsystems/map). **Why this matters beyond features:** @@ -179,11 +181,11 @@ How the two approaches compare across key dimensions: | TCO model | SaaS | Utility | | Track record | Canton Network: ~2 years | ~10 years, no complete outages | -![Architecture Comparison](/assets/images/2026-01-30-public-rails-vs-private-ledgers/architecture_comparison.png) +![Architecture Comparison](../assets/posts/2026-01-30-public-rails-vs-private-ledgers/architecture_comparison.png) **Key difference:** Canton failures are correlated (one domain down stops the entire cross-domain transaction). Public L2 failures are isolated (L2 down doesn't stop the L1 or other L2s). -![What You're Betting On](/assets/images/2026-01-30-public-rails-vs-private-ledgers/betting_slips.png) +![What You're Betting On](../assets/posts/2026-01-30-public-rails-vs-private-ledgers/betting_slips.png) ## Closing @@ -193,7 +195,7 @@ For institutions with genuine privacy requirements (adversarial counterparties, The Ethereum privacy stack is live, battle-tested, and increasingly enterprise-ready. The seven questions above will tell you whether it fits your requirements. -For implementation patterns and institutional guidance, see the [IPTF Privacy Map](https://github.com/ethereum/iptf-map) or contact the [Institutional Privacy Task Force](https://iptf.ethereum.org/). +For implementation patterns and institutional guidance, see the [IPTF Privacy Map](https://github.com/ethsystems/map) or contact the [Institutional Privacy Task Force](https://ethsystems.org/). ## References diff --git a/src/posts/2026-02-05-private-bonds-on-privacy-l2s.md b/src/posts/2026-02-05-private-bonds-on-privacy-l2s.md index e94e954..ab6eeb9 100644 --- a/src/posts/2026-02-05-private-bonds-on-privacy-l2s.md +++ b/src/posts/2026-02-05-private-bonds-on-privacy-l2s.md @@ -4,7 +4,7 @@ title: "Building Private Bonds on Ethereum - Part 2" description: "Part 2 of our private bonds series: we rebuild the same protocol on Aztec, where notes, nullifiers, and ZK proofs are handled by the network itself. 200 lines of Noir replace three separate components." date: 2026-02-05 09:00:00 +0100 author: "Yanis" -image: /assets/images/2026-01-16-building-private-bonds-on-ethereum/building_private_bonds_on_ethereum.png +image: ../assets/posts/2026-01-16-building-private-bonds-on-ethereum/building_private_bonds_on_ethereum.png tags: - private-bonds - privacy-L2 @@ -14,6 +14,8 @@ tags: - proof-of-concept --- +*This post was written when IPTF (now EthSystems) was at the Ethereum Foundation* + In [Part 1](/building-private-bonds-on-ethereum/), we built private zero-coupon bonds from scratch on Ethereum. The result worked, but required three distinct components: a Noir circuit for ZK proofs, a Solidity contract for on-chain state, and a Rust wallet for key management and proof generation. We also needed a trusted relayer (the issuer) to coordinate transactions and prevent frontrunning. _That architecture raised an obvious question: what if the network itself handled all this complexity?_ @@ -32,7 +34,7 @@ When we built the custom UTXO system, we had to implement every privacy primitiv **Encrypted mempool** solves frontrunning without a trusted relayer. In our custom implementation, the issuer had to batch transactions to prevent competitors from seeing pending trades. On Aztec, transactions are encrypted before entering the mempool. Sequencers process them without knowing the contents until execution. -![PXE architecture and encrypted transaction flow](/assets/images/2026-02-05-private-bonds-on-privacy-l2s/img-2-public-private-aztec.png) +![PXE architecture and encrypted transaction flow](../assets/posts/2026-02-05-private-bonds-on-privacy-l2s/img-2-public-private-aztec.png) _Source: [Aztec Documentation](https://docs.aztec.network/developers/docs/foundational-topics)_ **Decentralized sequencing** removes the single point of trust. Our custom system required the issuer to relay all transactions. On Aztec, a decentralized sequencer network orders and executes transactions. The issuer remains important for business logic (whitelist management, distribution), but loses their privileged position in transaction ordering. @@ -131,7 +133,7 @@ For atomic DvP (Delivery-vs-Payment), the flow becomes: 4. Contract verifies both authwits, atomically swaps assets 5. Both authwits are nullified (cannot be replayed) -![Authwit DvP flow](/assets/images/2026-02-05-private-bonds-on-privacy-l2s/img-1-authwit-dvp.png) +![Authwit DvP flow](../assets/posts/2026-02-05-private-bonds-on-privacy-l2s/img-1-authwit-dvp.png) The key property: both parties commit to exact terms before execution. The seller cannot receive less than expected. The buyer cannot pay more. If either authwit is missing or mismatched, the transaction fails atomically. @@ -214,6 +216,6 @@ The tradeoffs are also structural to this approach: For teams that want to ship a prototype without building cryptographic infrastructure from scratch, privacy L2s offer a faster starting point. For teams that need precise control over every layer, the custom UTXO approach from Part 1 remains viable. -The full implementation (on Aztec) is [open source](https://github.com/ethereum/iptf-pocs/tree/main/pocs/private-bond/privacy-l2), with a detailed [specification](https://github.com/ethereum/iptf-pocs/blob/main/pocs/private-bond/privacy-l2/SPEC.md) covering the protocol design. +The full implementation (on Aztec) is [open source](https://github.com/ethsystems/pocs/tree/main/pocs/private-bond/privacy-l2), with a detailed [specification](https://github.com/ethsystems/pocs/blob/main/pocs/private-bond/privacy-l2/SPEC.md) covering the protocol design. In Part 3, we will explore a third approach: fully homomorphic encryption (FHE). Where UTXO models hide data by never putting it on-chain, FHE allows computation on encrypted data directly. Different cryptography, different tradeoffs, same institutional requirements. diff --git a/src/posts/2026-02-12-private-bonds-with-fhe.md b/src/posts/2026-02-12-private-bonds-with-fhe.md index 88b93a4..0855e7b 100644 --- a/src/posts/2026-02-12-private-bonds-with-fhe.md +++ b/src/posts/2026-02-12-private-bonds-with-fhe.md @@ -3,7 +3,7 @@ layout: post title: "Building Private Bonds on Ethereum - Part 3" date: 2026-02-12 09:00:00 +0100 author: "Yanis" -image: /assets/images/2026-01-16-building-private-bonds-on-ethereum/building_private_bonds_on_ethereum.png +image: ../assets/posts/2026-01-16-building-private-bonds-on-ethereum/building_private_bonds_on_ethereum.png description: "Exploring Fully Homomorphic Encryption as a path to confidential institutional bonds on Ethereum, the third approach in our Private Bond PoC series." tags: - private-bonds @@ -14,6 +14,8 @@ tags: - proof-of-concept --- +*This post was written when IPTF (now EthSystems) was at the Ethereum Foundation* + _This is Part 3 of our Private Bond proof-of-concept series. In [Part 1](/building-private-bonds-on-ethereum/) we explored Custom UTXOs, in [Part 2](/private-bonds-on-privacy-l2s/) we covered Privacy L2s with Aztec. Now we examine a fundamentally different approach: Fully Homomorphic Encryption._ ## What is Fully Homomorphic Encryption? @@ -22,7 +24,7 @@ Fully Homomorphic Encryption (FHE) allows computations directly on encrypted dat Imagine a locked box that you can manipulate from the outside (adding, subtracting, comparing what's inside) without ever opening it. Only the keyholder can peek inside, but anyone can perform operations. -![FHE transfer flow showing encryption, on-chain computation, and threshold decryption](/assets/images/2026-02-12-private-bonds-fhe/fhe-homomorphic-property.png) +![FHE transfer flow showing encryption, on-chain computation, and threshold decryption](../assets/posts/2026-02-12-private-bonds-fhe/fhe-homomorphic-property.png) This property makes FHE compelling for privacy-preserving finance: a smart contract can update encrypted balances, verify encrypted conditions, and transfer encrypted amounts, all while the actual values remain hidden from everyone including the blockchain observers. @@ -87,7 +89,7 @@ Zama's architecture separates concerns: **Threshold Network**: When decryption is needed (e.g., user wants to see their balance), they request it through a Gateway. The threshold network coordinates: if 9 of 13 operators agree, the value is decrypted and returned. -![Zama architecture: on-chain contract, off-chain coprocessor, and threshold decryption network](/assets/images/2026-02-12-private-bonds-fhe/zama-architecture.png) +![Zama architecture: on-chain contract, off-chain coprocessor, and threshold decryption network](../assets/posts/2026-02-12-private-bonds-fhe/zama-architecture.png) This means: @@ -97,7 +99,7 @@ This means: ## The Smart Contract -Our [`ConfidentialBond.sol`](https://github.com/ethereum/iptf-pocs/tree/master/pocs/private-bond/fhe) contract is roughly 300 lines, simpler than UTXO, comparable to Aztec. Here's the core structure: +Our [`ConfidentialBond.sol`](https://github.com/ethsystems/pocs/tree/master/pocs/private-bond/fhe) contract is roughly 300 lines, simpler than UTXO, comparable to Aztec. Here's the core structure: ### Public State @@ -131,7 +133,7 @@ Users must query their balance after the transaction to confirm whether it succe **Redemption**: After maturity, bondholders burn their holdings. Settlement occurs off-chain. -**[Audit Access](https://github.com/ethereum/iptf-map/blob/master/patterns/pattern-regulatory-disclosure-keys-proofs.md)**: The issuer can grant regulators permission to decrypt specific balances: +**[Audit Access](https://github.com/ethsystems/map/blob/master/patterns/pattern-regulatory-disclosure-keys-proofs.md)**: The issuer can grant regulators permission to decrypt specific balances: ```solidity function grantAuditAccess(address account, address auditor) external onlyOwner { @@ -143,7 +145,7 @@ Access is per-ciphertext. When a balance changes (new ciphertext handle), the au ## Comparing the Three Approaches -We've now built the same bond (whitelisted participants, private amounts, regulatory access) using three different privacy technologies. Here's how they compare (see also our [approach analysis](https://github.com/ethereum/iptf-map/blob/master/approaches/approach-private-bonds.md) and [benchmarks](https://github.com/ethereum/iptf-pocs/blob/master/pocs/private-bond/BENCHMARK.md)): +We've now built the same bond (whitelisted participants, private amounts, regulatory access) using three different privacy technologies. Here's how they compare (see also our [approach analysis](https://github.com/ethsystems/map/blob/master/approaches/approach-private-bonds.md) and [benchmarks](https://github.com/ethsystems/pocs/blob/master/pocs/private-bond/BENCHMARK.md)): | Aspect | Custom UTXO | Privacy L2 (Aztec) | FHE (Zama) | | -------------------- | ------------------ | ------------------ | ----------------------- | @@ -171,19 +173,19 @@ We've now built the same bond (whitelisted participants, private amounts, regula ## Trade-offs -[Custom UTXO](https://github.com/ethereum/iptf-map/blob/master/patterns/pattern-zk-shielded-balances.md) offers the strongest privacy guarantees: even addresses are obscured via nullifiers, and users control their own keys. Railgun and similar systems prove the model works in production. But implementation complexity is significant. Our PoC required building notes, Merkle trees, and nullifier management from scratch. Nullifiers also accumulate forever, creating storage concerns at scale (mitigations like [epoch-based pruning](https://eprint.iacr.org/2025/2031) are being researched but not yet deployed). +[Custom UTXO](https://github.com/ethsystems/map/blob/master/patterns/pattern-zk-shielded-balances.md) offers the strongest privacy guarantees: even addresses are obscured via nullifiers, and users control their own keys. Railgun and similar systems prove the model works in production. But implementation complexity is significant. Our PoC required building notes, Merkle trees, and nullifier management from scratch. Nullifiers also accumulate forever, creating storage concerns at scale (mitigations like [epoch-based pruning](https://eprint.iacr.org/2025/2031) are being researched but not yet deployed). -[Privacy L2s](https://github.com/ethereum/iptf-map/blob/master/patterns/pattern-privacy-l2s.md) like Aztec handle the hard parts for you: notes, proofs, encryption. Our contract was just 200 lines. Private composability is native, meaning your bonds could interact with private lending or swaps on the same L2. The catch: neither Aztec nor Miden are live yet (both scheduled for launch later in 2026), so we can't measure real costs. And the learning curve exists: Noir is not Solidity. +[Privacy L2s](https://github.com/ethsystems/map/blob/master/patterns/pattern-privacy-l2s.md) like Aztec handle the hard parts for you: notes, proofs, encryption. Our contract was just 200 lines. Private composability is native, meaning your bonds could interact with private lending or swaps on the same L2. The catch: neither Aztec nor Miden are live yet (both scheduled for launch later in 2026), so we can't measure real costs. And the learning curve exists: Noir is not Solidity. FHE is the gentlest onramp. If you know Solidity, you can write confidential contracts quickly. Standard wallets work. But you trade self-custody for threshold trust: your funds depend on the network's availability, and if a quorum of KMS operators collude, they could reconstruct the decryption key. In normal operation, no individual operator sees plaintext (user decryptions are re-encrypted under the user's key via MPC). For institutions already comfortable with custodial relationships, this may be acceptable. For those seeking self-sovereign privacy, it's a meaningful concession. -It's worth noting that the coprocessor model isn't exclusive to FHE. [TACEO's Merces](https://merces-demo.taceo.io/) (built from their [private_deposit](https://github.com/TaceoLabs/private_deposit/tree/main) codebase) demonstrates a similar architecture using MPC instead: a three-party network maintains secret-shared balances and generates collaborative SNARKs (Groth16 proofs) to verify state transitions on-chain. This is an application of the [co-SNARK pattern](https://github.com/ethereum/iptf-map/blob/master/patterns/pattern-co-snark.md), where multiple parties jointly produce a ZK proof without revealing their individual inputs. The trust assumption is the same, you rely on an honest majority of operators rather than holding your own keys, but the MPC path avoids FHE's computational overhead and adds native verifiability through the co-SNARK proofs. We could have built our bond PoC on this stack as well. +It's worth noting that the coprocessor model isn't exclusive to FHE. [TACEO's Merces](https://merces-demo.taceo.io/) (built from their [private_deposit](https://github.com/TaceoLabs/private_deposit/tree/main) codebase) demonstrates a similar architecture using MPC instead: a three-party network maintains secret-shared balances and generates collaborative SNARKs (Groth16 proofs) to verify state transitions on-chain. This is an application of the [co-SNARK pattern](https://github.com/ethsystems/map/blob/master/patterns/pattern-co-snark.md), where multiple parties jointly produce a ZK proof without revealing their individual inputs. The trust assumption is the same, you rely on an honest majority of operators rather than holding your own keys, but the MPC path avoids FHE's computational overhead and adds native verifiability through the co-SNARK proofs. We could have built our bond PoC on this stack as well. ## Conclusion Three paths to the same destination: private institutional bonds on Ethereum. Each works. Each makes different trade-offs on privacy, complexity, and trust. The choice depends on what your institution prioritizes. -The code for all three approaches is available in the [IPTF PoC repository](https://github.com/ethereum/iptf-pocs/tree/master/pocs/private-bond), with detailed specs and benchmarks. For a broader view of privacy patterns and vendor landscape, see the [IPTF Knowledge Map](https://github.com/ethereum/iptf-map). We welcome feedback and contributions. +The code for all three approaches is available in the [IPTF PoC repository](https://github.com/ethsystems/pocs/tree/master/pocs/private-bond), with detailed specs and benchmarks. For a broader view of privacy patterns and vendor landscape, see the [IPTF Knowledge Map](https://github.com/ethsystems/map). We welcome feedback and contributions. --- diff --git a/src/posts/2026-02-19-building-private-transfers-on-ethereum.md b/src/posts/2026-02-19-building-private-transfers-on-ethereum.md index 59e151c..71e62fd 100644 --- a/src/posts/2026-02-19-building-private-transfers-on-ethereum.md +++ b/src/posts/2026-02-19-building-private-transfers-on-ethereum.md @@ -4,7 +4,7 @@ title: "Building Private Transfers on Ethereum with Shielded Pools" description: "A proof-of-concept for compliance-first private stablecoin transfers using a shielded pool on Ethereum L1: covering KYC-gated entry, UTXO commitments, dual-key architecture, and ZK circuits in Noir." date: 2026-02-19 10:00:00 +0100 author: "Aaryamann" -image: /assets/images/2026-02-19-building-private-transfers-on-ethereum/hero.png +image: ../assets/posts/2026-02-19-building-private-transfers-on-ethereum/hero.png tags: - private-transfers - shielded-pools @@ -14,13 +14,15 @@ tags: - proof-of-concept --- +*This post was written when IPTF (now EthSystems) was at the Ethereum Foundation* + Every stablecoin transfer on Ethereum is public. When an institution moves USDC to a supplier, that payment is visible to every competitor, analyst, and observer on the network. Treasury positions, supplier relationships, settlement timing and payment frequency are visible to anyone with a block explorer. Traditional banking solved this decades ago. Payment details are visible only to the counterparties and their banks. On public blockchains, institutions don't have that option, unless we build it. In a [previous post](/building-private-bonds-on-ethereum/), we built private zero-coupon bonds using a UTXO model and ZK proofs. That PoC demonstrated the cryptographic primitives: commitments, nullifiers, Merkle trees, encrypted memos. This one tackles a different problem: stablecoin payments where compliance gating, not just privacy, is the primary design constraint. -This post walks through a proof-of-concept that brings banking-grade payment privacy to stablecoin transfers on Ethereum L1. The design prioritizes compliance-first privacy: only KYC-verified participants can enter the system, and viewing keys enable selective disclosure for regulators. The full implementation is [open source](https://github.com/ethereum/iptf-pocs/pull/15), with a detailed [specification](https://github.com/ethereum/iptf-pocs/tree/main/pocs/private-payment/shielded-pool/SPEC.md). +This post walks through a proof-of-concept that brings banking-grade payment privacy to stablecoin transfers on Ethereum L1. The design prioritizes compliance-first privacy: only KYC-verified participants can enter the system, and viewing keys enable selective disclosure for regulators. The full implementation is [open source](https://github.com/ethsystems/pocs/pull/15), with a detailed [specification](https://github.com/ethsystems/pocs/tree/main/pocs/private-payment/shielded-pool/SPEC.md). ## The Gated Shielded Pool @@ -62,7 +64,7 @@ The deposit proof's public inputs are the commitment, token address, amount, and One caveat: deposit and withdrawal amounts are public, so matching amounts can reveal a link, especially in small pools. Transfers hide amounts, but shielding and unshielding do not. Split notes via transfers before withdrawing to reduce correlation. -![Deposit Flow](/assets/images/2026-02-19-building-private-transfers-on-ethereum/deposit.png) +![Deposit Flow](../assets/posts/2026-02-19-building-private-transfers-on-ethereum/deposit.png) ### Transfer @@ -81,13 +83,13 @@ After verification, the contract marks both input nullifiers as spent and append The sender encrypts each output note for its recipient using ECDH: the sender generates an ephemeral key pair, computes a shared secret with the recipient's viewing public key, derives an encryption key via HKDF, and encrypts the note contents with [ChaCha20-Poly1305](https://datatracker.ietf.org/doc/html/rfc7539). The ciphertext is included in the transaction's `Transfer` event. Recipients scan these events, attempt decryption with their viewing key, and discover notes addressed to them. -![Transfer Flow](/assets/images/2026-02-19-building-private-transfers-on-ethereum/transfer.png) +![Transfer Flow](../assets/posts/2026-02-19-building-private-transfers-on-ethereum/transfer.png) ### Withdraw Converts a private note back to public tokens. The user proves they own a note in the commitment tree and that the claimed amount and recipient match. The contract verifies the proof, marks the nullifier as spent, and transfers tokens to the specified address. -![Withdrawal Flow](/assets/images/2026-02-19-building-private-transfers-on-ethereum/withdraw.png) +![Withdrawal Flow](../assets/posts/2026-02-19-building-private-transfers-on-ethereum/withdraw.png) ## Architecture @@ -151,4 +153,4 @@ The immediate next steps for the PoC are variable input/output circuits for effi For institutions tokenizing fiat on Ethereum, a gated shielded pool offers payment privacy with a compliance model that maps directly to existing banking practice: identity at onboarding, selective disclosure for auditors, revocable access. The tradeoff is a smaller anonymity set than permissionless alternatives and tooling that is not yet production-grade. But the cryptography is proven, the architecture works end-to-end, and the anonymity set grows with every customer onboarded. -The implementation is [open source](https://github.com/ethereum/iptf-pocs/pull/15). The [specification](https://github.com/ethereum/iptf-pocs/tree/master/pocs/private-payment/shielded-pool/SPEC.md) covers every circuit constraint, data structure, and security consideration in detail. The [use case](https://github.com/ethereum/iptf-map/blob/master/use-cases/private-stablecoins.md) and [approach](https://github.com/ethereum/iptf-map/blob/master/approaches/approach-private-payments.md) documents on the IPTF Map provide additional context on how this fits into the broader institutional privacy landscape. Pull requests are welcome. +The implementation is [open source](https://github.com/ethsystems/pocs/pull/15). The [specification](https://github.com/ethsystems/pocs/tree/master/pocs/private-payment/shielded-pool/SPEC.md) covers every circuit constraint, data structure, and security consideration in detail. The [use case](https://github.com/ethsystems/map/blob/master/use-cases/private-stablecoins.md) and [approach](https://github.com/ethsystems/map/blob/master/approaches/approach-private-payments.md) documents on the IPTF Map provide additional context on how this fits into the broader institutional privacy landscape. Pull requests are welcome. diff --git a/src/posts/2026-02-26-private-stablecoins-with-plasma.md b/src/posts/2026-02-26-private-stablecoins-with-plasma.md index 981bf35..aec7d17 100644 --- a/src/posts/2026-02-26-private-stablecoins-with-plasma.md +++ b/src/posts/2026-02-26-private-stablecoins-with-plasma.md @@ -4,7 +4,7 @@ title: "Building Private Transfers on Ethereum with Plasma" description: "Explore how ZK-plasma enables private stablecoin transfers on Ethereum. Covers off-chain execution, balance proofs, and deployment tradeoffs for institutions." date: 2026-02-26 09:00:00 +0100 author: "Aaryamann" -image: /assets/images/2026-02-26-private-stablecoins-with-plasma/hero.png +image: ../assets/posts/2026-02-26-private-stablecoins-with-plasma/hero.png tags: - private-transfers - plasma @@ -13,6 +13,8 @@ tags: - proof-of-concept --- +*This post was written when IPTF (now EthSystems) was at the Ethereum Foundation* + In a [recent post](/building-private-transfers-on-ethereum/), we built a shielded pool for private stablecoin transfers on Ethereum L1. The approach works: KYC-gated entry, UTXO commitments, dual-key architecture for selective disclosure. But every transfer writes new commitments and nullifiers to the chain. The pool contract's state grows with every transaction, and every state transition requires on-chain ZK proof verification. Rather than iterating on the shielded pool, we explored a different approach. @@ -29,11 +31,11 @@ For institutions, this changes the operating model. Transaction contents (amount The tradeoffs are different from the shielded pool. On-chain costs drop significantly since the chain only stores block roots and signatures. Privacy against public observers is stronger because transfer details are never published, but client-side proof generation is computationally intensive, the anonymity set depends on the deployment model, and the infrastructure stack (block builders, store vaults, provers) introduces new operational dependencies. -We built a proof-of-concept on [Intmax2](https://eprint.iacr.org/2025/021), a ZK-plasma protocol that implements this model with recursive proofs via [Plonky2](https://github.com/0xPolygonZero/plonky2). The [specification](https://github.com/ethereum/iptf-pocs/tree/master/pocs/private-payment/plasma/SPEC.md) covers every protocol flow and data structure in detail. +We built a proof-of-concept on [Intmax2](https://eprint.iacr.org/2025/021), a ZK-plasma protocol that implements this model with recursive proofs via [Plonky2](https://github.com/0xPolygonZero/plonky2). The [specification](https://github.com/ethsystems/pocs/tree/master/pocs/private-payment/plasma/SPEC.md) covers every protocol flow and data structure in detail. ## Architecture -![Architecture](/assets/images/2026-02-26-private-stablecoins-with-plasma/architecture.png) +![Architecture](../assets/posts/2026-02-26-private-stablecoins-with-plasma/architecture.png) - **Institution**: holds keys locally, initiates deposits, transfers, and withdrawals - **Store Vault**: encrypted off-chain storage where senders publish transaction data for recipients to retrieve @@ -54,11 +56,11 @@ Each component is pluggable: the proof backend, storage layer, and contract inte Deposits convert public ERC-20 tokens into a private balance on the plasma chain. The user locks tokens in a Liquidity contract on L1. The contract relays deposit data to the Rollup contract on the L2 via a cross-chain messenger. The Rollup contract inserts the deposit into its Merkle tree, and the validity prover asynchronously generates a proof for the new block state. The user polls until the deposit is confirmed, then updates their local balance proof. -In the target architecture, deposits are gated by an attestation registry: a ZK proof of Merkle inclusion in an on-chain KYC attestation tree, identical in concept to the [shielded pool's approach](/building-private-transfers-on-ethereum/). The [attestation registry](https://github.com/ethereum/iptf-pocs/pull/15) from the shielded pool PoC can be reused here with minimal modification; the core mechanism is the same. +In the target architecture, deposits are gated by an attestation registry: a ZK proof of Merkle inclusion in an on-chain KYC attestation tree, identical in concept to the [shielded pool's approach](/building-private-transfers-on-ethereum/). The [attestation registry](https://github.com/ethsystems/pocs/pull/15) from the shielded pool PoC can be reused here with minimal modification; the core mechanism is the same. The attestation proof is zero-knowledge: the on-chain verifier learns only that the depositor holds a valid, non-expired KYC attestation. It does not learn which attestation leaf was used, which compliance authority issued it, or when the attestation was granted. An observer sees that someone deposited a known amount of a known token, but cannot determine who deposited it or which compliance authority verified them. -![Deposit Flow](/assets/images/2026-02-26-private-stablecoins-with-plasma/deposit.png) +![Deposit Flow](../assets/posts/2026-02-26-private-stablecoins-with-plasma/deposit.png) *Deposit flow: tokens lock on L1, relay to the Rollup contract on L2, and the user updates their local balance proof after the validity prover confirms the block.* @@ -72,7 +74,7 @@ After the block is posted, the sender generates a recursive ZK validity proof at The zero-knowledge property here is precise: the recipient learns only the sender's identity, the amount, and that the sender had sufficient balance at the time of the transfer. They learn nothing about the sender's total balance, other recipients in the sender's transaction batch, or what any other sender in the block was doing. The sender list (public keys) for each block is visible on-chain, so observers can see *who* participated as senders, but not *what* they sent or to *whom*. Repeated participation across blocks also reveals activity frequency: an observer can track how often a public key appears as a sender, even without knowing transfer contents or recipients. -![Transfer Flow](/assets/images/2026-02-26-private-stablecoins-with-plasma/transfer.png) +![Transfer Flow](../assets/posts/2026-02-26-private-stablecoins-with-plasma/transfer.png) *Transfer flow: the block builder only sees salted hashes. After the block is posted, the sender encrypts the transaction details for the recipient via the store vault.* @@ -80,7 +82,7 @@ The zero-knowledge property here is precise: the recipient learns only the sende Withdrawals convert a private plasma balance back to public L1 tokens. The user constructs a transfer targeting an L1 address, which signals withdrawal intent and goes through the normal transfer protocol. Once the block is proven by the validity prover, the user submits a withdrawal claim to the Withdrawal contract with a ZK balance proof. The contract verifies the proof, deducts any previously withdrawn amounts, and transfers tokens to the L1 address. -![Withdraw Flow](/assets/images/2026-02-26-private-stablecoins-with-plasma/withdraw.png) +![Withdraw Flow](../assets/posts/2026-02-26-private-stablecoins-with-plasma/withdraw.png) *Withdrawal flow: the user proves their balance via a ZK proof and claims tokens on L1.* @@ -114,7 +116,7 @@ The design maps each privacy mechanism to a specific regulatory obligation: - **Compromised store vault:** operator learns access patterns (who queries when) but cannot decrypt data. - **Compromised viewing key:** leaks one user's full history without granting spending authority. -The [specification](https://github.com/ethereum/iptf-pocs/tree/master/pocs/private-payment/plasma/SPEC.md) documents mitigations for each adversary class in detail. +The [specification](https://github.com/ethsystems/pocs/tree/master/pocs/private-payment/plasma/SPEC.md) documents mitigations for each adversary class in detail. ## Limitations @@ -144,4 +146,4 @@ Private transfers are one layer of an institutional payment pipeline. Upcoming p On the proving layer, [PlasmaBlind](https://pse.dev/mastermap/ptr) is an emerging alternative that uses [folding-scheme-based IVC](https://sonobe.pse.dev/) rather than Plonky2's recursive SNARKs for the balance proof pipeline. Folding schemes reduce client-side proving costs, which could make generating balance proofs on mobile and browser clients more practical. It is under active R&D by PSE. -The implementation is [open source](https://github.com/ethereum/iptf-pocs/pull/19). The [specification](https://github.com/ethereum/iptf-pocs/tree/master/pocs/private-payment/plasma/SPEC.md) covers every protocol flow, data structure, and security consideration in detail. The [use case](https://github.com/ethereum/iptf-map/blob/master/use-cases/private-stablecoins.md) and [approach](https://github.com/ethereum/iptf-map/blob/master/approaches/approach-private-payments.md) documents on the IPTF Map provide additional context. Pull requests are welcome. +The implementation is [open source](https://github.com/ethsystems/pocs/pull/19). The [specification](https://github.com/ethsystems/pocs/tree/master/pocs/private-payment/plasma/SPEC.md) covers every protocol flow, data structure, and security consideration in detail. The [use case](https://github.com/ethsystems/map/blob/master/use-cases/private-stablecoins.md) and [approach](https://github.com/ethsystems/map/blob/master/approaches/approach-private-payments.md) documents on the IPTF Map provide additional context. Pull requests are welcome. diff --git a/src/posts/2026-03-05-private-crosschain-atomic-swap-part-1.md b/src/posts/2026-03-05-private-crosschain-atomic-swap-part-1.md index 13129e1..32faa0a 100644 --- a/src/posts/2026-03-05-private-crosschain-atomic-swap-part-1.md +++ b/src/posts/2026-03-05-private-crosschain-atomic-swap-part-1.md @@ -4,7 +4,7 @@ title: "Private Crosschain Atomic Swaps (Part 1 of 2)" description: "How to build atomic delivery-versus-payment across two chains while hiding amounts, prices, and counterparty identities. Part 1 covers the protocol: shielded UTXO notes, stealth addresses, and the coordination problem." date: 2026-03-06 10:00:00 +0100 author: "Yanis, Aaryamann" -image: /assets/images/2026-03-05-private-crosschain-swap-part-1/hero.png +image: ../assets/posts/2026-03-05-private-crosschain-swap-part-1/hero.png tags: - atomic-swap - crosschain @@ -13,6 +13,8 @@ tags: - proof-of-concept --- +*This post was written when IPTF (now EthSystems) was at the Ethereum Foundation* + Settlement risk has been around as long as trading itself. Two parties trade a bond for cash, and neither wants to go first. The buyer will not pay before receiving the asset; the seller will not hand over the asset before receiving payment. Traditional finance solved this decades ago with trusted intermediaries: custodians, depositories, clearing houses. The principle is [Delivery-versus-Payment](https://www.investopedia.com/terms/d/deliveryversuspayment.asp) (DvP) — both legs of the trade are conditioned on each other, so neither finalizes unless both do. Infrastructure like [DTCC](https://www.dtcc.com/) or [Euroclear](https://www.euroclear.com/) enforces this, holding both legs in escrow before releasing them together when conditions are met. In [repo markets](https://www.investopedia.com/terms/r/repurchaseagreement.asp), where one party sells securities and agrees to repurchase them (often overnight), the intermediary chain is longer. A bank holds collateral through a custodian, posts it to a triparty agent, which moves it to a counterparty's custodian. Every step needs a trusted party and a separate settlement instruction. The system works, but it is slow ([T+2](https://www.investor.gov/introduction-investing/investing-basics/glossary/settling-securities-transactions-t2), two business days after the trade, is standard), expensive, and depends on the integrity of every intermediary in the chain. @@ -55,7 +57,7 @@ Alice holds USD notes on Network 1. Bob holds bond notes on Network 2. They want The core note structure is the same as the single-chain protocol (commitment, nullifier, owner key, salt), with two additions — `fallbackOwner` and `timeout` — that we will come back to: -![Note fields are hashed into a commitment stored on-chain; spending reveals a nullifier that prevents double-spending](/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-3-note-structure.png) +![Note fields are hashed into a commitment stored on-chain; spending reveals a nullifier that prevents double-spending](../assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-3-note-structure.png) The `fallbackOwner` and `timeout` fields will make sense once we explain what can go wrong. For now, the question is how Alice and Bob claim each other's locked note. @@ -71,7 +73,7 @@ What we need is a way for Alice to lock a note that _only_ Bob can spend, withou ### Stealth addresses -[Stealth addresses](https://github.com/ethereum/iptf-map/blob/master/patterns/pattern-stealth-addresses.md) solve a simple problem: Bob has a public key known to everyone, and Alice wants to send him funds without anyone else being able to tell that Bob is the recipient. She uses Bob's public key to derive a fresh one-time address that only Bob can spend from, but that no observer can link back to him. +[Stealth addresses](https://github.com/ethsystems/map/blob/master/patterns/pattern-stealth-addresses.md) solve a simple problem: Bob has a public key known to everyone, and Alice wants to send him funds without anyone else being able to tell that Bob is the recipient. She uses Bob's public key to derive a fresh one-time address that only Bob can spend from, but that no observer can link back to him. Each participant has a long-lived meta key pair `(meta_sk, meta_pk)` that is published. To lock a note for a counterparty, the sender generates a fresh ephemeral key pair `(eph_sk, eph_pk)` (conventionally written `r, R = r·G`) and computes a shared secret via ECDH: @@ -89,7 +91,7 @@ stealth_sk = meta_sk + H("stealth", shared_secret) To claim, the counterparty needs two things: the ephemeral public key `eph_pk` and the salt used to construct the note commitment. With `eph_pk` and `meta_sk`, they derive `stealth_sk`. With the salt, they reconstruct the full note and generate a claim proof. -![Sender derives stealth_pk from ephemeral key and recipient's meta_pk; recipient recovers stealth_sk from published eph_pk and own meta_sk](/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-2-stealth-derivation.png) +![Sender derives stealth_pk from ephemeral key and recipient's meta_pk; recipient recovers stealth_sk from published eph_pk and own meta_sk](../assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-2-stealth-derivation.png) The construction is symmetric. Alice generates `(eph_sk_A, eph_pk_A)`, computes `stealth_pk_B` from Bob's meta-key, and locks her USD note with `owner = stealth_pk_B` on Network 1. Bob does the same on Network 2 for Alice. Neither can claim the other's note directly: Alice does not have `meta_sk_B`, Bob does not have `meta_sk_A`. @@ -109,7 +111,7 @@ Before solving coordination, there is a simpler question: what happens if coordi The protocol always terminates in one of two outcomes: both parties receive the other's asset, or both receive their own back. There is no stuck state, no capital locked indefinitely. -![If the timeout passes without settlement, each party reclaims their own funds using the fallbackOwner key embedded in the note](/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-5-timeout-refund.png) +![If the timeout passes without settlement, each party reclaims their own funds using the fallbackOwner key embedded in the note](../assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-5-timeout-refund.png) ## How the pieces fit @@ -140,11 +142,11 @@ The protocol hides amounts, asset types, counterparty identities, and the link b ZK circuits verify note formation and ownership. Shielded pools prevent double-spending. The coordinator makes revelation atomic across chains. -![Both parties lock notes to stealth addresses on their respective chains — the contract verifies each note via ZK proof — but the two locks are independent: what makes them atomic is the subject of Part 2](/assets/images/2026-03-05-private-crosschain-swap-part-1/diagram-4-locking-flow.png) +![Both parties lock notes to stealth addresses on their respective chains — the contract verifies each note via ZK proof — but the two locks are independent: what makes them atomic is the subject of Part 2](../assets/posts/2026-03-05-private-crosschain-swap-part-1/diagram-4-locking-flow.png) The coordinator is the only component not yet specified. It could be built from a Trusted Execution Environment, a multi-party computation protocol, or fully homomorphic encryption, each with different trust assumptions and performance trade-offs. In Part 2, we pick one: a TEE running in AWS Nitro Enclaves. We go inside the enclave, examine what attestation actually proves, work through the real attack surfaces, and walk through what the demo logs show. -The full implementation is open source, with a detailed [specification](https://github.com/ethereum/iptf-pocs/tree/main/pocs/approach-private-trade-settlement/tee_swap/SPEC.md) and an [interactive protocol walkthrough](/tee-protocol-page). +The full implementation is open source, with a detailed [specification](https://github.com/ethsystems/pocs/tree/main/pocs/approach-private-trade-settlement/tee_swap/SPEC.md) and an [interactive protocol walkthrough](/tee-protocol-page). ## References diff --git a/src/posts/2026-03-18-diy-validium.md b/src/posts/2026-03-18-diy-validium.md index 885e3f6..6264172 100644 --- a/src/posts/2026-03-18-diy-validium.md +++ b/src/posts/2026-03-18-diy-validium.md @@ -4,7 +4,7 @@ title: "DIY Validium: Private Logic on Public Rails" description: "A validium PoC where the business logic is ordinary Rust, proved in zero knowledge and verified on Ethereum." date: 2026-03-18 10:00:00 +0800 author: "Oskar" -image: /assets/images/2026-03-18-diy-validium/hero.png +image: ../assets/posts/2026-03-18-diy-validium/hero.png tags: - private-transfers - validium @@ -13,6 +13,8 @@ tags: - proof-of-concept --- +*This post was written when IPTF (now EthSystems) was at the Ethereum Foundation* + What does a private payment system look like when the business logic is ordinary Rust, proved in zero knowledge and verified on Ethereum? We built a [validium](https://ethereum.org/developers/docs/scaling/validium/) to find out. Here's a line of vanilla Rust code: @@ -33,7 +35,7 @@ The key component is the zkVM (zero-knowledge virtual machine). RISC Zero is one In earlier posts we explored [shielded pools](/building-private-transfers-on-ethereum/) (UTXO model, privacy from everyone) and [plasma](/private-stablecoins-with-plasma/) (client-side proving, self-sovereign exit). The validium sits at a different point in the design space: the operator sees everything, but the chain enforces correctness. That tradeoff is worth understanding. The operator is trusted for liveness and deposit crediting, but cannot forge state transitions. The trust section below makes the boundaries explicit. -![zkVM pattern: private inputs go in, only proof comes out](/assets/images/2026-03-18-diy-validium/zkvm-pattern.png) +![zkVM pattern: private inputs go in, only proof comes out](../assets/posts/2026-03-18-diy-validium/zkvm-pattern.png) ## Inside a guest program @@ -96,7 +98,7 @@ The guest program is the only thing that changes. The proving and verification i The disclosure proof proves something about an account inside a Merkle tree, maintained by an operator, anchored to Ethereum. Here's the architecture we ended up with: -![Three-layer validium architecture: operator, ZK layer, Ethereum](/assets/images/2026-03-18-diy-validium/architecture.png) +![Three-layer validium architecture: operator, ZK layer, Ethereum](../assets/posts/2026-03-18-diy-validium/architecture.png) The operator holds account state off-chain: a public key, a balance, and a random salt per account. This is the only place where plaintext balances exist. RISC Zero guest programs prove that state transitions follow the rules: the prover executes the program and produces a STARK proof. Raw STARKs are too large to verify on Ethereum directly, so RISC Zero wraps each one in a Groth16 SNARK via proof composition. The on-chain verifier checks the compact Groth16 proof (a few hundred bytes, affordable gas). Ethereum stores a single Merkle root and verifies these proofs. The contracts check that the old root matches, verify the proof seal, and update the root. @@ -121,7 +123,7 @@ Withdrawal is a single-leaf state transition. The bridge verifies the proof, upd Disclosure is the compliance proof detailed above. Read-only, no state mutation. -The full [specification](https://github.com/ethereum/iptf-pocs/tree/master/pocs/diy-validium/SPEC.md) covers each operation in detail. +The full [specification](https://github.com/ethsystems/pocs/tree/master/pocs/diy-validium/SPEC.md) covers each operation in detail. ## Trust, safety, and trade-offs @@ -131,7 +133,7 @@ The operator sees every balance, every transfer, every identity. They control wh The PoC implements three tiers of withdrawal. Each assumes less about operator cooperation. -![Censorship resistance spectrum: normal, forced, escape](/assets/images/2026-03-18-diy-validium/censorship-resistance.png) +![Censorship resistance spectrum: normal, forced, escape](../assets/posts/2026-03-18-diy-validium/censorship-resistance.png) Normal withdrawal is the default path. The operator provides the Merkle path, generates a proof, the bridge transfers tokens. @@ -139,7 +141,7 @@ If the operator refuses to process your withdrawal (censorship), you can submit If the operator disappears entirely (seven days of inactivity), anyone can freeze the bridge permanently. Once frozen, users recover funds by revealing their balance on-chain via a Merkle proof. No ZK proof needed, because there's no one left to hide from. Privacy gets sacrificed for fund recovery. This is the same escape hatch pattern that StarkEx and ZKSync use. -There's a real catch, though. To use the escape hatch, you need to have saved your current public key, balance, salt, leaf index, and Merkle sibling path. The salt changes on every state transition. Lose your current salt and you can't construct a valid commitment. For multi-device setups and institutional key management, this is not a trivial problem. The [SPEC](https://github.com/ethereum/iptf-pocs/tree/master/pocs/diy-validium/SPEC.md) describes layered DA extensions to reduce the burden: blob checkpoints (operator periodically posts Merkle snapshots to EIP-4844 blobs) and encrypted blobs (data encrypted to a DA committee, preserving privacy until escape is actually needed). +There's a real catch, though. To use the escape hatch, you need to have saved your current public key, balance, salt, leaf index, and Merkle sibling path. The salt changes on every state transition. Lose your current salt and you can't construct a valid commitment. For multi-device setups and institutional key management, this is not a trivial problem. The [SPEC](https://github.com/ethsystems/pocs/tree/master/pocs/diy-validium/SPEC.md) describes layered DA extensions to reduce the burden: blob checkpoints (operator periodically posts Merkle snapshots to EIP-4844 blobs) and encrypted blobs (data encrypted to a DA committee, preserving privacy until escape is actually needed). ### The trust model @@ -182,7 +184,7 @@ On the engineering side: the operator is centralized (production would use a DA A few questions we keep returning to: -**Private shared state.** This validium gives one operator full visibility. But what about two institutions that want to share a ledger (bilateral netting positions, a shared collateral pool) without either seeing the other's full book? That's the private shared state problem. TACEO's Merces uses MPC with co-SNARKs: parties secret-share their inputs and jointly produce a ZK proof without any single node seeing plaintext. FHE-based approaches compute over encrypted data directly. TEE-based approaches (like the [Nitro enclave work](/private-crosschain-atomic-swap-part-2/) we explored earlier) use hardware isolation. Each makes different bets on simplicity, throughput, and trust. The [IPTF map](https://github.com/ethereum/iptf-map) breaks down these patterns in detail. +**Private shared state.** This validium gives one operator full visibility. But what about two institutions that want to share a ledger (bilateral netting positions, a shared collateral pool) without either seeing the other's full book? That's the private shared state problem. TACEO's Merces uses MPC with co-SNARKs: parties secret-share their inputs and jointly produce a ZK proof without any single node seeing plaintext. FHE-based approaches compute over encrypted data directly. TEE-based approaches (like the [Nitro enclave work](/private-crosschain-atomic-swap-part-2/) we explored earlier) use hardware isolation. Each makes different bets on simplicity, throughput, and trust. The [IPTF map](https://github.com/ethsystems/map) breaks down these patterns in detail. **Cross-validium transfers.** Moving funds between validiums currently requires a public withdraw-then-deposit cycle, which links the two operations on-chain. Private atomic bridges are unsolved. The fundamental tension is between privacy, atomicity, and latency, and it's not clear you can have all three. @@ -190,4 +192,4 @@ A few questions we keep returning to: The `assert!` line in the guest program is the whole point. The guest program is where business logic lives. Everything else, the Merkle tree, the bridge contract, the escape hatch, exists to make that one line trustworthy on a public chain. -The full implementation is [open source](https://github.com/ethereum/iptf-pocs/tree/master/pocs/diy-validium), with a detailed [specification](https://github.com/ethereum/iptf-pocs/tree/master/pocs/diy-validium/SPEC.md) and [formal requirements](https://github.com/ethereum/iptf-pocs/tree/master/pocs/diy-validium/REQUIREMENTS.md). For production validium infrastructure, ZKSync's Prividium provides this architecture with production DA and sequencing. The code is open and we'd welcome feedback. +The full implementation is [open source](https://github.com/ethsystems/pocs/tree/master/pocs/diy-validium), with a detailed [specification](https://github.com/ethsystems/pocs/tree/master/pocs/diy-validium/SPEC.md) and [formal requirements](https://github.com/ethsystems/pocs/tree/master/pocs/diy-validium/REQUIREMENTS.md). For production validium infrastructure, ZKSync's Prividium provides this architecture with production DA and sequencing. The code is open and we'd welcome feedback. diff --git a/src/posts/2026-03-18-private-crosschain-atomic-swap-part-2.md b/src/posts/2026-03-18-private-crosschain-atomic-swap-part-2.md index 91876a0..5d07fd3 100644 --- a/src/posts/2026-03-18-private-crosschain-atomic-swap-part-2.md +++ b/src/posts/2026-03-18-private-crosschain-atomic-swap-part-2.md @@ -4,7 +4,7 @@ title: "Private Crosschain Atomic Swaps (Part 2 of 2)" description: "How a Trusted Execution Environment (TEE) can coordinate private crosschain atomic swaps today, what the real attack surfaces are, and why TEEs are a practical bridge to stronger cryptographic solutions." date: 2026-03-13 10:00:00 +0100 author: "Yanis" -image: /assets/images/2026-03-05-private-crosschain-swap-part-1/hero.png +image: ../assets/posts/2026-03-05-private-crosschain-swap-part-1/hero.png tags: - atomic-swap - crosschain @@ -13,6 +13,8 @@ tags: - proof-of-concept --- +*This post was written when IPTF (now EthSystems) was at the Ethereum Foundation* + In [Part 1](/private-crosschain-atomic-swap-part-1/), we built a protocol for private crosschain settlement. Shielded UTXO notes on two chains hide amounts and asset types. Stealth addresses let each party lock a note that only the counterparty can claim, without revealing who that counterparty is on-chain. A fallback timeout guarantees that if anything goes wrong, both parties reclaim their own funds. The remaining problem is coordination. Each party holds a secret (an ephemeral key and an encrypted salt) that the other needs to claim. Revealing these secrets must happen simultaneously, otherwise whoever goes first can be cheated, whoever goes second can defect. We left the coordinator as a black box. This post opens it. @@ -48,7 +50,7 @@ For institution-to-institution bilateral settlement, both parties know each othe Before submitting anything to the coordinator, each party needs assurance that the code running inside the enclave is exactly the open-source coordinator, unmodified and unobserved by the operator. This is the job of remote attestation. -![Remote attestation flow](/assets/images/2026-03-18-private-crosschain-swap-part-2/diagram-1-attestation-flow.png) +![Remote attestation flow](../assets/posts/2026-03-18-private-crosschain-swap-part-2/diagram-1-attestation-flow.png) The build process is deterministic. The coordinator binary, its configuration, and its dependencies are packaged into an image. A build tool hashes everything in that image into a set of measurements (fingerprints of the code, configuration, and boot chain). These measurements are public: anyone can rebuild the image from source and verify they get the same hash. @@ -60,7 +62,7 @@ When a client connects, the TLS handshake presents this certificate. The client The coordinator's job is narrow: receive submissions from both parties, verify that their locked notes match the agreed swap terms, and publish the claim secrets atomically. -![TEE coordinator architecture](/assets/images/2026-03-18-private-crosschain-swap-part-2/diagram-2-tee-coordinator.png) +![TEE coordinator architecture](../assets/posts/2026-03-18-private-crosschain-swap-part-2/diagram-2-tee-coordinator.png) ### What the coordinator receives @@ -145,7 +147,7 @@ The TEE coordinator is a starting point, not the destination. MPC could replace The coordination problem reduces to this: two parties each hold private inputs (their ephemeral key and encrypted salt), and we need a single proof that both sets of inputs are consistent with the on-chain state. That is what co-SNARKs solve — each party contributes their secret inputs to a joint ZK proof without revealing them to anyone. The proof itself becomes the atomic revelation. If it verifies, both sides are consistent. No trusted intermediary, no hardware assumption, no coordinator to compromise. The coordinator becomes a protocol rather than a party. -The full implementation is open source, with a detailed [specification](https://github.com/ethereum/iptf-pocs/tree/main/pocs/approach-private-trade-settlement/tee_swap/SPEC.md) and an [interactive protocol walkthrough](/tee-protocol-page). +The full implementation is open source, with a detailed [specification](https://github.com/ethsystems/pocs/tree/main/pocs/approach-private-trade-settlement/tee_swap/SPEC.md) and an [interactive protocol walkthrough](/tee-protocol-page). ## References diff --git a/src/posts/2026-04-14-resilient-plural-identity.md b/src/posts/2026-04-14-resilient-plural-identity.md index 59a2ed2..52f9bc2 100644 --- a/src/posts/2026-04-14-resilient-plural-identity.md +++ b/src/posts/2026-04-14-resilient-plural-identity.md @@ -4,7 +4,7 @@ title: "Resilient Plural Identity" description: "Designing identity on Ethereum that survives issuer failure: plural attestation sources, vOPRF sybil resistance, and an on-chain trust anchor that no single party can revoke." date: 2026-04-22 10:00:00 +0100 author: "Aaryamann, Oskar" -image: /assets/images/2026-04-14-resilient-private-identity/hero.png +image: ../assets/posts/2026-04-14-resilient-private-identity/hero.png tags: - identity - plural-identity @@ -14,6 +14,8 @@ tags: - proof-of-concept --- +*This post was written when IPTF (now EthSystems) was at the Ethereum Foundation* + *This post opens our three-part resilience series on identity, payments, and coordination. Where previous IPTF writeups started from an institutional requirement and designed forward, these start from a failure mode (a sanctioned jurisdiction, a collapsed issuer, an internet shutdown) and work back to what Ethereum can offer. Expect the voice to shift accordingly.* Self-sovereign identity is a user having the final say over who they are online, and over the status they carry into any system they join. Most coordination problems we care about, whether voting in a DAO, posting in a community forum, claiming aid from an NGO, or settling a regulated transaction, eventually need to answer a question about the person on the other side: are they real, are they unique, do they meet some criterion. Today that question is almost always answered by a single outside authority. A government issues a passport. A platform issues an account. A compliance vendor maintains a list. Identity is where self-sovereignty meets its hardest test, because dependence on that authority is the weak link. @@ -24,7 +26,7 @@ When that authority shuts down, gets sanctioned, or turns adversarial, already-v We built a proof-of-concept that removes this dependency. After a one-time enrollment, an on-chain Merkle root on Ethereum, censorship-resistant and always available, becomes the sole trust anchor. The identity provider can go offline, revoke everything, or turn adversarial. Holders keep proving attributes. New enrollees join through any accepted identity source, not just the original provider. Plurality is the default: no single issuer holds a monopoly over who gets to participate. -The implementation is [open source](https://github.com/ethereum/iptf-pocs/tree/master/pocs/private-identity/resilient-private-identity), with a detailed [specification](https://github.com/ethereum/iptf-pocs/tree/master/pocs/private-identity/resilient-private-identity/SPEC.md). +The implementation is [open source](https://github.com/ethsystems/pocs/tree/master/pocs/private-identity/resilient-private-identity), with a detailed [specification](https://github.com/ethsystems/pocs/tree/master/pocs/private-identity/resilient-private-identity/SPEC.md). ## How identity verification works today @@ -38,7 +40,7 @@ Every step depends on the issuer being live and cooperative. [ZKPassport](https: The issuer can fail in several ways. It can shut down (bankruptcy, sanctions, corporate dissolution). It can turn adversarial (mass-revoke credentials, publish false revocation lists, forge credentials for non-holders, refuse new issuance). It can simply go offline. In every case, the holder is left with a credential that cannot be independently verified. -![Traditional identity systems require the issuer to be online for every revocation check; resilient private identity replaces the issuer with an on-chain Merkle root as the sole trust anchor](/assets/images/2026-04-14-resilient-private-identity/resilient_identity_comparison.png) +![Traditional identity systems require the issuer to be online for every revocation check; resilient private identity replaces the issuer with an on-chain Merkle root as the sole trust anchor](../assets/posts/2026-04-14-resilient-private-identity/resilient_identity_comparison.png) ## Breaking the issuer dependency @@ -58,7 +60,7 @@ In this view, sybil resistance is not bolted on top of identity. It is the prope Enrollment is a single on-chain transaction. The work before that transaction is what makes the protocol resilient. The holder proves identity ownership using an existing source (passport, national ID, email, web2 account), then obtains a deterministic sybil-resistant tag from a [vOPRF](https://www.rfc-editor.org/rfc/rfc9497) (verifiable Oblivious Pseudorandom Function) network: a cryptographic protocol that maps an input to a deterministic but unpredictable output using a secret key, without the key holder learning the input or the requester learning the key. The tag ensures one real-world identity maps to exactly one on-chain leaf, regardless of when enrollment happens. The holder generates a ZK proof binding everything together and submits it in a single transaction. -![Enrollment flow: the holder obtains identity evidence, blinds it, sends it to the vOPRF MPC network for evaluation with a link proof, then submits the enrollment proof and leaf to the on-chain contract](/assets/images/2026-04-14-resilient-private-identity/resilient_identity_flow.png) +![Enrollment flow: the holder obtains identity evidence, blinds it, sends it to the vOPRF MPC network for evaluation with a link proof, then submits the enrollment proof and leaf to the on-chain contract](../assets/posts/2026-04-14-resilient-private-identity/resilient_identity_flow.png) After this transaction, the holder stores their secret and attributes locally. The issuer is no longer involved. Ethereum itself becomes the trust anchor: a censorship-resistant, permissionless ledger that no single party can take offline or tamper with. The smart contract with its ZK verifier is the resilient issuer. @@ -70,7 +72,7 @@ The holder generates a ZK membership proof demonstrating three things: they have The nullifier is deterministic per holder and per verifier scope: the same holder always produces the same nullifier for the same verifier, preventing duplicate presentations within a single application. Across different verifiers, the nullifiers are unrelated. An observer who sees a proof submitted to Verifier A and another to Verifier B cannot determine whether the same holder produced both. -![Verification flow: the verifier requests an attribute proof, the holder generates a ZK membership proof, and the on-chain IdentityVerifier checks root freshness, nullifier uniqueness, and proof validity](/assets/images/2026-04-14-resilient-private-identity/resilient_identity_verification_flow.png) +![Verification flow: the verifier requests an attribute proof, the holder generates a ZK membership proof, and the on-chain IdentityVerifier checks root freshness, nullifier uniqueness, and proof validity](../assets/posts/2026-04-14-resilient-private-identity/resilient_identity_verification_flow.png) The verifier calls `IdentityVerifier.verifyProof(...)`. The contract checks root freshness (last 1000 roots in a circular buffer), nullifier uniqueness, and proof validity. If the call does not revert, the proof is valid. @@ -96,7 +98,7 @@ The protocol layers three independent factors: When sources are honest, the cryptographic layer alone enforces one-to-one binding. When sources are compromised, the economic layer kicks in: N sybil leaves require N * 0.1 ETH locked. The stake is a refundable bond. Holders reclaim it by unstaking, which removes their leaf from the tree. -The social layer (specified in the [README](https://github.com/ethereum/iptf-pocs/tree/master/pocs/private-identity/resilient-private-identity#future-work-web-of-trust)) adds a third constraint: each existing member has a lifetime vouch budget of V=2, and new enrollees need K=3 vouches from existing members. Vouches are aggregated into a single recursive proof off-chain and submitted atomically with the enrollment transaction. No vouch graph is ever visible on-chain. An attacker with T fake identities creates at most 2T additional sybils. Growth is linear, not exponential. +The social layer (specified in the [README](https://github.com/ethsystems/pocs/tree/master/pocs/private-identity/resilient-private-identity#future-work-web-of-trust)) adds a third constraint: each existing member has a lifetime vouch budget of V=2, and new enrollees need K=3 vouches from existing members. Vouches are aggregated into a single recursive proof off-chain and submitted atomically with the enrollment transaction. No vouch graph is ever visible on-chain. An attacker with T fake identities creates at most 2T additional sybils. Growth is linear, not exponential. These three factors match the plural-identity cost structure from earlier: one identity is cheap, ten cost ten times as much, a million are priced out of reach. The cryptographic factor binds each identity to a real credential. The economic factor prices the right to hold multiples. @@ -183,4 +185,4 @@ The [privacy-ethereum/zkspecs](https://github.com/privacy-ethereum/zkspecs) repo The immediate extensions are multi-source identity integration (recursive verification of existing identity proof systems inside Noir, so attributes are cryptographically verified rather than self-declared and sybil resistance works across identity sources), web-of-trust vouching as a third sybil factor, and epoch-based key rotation for forward secrecy. -The [specification](https://github.com/ethereum/iptf-pocs/tree/master/pocs/private-identity/resilient-private-identity/SPEC.md) covers every circuit constraint, data structure, and security consideration. The [use case](https://github.com/ethereum/iptf-map/blob/master/use-cases/resilient-identity-continuity.md) and [approach](https://github.com/ethereum/iptf-map/blob/master/approaches/approach-private-identity.md) documents on the IPTF Map show how this fits into the broader institutional privacy work. Pull requests are welcome. +The [specification](https://github.com/ethsystems/pocs/tree/master/pocs/private-identity/resilient-private-identity/SPEC.md) covers every circuit constraint, data structure, and security consideration. The [use case](https://github.com/ethsystems/map/blob/master/use-cases/resilient-identity-continuity.md) and [approach](https://github.com/ethsystems/map/blob/master/approaches/approach-private-identity.md) documents on the IPTF Map show how this fits into the broader institutional privacy work. Pull requests are welcome. diff --git a/src/posts/2026-05-14-resilient-disbursement-rails.md b/src/posts/2026-05-14-resilient-disbursement-rails.md index a4e7104..fef8cf1 100644 --- a/src/posts/2026-05-14-resilient-disbursement-rails.md +++ b/src/posts/2026-05-14-resilient-disbursement-rails.md @@ -4,7 +4,7 @@ title: "Resilient Disbursement Rails" description: "Aid payments on Ethereum that protect recipients even when local partners are compromised, or when recipients cash out into local currency." date: 2026-05-14 15:00:00 +0200 author: "Aaryamann" -image: /assets/images/2026-05-14-resilient-disbursement-rails/hero.png +image: ../assets/posts/2026-05-14-resilient-disbursement-rails/hero.png tags: - disbursement - shielded-pool @@ -15,6 +15,8 @@ tags: - proof-of-concept --- +*This post was written when IPTF (now EthSystems) was at the Ethereum Foundation* + *This is the second post in our three-part resilience series, after [Resilient Plural Identity](/resilient-plural-identity/). The identity post asked how a person can keep proving who they are when the authority that vouched for them disappears or turns hostile. This one asks how money can reach that person when the local partner handing out the funds can be forced to share its records, and when the exchange where they cash out into local currency records their identity.* Humanitarian payments create lists. Wherever the rail runs, some party keeps a record. That record is one subpoena or one regime change away from being evidence against a specific recipient. The best beneficiary database is the one you never create. @@ -27,7 +29,7 @@ We have run into versions of this in our own work. The [shielded pool](/building The constraint set is sharper when recipients live somewhere the authorities are hostile to the funder, the implementing partner, or the recipients themselves. Recipients cannot be assumed to have a phone or an internet connection. Implementing partners may get breached, coerced, or inherited by successor regimes. The protocol assumes every party in the path between funder and recipient will eventually be compromised. -The implementation is [open source](https://github.com/ethereum/iptf-pocs/tree/master/pocs/private-payment/resilient-disbursement-rails), with a detailed [specification](https://github.com/ethereum/iptf-pocs/blob/master/pocs/private-payment/resilient-disbursement-rails/SPEC.md). +The implementation is [open source](https://github.com/ethsystems/pocs/tree/master/pocs/private-payment/resilient-disbursement-rails), with a detailed [specification](https://github.com/ethsystems/pocs/blob/master/pocs/private-payment/resilient-disbursement-rails/SPEC.md). ## How aid disbursement breaks today @@ -45,7 +47,7 @@ The recipient constraint set is the hardest part. Recipients hold tamper-resista ## What we built -![The funder shields per-recipient amounts atomically; recipients claim through smartcard-signed vouchers relayed through mesh and anonymous transports; the off-ramp is the trust boundary](/assets/images/2026-05-14-resilient-disbursement-rails/what_we_built.png) +![The funder shields per-recipient amounts atomically; recipients claim through smartcard-signed vouchers relayed through mesh and anonymous transports; the off-ramp is the trust boundary](../assets/posts/2026-05-14-resilient-disbursement-rails/what_we_built.png) A round runs in four stages. The funder publishes the round and shields the full disbursement in one atomic transaction. Recipients sign an offline voucher on a smartcard, under a stealth key derived on-card. In the production path, a relay turns the voucher into two zero-knowledge proofs and submits through Tor, Nym, or HOPR; the PoC keeps those as explicit transport boundaries and uses direct in-process adapters. The claim contract verifies, unshields to a one-time stealth destination, and burns the nullifier. After the round closes plus a 30-day timelock, the funder multisig can sweep any residual via balance accounting; there is no zero-knowledge proof on the residual path. @@ -53,13 +55,13 @@ Two things are out of scope. Cross-funder anonymity, because each claim contract ## How a round publishes -![Round publication is atomic. The factory verifies cohort identity against the Registry, deposits one Poseidon commitment per active card into the claim contract's sub-tree, and registers the header](/assets/images/2026-05-14-resilient-disbursement-rails/how_a_round_publishes.png) +![Round publication is atomic. The factory verifies cohort identity against the Registry, deposits one Poseidon commitment per active card into the claim contract's sub-tree, and registers the header](../assets/posts/2026-05-14-resilient-disbursement-rails/how_a_round_publishes.png) Round publication is the only on-chain event in which the funder appears, and it is one atomic transaction. The factory pulls the funder's tokens, deposits one commitment per active card into the claim contract's pool sub-tree, and registers the signed header. A revert at any step reverts the whole thing. ## How recipients claim -![The smartcard derives the per-claim stealth public key on-card via HMAC, constructs the 308-byte preimage internally, and signs with secp256k1 ECDSA.](/assets/images/2026-05-14-resilient-disbursement-rails/how_recipients_claim.png) +![The smartcard derives the per-claim stealth public key on-card via HMAC, constructs the 308-byte preimage internally, and signs with secp256k1 ECDSA.](../assets/posts/2026-05-14-resilient-disbursement-rails/how_recipients_claim.png) The recipient inserts the smartcard into a companion device. The companion verifies the funder's signature on the round header (the only point in the protocol where that signature is checked) and passes the voucher context to the card. The card derives a per-claim stealth keypair from the master secret via HMAC-SHA256, builds the voucher preimage internally, and signs it with secp256k1 ECDSA. The card must construct the preimage internally rather than accept a pre-hashed digest, otherwise a malicious companion could substitute a different derived public key and have the card sign over the lie. @@ -69,7 +71,7 @@ This split is what makes Registry compromise survivable. The nullifier folds the ## How relays close the loop -![The relay decrypts the voucher, generates the claim and pool-withdraw proofs, and submits through Tor, Nym, or HOPR. The claim contract verifies both proofs, enforces cross-proof binding via the shared nullifier, and unshields to the stealth destination.](/assets/images/2026-05-14-resilient-disbursement-rails/how_relays_close_the_loop.png) +![The relay decrypts the voucher, generates the claim and pool-withdraw proofs, and submits through Tor, Nym, or HOPR. The claim contract verifies both proofs, enforces cross-proof binding via the shared nullifier, and unshields to the stealth destination.](../assets/posts/2026-05-14-resilient-disbursement-rails/how_relays_close_the_loop.png) The companion hands the encrypted voucher to mesh transport (Briar over Bluetooth LE plus Tor, Meshtastic over LoRa, Reticulum over LoRa or packet radio, or any conforming adapter). A relay decrypts, generates the two zero-knowledge proofs, and submits the claim transaction through Tor, Nym, or HOPR. Relays sign with rotating EOAs to bound cross-round linkability. @@ -162,4 +164,4 @@ There also exist multiple large-scale humanitarian cash-and-voucher programs tha Three threads we want to pull on. Forward-secure signature chains on-card, to retire the long-lived master secret and bound past-claim disclosure under card seizure. Cross-funder anonymity, by lifting the per-claim-contract sub-tree partition into a shared association set with a compatible compliance witness. Batch-withdraw circuits, to amortize verification gas across multiple claims per transaction. -The [specification](https://github.com/ethereum/iptf-pocs/blob/master/pocs/private-payment/resilient-disbursement-rails/SPEC.md) has the full circuit constraints, data structures, and security considerations. The IPTF Map [use case](https://github.com/ethereum/iptf-map/blob/master/use-cases/resilient-disbursement-rails.md) and [approach](https://github.com/ethereum/iptf-map/blob/master/approaches/approach-private-payments.md) documents place this in the broader institutional-privacy work. Pull requests are welcome. +The [specification](https://github.com/ethsystems/pocs/blob/master/pocs/private-payment/resilient-disbursement-rails/SPEC.md) has the full circuit constraints, data structures, and security considerations. The IPTF Map [use case](https://github.com/ethsystems/map/blob/master/use-cases/resilient-disbursement-rails.md) and [approach](https://github.com/ethsystems/map/blob/master/approaches/approach-private-payments.md) documents place this in the broader institutional-privacy work. Pull requests are welcome. diff --git a/src/posts/2026-05-28-resilient-civic-participation.md b/src/posts/2026-05-28-resilient-civic-participation.md index 12eccfa..2f7d401 100644 --- a/src/posts/2026-05-28-resilient-civic-participation.md +++ b/src/posts/2026-05-28-resilient-civic-participation.md @@ -4,7 +4,7 @@ title: "Resilient Civic Participation" description: "Petitions on Ethereum where the signer list never exists and the outcome stays verifiable from chain state alone." date: 2026-05-29 15:00:00 +0200 author: "Aaryamann" -image: /assets/images/2026-05-28-resilient-civic-participation/hero.png +image: ../assets/posts/2026-05-28-resilient-civic-participation/hero.png tags: - civic-participation - petition @@ -15,13 +15,15 @@ tags: - proof-of-concept --- +*This post was written when IPTF (now EthSystems) was at the Ethereum Foundation* + *This is the third and final post in the resilience series, after [Resilient Plural Identity](/resilient-plural-identity/) and [Resilient Disbursement Rails](/resilient-disbursement-rails/). The identity post asked how a person can keep proving who they are when the authority that vouched for them disappears. The disbursement post asked how money can reach that person when the rail handing it out can be subpoenaed. This post asks the third question in the same shape: how can a person co-sign a public decision (a petition, an initiative, a citizen-led inquiry) without ending up on a list that a future regime can use against them.* Petitions create signed lists. Whoever holds that list gets the leverage to make signing dangerous. The best petition system is the one that never produces a list at all; what survives the round is a count. The protocol described here removes the list. Signers prove eligibility against an external credential layer, sign at most once per petition, and submit through a relayer that aggregates many signatures into a single batched proof on Ethereum. The on-chain artifacts are per-class counts and a Boolean outcome, verifiable from L1 as long as the [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) blob payloads remain reachable through consensus retention or, past that window, a voluntary archive. A future government, a successor regime, or a coerced platform operator can demand the list, but the protocol does not produce one to hand over. -The implementation is [open source](https://github.com/ethereum/iptf-pocs/tree/master/pocs/civic-participation/resilient-civic-participation), with a detailed [specification](https://github.com/ethereum/iptf-pocs/blob/master/pocs/civic-participation/resilient-civic-participation/SPEC.md). Sybil resistance is delegated to the [ResilientIdentity](/resilient-plural-identity/) credential layer from the first post in this series. +The implementation is [open source](https://github.com/ethsystems/pocs/tree/master/pocs/civic-participation/resilient-civic-participation), with a detailed [specification](https://github.com/ethsystems/pocs/blob/master/pocs/civic-participation/resilient-civic-participation/SPEC.md). Sybil resistance is delegated to the [ResilientIdentity](/resilient-plural-identity/) credential layer from the first post in this series. ## How petition signing breaks today @@ -33,7 +35,7 @@ Signers in adversarial jurisdictions face additional constraints: intermittent n ## What we built -![Organizer registers and funds a petition against the ResilientIdentity root R; signers build SNARKs that relayers aggregate into batches against the petition registry; disputants can repudiate active batches and a resolver closes the round.](/assets/images/2026-05-28-resilient-civic-participation/what_we_built.png) +![Organizer registers and funds a petition against the ResilientIdentity root R; signers build SNARKs that relayers aggregate into batches against the petition registry; disputants can repudiate active batches and a resolver closes the round.](../assets/posts/2026-05-28-resilient-civic-participation/what_we_built.png) A petition round runs in five stages. The organiser registers a petition under an existing ResilientIdentity (RI) Merkle root and escrows a bounty in an ERC-20 token. Eligible signers, each holding an RI credential, build a signer zero-knowledge proof against the registered petition and submit it through any reachable relayer. The relayer aggregates many signer proofs into a single batch proof that recursively verifies every inner proof, and publishes the batch as an EIP-4844 blob transaction. A two-week dispute window opens; anyone can repudiate a batch by submitting KZG openings that prove a specific record violates the batch rules. After the dispute window closes, anyone can compute the outcome from the blob payloads and the on-chain Indexed Merkle Tree roots, submit a resolution proof, and claim the bounty. @@ -47,7 +49,7 @@ In every documented case where a signer list became evidence, the evidence was s ## How a petition publishes -![Registration is one atomic transaction: the registry validates the class-bound predicate, asserts the bounty floor, derives petition_id, advances the global slot counter, escrows the bounty, and emits PetitionRegistered.](/assets/images/2026-05-28-resilient-civic-participation/how_a_petition_publishes.png) +![Registration is one atomic transaction: the registry validates the class-bound predicate, asserts the bounty floor, derives petition_id, advances the global slot counter, escrows the bounty, and emits PetitionRegistered.](../assets/posts/2026-05-28-resilient-civic-participation/how_a_petition_publishes.png) Registration is a single atomic transaction. The organiser supplies a predicate over signer attributes (for example, "is a citizen of one of these EU member states"), a class set (the per-jurisdiction buckets the threshold tally counts over), a per-class threshold (the minimum signatures required in each bucket), and a close-at-block deadline. The bounty must clear a calibration floor that scales with the threshold sum and the predicate's operation count, so a resolver always has economic incentive to produce the outcome proof. @@ -55,7 +57,7 @@ The registry rejects predicates that are not *class-bound*, meaning predicates w ## How signing works -![The signer advances the local FSRT to the petition's slot, derives the nullifier and identity tag, sends a signer SNARK to a relayer, and zeroises the slot's seed material once the petition's dispute window closes.](/assets/images/2026-05-28-resilient-civic-participation/how_signing_works.png) +![The signer advances the local FSRT to the petition's slot, derives the nullifier and identity tag, sends a signer SNARK to a relayer, and zeroises the slot's seed material once the petition's dispute window closes.](../assets/posts/2026-05-28-resilient-civic-participation/how_signing_works.png) A signer reads the petition's metadata from the registry and advances their local FSRT to the petition's slot. The advance is monotone, so once a signer passes slot k the local state cannot regress back to k without re-enrolment; that is what enforces "at most one signature per signer per petition" even under partial device compromise. The signer derives two values from the slot. A *nullifier* is a one-time tag that lets the chain reject a second signature without revealing who signed; it hashes the slot value, petition identifier, class index, class tag, and identity secret, and the registry deduplicates on it. An *identity tag* hashes the slot value and petition identifier, and the batch SNARK uses it to reject any two records in the same batch that share a signer. @@ -65,13 +67,13 @@ The signer SNARK's public inputs commit to the petition identifier, class index, The relayer collects up to a configured maximum of signer submissions per petition, sorts them by the canonical leaf hash, and recursively verifies all inner proofs inside a single outer batch proof, which keeps on-chain verifier cost sub-linear in batch size. The batch proof commits to the prior and new Indexed Merkle Tree (IMT) roots, to the prior and new leaf counts, and to the field-element decomposition of the blob payload that the registry uses to verify on-chain that the batch and the blob agree byte-for-byte. The IMT shape gives cheap non-membership proofs, which the intra-batch-duplicate-identity-tag dispute path needs to verify cheaply on L1. -![The relayer recursively verifies N signer proofs into one batch SNARK and publishes the records as an EIP-4844 blob; the registry verifies the SNARK against its prior state and binds the batch to the blob via the 0x0a KZG precompile before advancing roots and leaf count.](/assets/images/2026-05-28-resilient-civic-participation/how_batches_publish.png) +![The relayer recursively verifies N signer proofs into one batch SNARK and publishes the records as an EIP-4844 blob; the registry verifies the SNARK against its prior state and binds the batch to the blob via the 0x0a KZG precompile before advancing roots and leaf count.](../assets/posts/2026-05-28-resilient-civic-participation/how_batches_publish.png) One subtle piece is the cross-field binding. The batch SNARK operates over BN254 (the curve Ethereum's pairing precompiles support); EIP-4844 blob payloads live in BLS12-381. The batch SNARK exposes the per-record BLS12-381 field-element decomposition as public inputs, and the registry calls the KZG point-evaluation precompile at address `0x0a` for each one against the blob's versioned hash (a short on-chain commitment that identifies the blob). The two sides together close the binding without any of the SNARK doing BLS12-381 verification in-circuit. A relayer that publishes a blob disagreeing with what the batch SNARK committed to fails the on-chain KZG check before the batch enters state. A relayer that publishes a blob agreeing with the batch SNARK but containing a record that violates petition rules can still be repudiated during the dispute window. ## How disputes work -![A disputant repudiates a batch by submitting KZG openings that prove a record violates the batch rules; the registry verifies the openings against the batch's versioned hash and either rolls back the batch and every later active batch, or reverts ViolationFalse.](/assets/images/2026-05-28-resilient-civic-participation/how_disputes_work.png) +![A disputant repudiates a batch by submitting KZG openings that prove a record violates the batch rules; the registry verifies the openings against the batch's versioned hash and either rolls back the batch and every later active batch, or reverts ViolationFalse.](../assets/posts/2026-05-28-resilient-civic-participation/how_disputes_work.png) A disputant repudiates a batch by producing one of three enumerated violations against blob records: a class tag outside the petition's class set, two records in the same batch sharing an identity tag, or two adjacent records out of canonical leaf order. Each is also enforced by the batch SNARK; the dispute window is the defense-in-depth backstop against circuit bugs or proof-system unsoundness. A successful dispute repudiates the offending batch and every later active batch, because later batches' prior-state bindings are no longer canonical. In production, the cascade requires the relayer ecosystem to be diverse enough that a successor relayer can rebuild from the repudiation point. A single-relayer deployment has no liveness if the only relayer is the malicious one. @@ -179,6 +181,6 @@ The pieces compose. A holder enrolled in [Resilient Plural Identity](/resilient- ## Working with us -If you are running a humanitarian organisation, a govtech platform, a civil-society campaign, an NGO, or a regulator with a use case where the existence of a list is itself the risk, we want to hear from you. The three protocols described in this series cover identity continuity, private disbursement, and credentialed petition signing; the wider [IPTF Map](https://github.com/ethereum/iptf-map) catalogues a broader set of primitives that Ethereum can support. Reach out by [opening an issue](https://github.com/ethereum/iptf-pocs/issues) on the PoC repository, or via the contact information on the [IPTF home page](https://iptf.ethereum.org/). +If you are running a humanitarian organisation, a govtech platform, a civil-society campaign, an NGO, or a regulator with a use case where the existence of a list is itself the risk, we want to hear from you. The three protocols described in this series cover identity continuity, private disbursement, and credentialed petition signing; the wider [IPTF Map](https://github.com/ethsystems/map) catalogues a broader set of primitives that Ethereum can support. Reach out by [opening an issue](https://github.com/ethsystems/pocs/issues) on the PoC repository, or via the contact information on the [IPTF home page](https://ethsystems.org/). -The [specification](https://github.com/ethereum/iptf-pocs/blob/master/pocs/civic-participation/resilient-civic-participation/SPEC.md) has the full circuit constraints, contract semantics, and security considerations. The IPTF Map [use case](https://github.com/ethereum/iptf-map/blob/master/use-cases/resilient-civic-participation.md) and [approach](https://github.com/ethereum/iptf-map/blob/master/approaches/approach-civic-participation.md) documents place this in the broader institutional-privacy work. +The [specification](https://github.com/ethsystems/pocs/blob/master/pocs/civic-participation/resilient-civic-participation/SPEC.md) has the full circuit constraints, contract semantics, and security considerations. The IPTF Map [use case](https://github.com/ethsystems/map/blob/master/use-cases/resilient-civic-participation.md) and [approach](https://github.com/ethsystems/map/blob/master/approaches/approach-civic-participation.md) documents place this in the broader institutional-privacy work. diff --git a/src/posts/2026-06-11-next-iteration-shielded-pools.md b/src/posts/2026-06-11-next-iteration-shielded-pools.md index 38e3929..62a218e 100644 --- a/src/posts/2026-06-11-next-iteration-shielded-pools.md +++ b/src/posts/2026-06-11-next-iteration-shielded-pools.md @@ -3,9 +3,11 @@ title: "Exploring Hardened Shielded Pools" description: "Extending a working shielded pool with epoch nullifiers and PIR, and why private selection is the harder problem left for private payments at scale." date: 2026-06-11T07:00:00-04:00 author: "Yanis" -image: /assets/images/2026-06-11-next-iteration-shielded-pools/hero.png +image: ../assets/posts/2026-06-11-next-iteration-shielded-pools/hero.png --- +*This post was written when IPTF (now EthSystems) was at the Ethereum Foundation* + Shielded pools are one way to make a private payment on a public blockchain. Two earlier posts on this blog built one, first for [private bonds](/building-private-bonds-on-ethereum/), then for [private payments](/building-private-transfers-on-ethereum/). The primitive works. What comes next is a run of bigger iterations meant to make it both more resilient and more scalable: hard for any single party to watch, censor, or quietly take control of, and fast enough to carry payments at Visa scale. The trick is getting there without giving up the four things that make shielding worth having in the first place: censorship resistance, openness, privacy, and security. A shielded pool has a few moving parts, and each one makes that hard in its own way. Through a wallet, it writes and reads the Ethereum contract state. And it generates, on the user's own device, the zero-knowledge proof that makes each spend valid, which is also where the post-quantum question bites. We will walk the open problems, then go deep on the two our prototype takes on. @@ -26,7 +28,7 @@ No design solves all of these at once. Our prototype tries to solve two: the on- ## What our prototype takes on -We built an extension on top of the working pool from the last post, keeping its note format and flows intact, and pointed it at two of those problems: keeping the on-chain state bounded, and letting a wallet read what it needs without anyone watching. The full [specification and implementation](https://github.com/ethereum/iptf-pocs/tree/master/pocs/private-payment/shielded-pool-extension) are open. +We built an extension on top of the working pool from the last post, keeping its note format and flows intact, and pointed it at two of those problems: keeping the on-chain state bounded, and letting a wallet read what it needs without anyone watching. The full [specification and implementation](https://github.com/ethsystems/pocs/tree/master/pocs/private-payment/shielded-pool-extension) are open. The rest of this post follows a spend through both, starting with the read. @@ -117,6 +119,6 @@ PIR reads a known row without revealing which one. That is the part our prototyp Four problems stand between a working shielded pool and private payments at scale. The prototype settles two of them. Epoch nullifiers bound the *active* nullifier state, so the set every validator has to consult stays a fixed size instead of growing without end. PIR lets a wallet read a known row, its note's commitment path, without revealing which row to the provider. What it does not settle is the harder problem the work surfaced. Finding the neighbor leaf in a nullifier tree and finding your notes in the crowd are both private selection, and neither reduces to a known-index fetch. -This was a proof-of-concept, built to make that gap concrete. The [specification and code](https://github.com/ethereum/iptf-pocs/tree/master/pocs/private-payment/shielded-pool-extension) are open, the [earlier](/building-private-bonds-on-ethereum/) [posts](/building-private-transfers-on-ethereum/) build the pool it extends, and the IPTF map carries the [use case](https://github.com/ethereum/iptf-map/blob/master/use-cases/private-stablecoins.md) and [approach](https://github.com/ethereum/iptf-map/blob/master/approaches/approach-private-payments.md) for context. +This was a proof-of-concept, built to make that gap concrete. The [specification and code](https://github.com/ethsystems/pocs/tree/master/pocs/private-payment/shielded-pool-extension) are open, the [earlier](/building-private-bonds-on-ethereum/) [posts](/building-private-transfers-on-ethereum/) build the pool it extends, and the IPTF map carries the [use case](https://github.com/ethsystems/map/blob/master/use-cases/private-stablecoins.md) and [approach](https://github.com/ethsystems/map/blob/master/approaches/approach-private-payments.md) for context. The work splits across two fronts. Ethereum's roadmap is taking on censorship resistance, gas abstraction, and scaling at the protocol level. Cryptographic research is closing the rest, and private selection is the hardest piece left. diff --git a/src/posts/2026-07-14-introducing-ethsystems.md b/src/posts/2026-07-14-introducing-ethsystems.md new file mode 100644 index 0000000..3b4b5c4 --- /dev/null +++ b/src/posts/2026-07-14-introducing-ethsystems.md @@ -0,0 +1,78 @@ +--- +title: "Introducing EthSystems" +description: "EthSystems builds confidential systems for institutional Ethereum, carrying forward the IPTF work as an independent company focused on privacy, protocol design, and commercial execution." +date: "2026-07-14" +author: "Oskar" +image: ../assets/posts/2026-07-14-introducing-ethsystems/hero.png +published: false +--- + +Welcome to EthSystems. **We build confidential systems for institutional Ethereum.** + +What does that mean? Since its inception in 2014, Ethereum has slowly been turning into the Schelling point for any infrastructure where there's desire for a shared, public, immutable ledger that no one controls. In the last few years, this is increasingly true for institutions. But the fight isn't over. There are many different futures on the horizon, and our actions today will define the defaults for decades to come. + +Public blockchains like Ethereum have a clear edge over private ledgers, as argued in this [post](https://ethsystems.org/blog/public-rails-vs-private-ledgers/). One of the bigger problems with public blockchains is the lack of built-in, modular privacy tools. Essentially: while relying on a globally shared, verifiable ledger, who has access to what information? Under what conditions? With what security, performance and usability properties? Having worked in the crypto privacy space for over a decade, we believe there'll be many different solutions, all with their own trade-offs. There's no silver bullet. + +### Institutional demand + +What do we mean by institutions? In the broadest sense possible, an institution is any structure of rules and norms, created by humans, that has some form of continuity. This includes anything from economic and legal systems, to businesses, governments, and civic organizations. All of these can benefit from technologies like Ethereum, especially when combined with privacy tech. + +From a market point of view, there's massive demand especially when it comes to financial institutions: stablecoins, tokenization, settlement. This is especially true with increased regulatory clarity in recent years. Wall Street et al have found crypto as an asset class, but not yet as a commercial infrastructure. This is changing. We are witnessing the global financial infrastructure upgrading in real time, a once-in-a-generation event. + +With this comes a need for technical rigor. We are past the YOLO phase of crypto. These institutions have real businesses with billions of dollars on the line. They need to know the systems they rely on are secure and designed properly. This means ensuring proper protocol design and architecture, with clear security and privacy properties. This is especially important when working with immutable and publicly shared ledgers like Ethereum. + +### Our team + +EthSystems is Mo Jalil, Oskar Thorén, and Aaryamann Challani. + +Our team has an extensive background in protocol research and design, especially when it comes to how to design confidential systems. Back in 2017, before privacy in crypto was cool, we were designing secure messaging, p2p and infrastructure privacy protocols. The kind that other privacy products run on. We've been running production crypto infrastructure ever since, from before "privacy" was even a narrative. We also have a background in enterprise technology and traditional finance, and several of us are repeat founders. + +For the past year, we've been working at the Ethereum Foundation (EF) as the Institutional Privacy Task Force (IPTF). As part of this we've been able to do two things at the same time: + +1. Talk to hundreds of institutions, including many of the biggest ones in the world +2. Ship technical work to help institutions onboard onto Ethereum, especially related to privacy + +Before IPTF, some of us had been at or around EF for several years, including setting up the APAC Enterprise team and advising on privacy, the access layer and institutional strategy. This is in addition to working in the Ethereum space for close to a decade. Working at the EF was a great privilege and a responsibility, and it opened a lot of doors. We are leaving on good terms with everyone and will continue collaborating with friends and colleagues. + +As EF continues doubling down on cypherpunk fundamentals, especially with a focus on individuals, there's room for an independent for-profit entity that can make different choices in the trade-off space. The fundamental properties that make this technology interesting: Censorship Resistance, Open Source, Privacy, and Security (CROPS) are relevant for both individuals and institutions. We've written more about the tension and overlap between cypherpunk and institutional privacy [here](https://ethsystems.org/blog/cypherpunk-x-institutional-privacy/) (note this was written before EF's recent Mandate). Our attitude here is best summarized as being pluralist: pragmatic *and* principled. + +As for the other spin-outs, we see ourselves as complementary: our niche is depth over breadth, with a focus on technical execution. + +### Proof of work + +While conversations we've had and have with institutions are confidential, the technical work is open source. We have built up a body of work over the last year, and will continue to ship publicly as EthSystems. Open source, protocol specifications, privacy and working with the larger Ethereum ecosystem are a core part of our DNA. Here's a sample of the things we've been doing: + +- [Ethereum Privacy Map](https://github.com/ethsystems/map) mapping real-world institutional privacy requirements across the Ethereum ecosystem with [use cases](https://ethsystems.org/use-cases/), [patterns](https://ethsystems.org/patterns/), [approaches](https://ethsystems.org/approaches/), [vendors](https://ethsystems.org/vendors/), [jurisdictions](https://ethsystems.org/jurisdictions/), and an [interactive map graph](https://ethsystems.org/map/). +- Private bonds PoCs: a three-part build exploring [zero-knowledge proofs](https://ethsystems.org/blog/building-private-bonds-on-ethereum/), [privacy L2s](https://ethsystems.org/blog/building-private-bonds-on-ethereum-part-2/), and [fully homomorphic encryption](https://ethsystems.org/blog/building-private-bonds-on-ethereum-part-3/) for confidential institutional bonds. +- Private stablecoin transfer PoCs: [compliance-first shielded pools on L1](https://ethsystems.org/blog/building-private-transfers-on-ethereum-with-shielded-pools/) with KYC-gated entry and selective disclosure, plus [ZK-plasma designs](https://ethsystems.org/blog/building-private-transfers-on-ethereum-with-plasma/) for scale. +- Private cross-chain atomic swap PoC: [part 1](https://ethsystems.org/blog/private-crosschain-atomic-swaps-part-1-of-2/) and [part 2](https://ethsystems.org/blog/private-crosschain-atomic-swaps-part-2-of-2/) cover delivery-versus-payment across chains. +- [DIY Validium PoC](https://ethsystems.org/blog/diy-validium-private-logic-on-public-rails/): ordinary business logic, proved with ZKPs, verified on Ethereum. +- Resilience PoCs: three-part series on [plural identity](https://ethsystems.org/blog/resilient-plural-identity/), [disbursement rails](https://ethsystems.org/blog/resilient-disbursement-rails/), and [civic participation](https://ethsystems.org/blog/resilient-civic-participation/) under issuer failure, adversarial jurisdictions, and private coordination constraints. +- [Hardened shielded pools PoC with specs](https://ethsystems.org/blog/exploring-hardened-shielded-pools/): extends shielded pools with epoch nullifiers and PIR-backed note discovery. +- General write-ups: [Cypherpunk x Institutional Privacy](https://ethsystems.org/blog/cypherpunk-x-institutional-privacy/) explores overlap and tension between cypherpunks and institutions when it comes to privacy; [Public Rails vs Private Ledgers](https://ethsystems.org/blog/public-rails-vs-private-ledgers/) compares cryptographic privacy on public chains with trust-based privacy on private ledgers. + +All PoCs are open source and have rigorous protocol specifications with security properties. This is in addition to countless private workshops, architecture reviews and reports. + +### A for-profit company + +EthSystems is a for-profit company. While we love public goods, we believe there's a real need for a credible commercial entity that focuses on the hard technical problems required for institutional adoption. + +The business model is simple: bespoke consulting, focused on solving the hardest blockers for institutional adoption. In practice, this means continuing a lot of the work we have been doing, only charging money for it. Commercial engagements often require a commercial counterparty. We are funded and backed by long-term Ethereum-aligned investors. This is a decade-long transition, and we aren't going anywhere. + +Examples of commercial activities: +- Workshops to turn vague interest into hard requirements +- Proof of concepts around real institutional friction points: payments, tokenized assets, selective disclosure, private settlement +- Architecture reviews and protocol specifications, formalizing security properties +- Production systems that integrate with vendors and infrastructure institutions already run + +In addition to this we will continue doing public good work such as open source libraries, protocol specifications, mapping the ecosystem, write-ups, and community engagement. + +This acts as a feedback loop between traditional institutions with real demand and the Ethereum community with protocols and products: Institutions rarely know how to ask for Ethereum-native systems, and Ethereum teams rarely understand how institutional procurement, compliance, and risk work. We sit in the middle and translate both ways. + +### How to work with us + +If you represent an institution that wants to build on Ethereum and you think we can be useful, please contact us [here](https://docs.google.com/forms/d/e/1FAIpQLSd_89XMoNNUbqzJOZFsFsf1ibk2BK4vb5KV1SLre5naNdt6IA/viewform). + +We are also hiring. Specifically, BD people, protocol research engineers and operations. See [join us](/join/) for more detail. You can also follow us on X [here](https://x.com/eth_systems). + +Many institutions in the future will run on Ethereum. One of the biggest challenges is how to make these systems confidential, and there won't be a single solution that fits all. We've got a lot of work ahead of us. Come join us if this future is exciting to you, either as a contributor or as a customer. diff --git a/src/styles/fonts.css b/src/styles/fonts.css new file mode 100644 index 0000000..6840dd1 --- /dev/null +++ b/src/styles/fonts.css @@ -0,0 +1,83 @@ +/* Self hosted, same origin fonts. */ +/* Family names match the tokens in tokens.css ("Fraunces", "Geist", "Geist Mono"). */ +/* The url() targets are the Fontsource variable woff2 files. Astro (Vite) rebases */ +/* and emits them as same origin, content hashed assets at build time, so there is */ +/* no third party request. font-display: swap on every face. */ +/* latin loads for our Latin only copy; latin-ext faces cost nothing at runtime */ +/* (unicode-range keeps them from downloading unless a matching glyph appears). */ + +/* Fraunces. Display only. Variable axes opsz, wght, SOFT, WONK (the "full" file). */ +@font-face { + font-family: "Fraunces"; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../../node_modules/@fontsource-variable/fraunces/files/fraunces-latin-full-normal.woff2) format("woff2-variations"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: "Fraunces"; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../../node_modules/@fontsource-variable/fraunces/files/fraunces-latin-ext-full-normal.woff2) format("woff2-variations"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* Geist Sans. Body, UI, buttons. Variable axis wght. */ +@font-face { + font-family: "Geist"; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../../node_modules/@fontsource-variable/geist/files/geist-latin-wght-normal.woff2) format("woff2-variations"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: "Geist"; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../../node_modules/@fontsource-variable/geist/files/geist-latin-ext-wght-normal.woff2) format("woff2-variations"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* Geist Mono. Kickers, labels, data. Variable axis wght. */ +@font-face { + font-family: "Geist Mono"; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../../node_modules/@fontsource-variable/geist-mono/files/geist-mono-latin-wght-normal.woff2) format("woff2-variations"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: "Geist Mono"; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../../node_modules/@fontsource-variable/geist-mono/files/geist-mono-latin-ext-wght-normal.woff2) format("woff2-variations"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* + Metric matched fallback for Geist body text. + Maps the system fallback (Arial, the most metric stable cross platform base) + so that the swap from fallback to Geist causes zero layout shift (CLS 0). + Referenced in base.css as the family right after "Geist" on body text. + + Starting values below. They were derived from a + size-adjust of 96% with Geist to Arial metric ratios: + size-adjust: 96% (x-height / advance match, starting point) + ascent-override: 95% (Geist typo ascent over em, divided by size-adjust) + descent-override: 24% (Geist typo descent over em, divided by size-adjust) + line-gap-override: 0% (Geist ships no line gap) +*/ +@font-face { + font-family: "Geist Fallback"; + src: local("Arial"); + size-adjust: 96%; + ascent-override: 95%; + descent-override: 24%; + line-gap-override: 0%; +} diff --git a/src/styles/globals.css b/src/styles/globals.css index b8cef78..3104a8c 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -15,7 +15,7 @@ body { font-size: 16px; line-height: 1.65; color: var(--color-text); - background: var(--color-bg); + background: var(--paper); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; @@ -58,13 +58,14 @@ a:hover { color: var(--color-cta-hover); } margin-bottom: 3rem; } .section-head h2 { - font-family: var(--font-sans); - font-weight: 700; - font-size: clamp(2.15rem, 3.8vw, 2.85rem); - line-height: 1.14; - letter-spacing: -0.025em; + font-family: var(--font-display); + font-weight: 420; + font-variation-settings: "opsz" 144, "SOFT" 0, "WONK" 0; + font-size: clamp(2.15rem, 3.8vw, 3rem); + line-height: 1.06; + letter-spacing: -0.012em; color: var(--color-primary); - max-width: 30ch; + max-width: 24ch; margin-bottom: 0.25rem; } .section-head .meta { @@ -173,8 +174,8 @@ section.block { result was text visibly sitting above center. With nowrap (single line) and no overflow:hidden, descenders render safely outside the line box. */ line-height: 1; - letter-spacing: -0.005em; - border-radius: var(--radius-sm); + letter-spacing: 0; + border-radius: 0; padding: 0.6rem 1.05rem; border: 1px solid transparent; transition: background var(--duration-fast) var(--easing-soft), @@ -242,19 +243,21 @@ section.block { border-top: none; } .prose h1 { - font-family: var(--font-sans); - font-size: clamp(1.75rem, 3vw, 2.1rem); - font-weight: 700; - letter-spacing: -0.022em; - line-height: 1.18; + font-family: var(--font-display); + font-size: clamp(2rem, 3.4vw, 2.5rem); + font-weight: 400; + font-variation-settings: "opsz" 144, "SOFT" 0, "WONK" 0; + letter-spacing: -0.014em; + line-height: 1.08; color: var(--color-primary); } .prose h2 { - font-family: var(--font-sans); - font-size: 1.5rem; - font-weight: 700; - letter-spacing: -0.018em; - line-height: 1.25; + font-family: var(--font-display); + font-size: 1.7rem; + font-weight: 420; + font-variation-settings: "opsz" 144, "SOFT" 0, "WONK" 0; + letter-spacing: -0.01em; + line-height: 1.14; color: var(--color-primary); /* Symmetric spacing: equal breathing room above and below the divider line so it reads as a section break, not as crowded @@ -438,6 +441,56 @@ section.block { background: var(--color-neutral-tint); } +/* ───── kicker (eyebrow) ───── */ +.kicker { + font-family: var(--font-mono); + font-size: var(--fs-kicker); + font-weight: 500; + letter-spacing: var(--tr-kicker); + text-transform: uppercase; + color: var(--muted); +} + +/* ───── skip link ───── */ +.skip-link { + position: absolute; + top: var(--s-2); + left: var(--s-2); + z-index: 200; + transform: translateY(-150%); + padding: var(--s-2) var(--s-4); + font-family: var(--font-sans); + font-size: var(--fs-small); + font-weight: 500; + color: var(--paper); + background-color: var(--ink); + border-radius: var(--r-sm); + transition: transform var(--dur-1) var(--ease); +} +.skip-link:focus { transform: translateY(0); } + +/* ───── focus ring + selection ───── */ +:where(a, area, button, input, label[for], select, summary, textarea, [tabindex]):focus-visible { + outline: 2px solid var(--navy); + outline-offset: 2px; +} +::selection { background: color-mix(in srgb, var(--navy) 15%, transparent); } + +/* ───── paper grain overlay ───── + Fixed, non-interactive, multiply-blended paper grain. Inline SVG data URI + (fractalNoise). No network request. */ +.grain { + position: fixed; + inset: 0; + z-index: 100; + pointer-events: none; + opacity: 0.035; + mix-blend-mode: multiply; + background-repeat: repeat; + background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='120'%20height='120'%3E%3Cfilter%20id='n'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.9'%20numOctaves='2'%20stitchTiles='stitch'/%3E%3C/filter%3E%3Crect%20width='100%25'%20height='100%25'%20filter='url(%23n)'/%3E%3C/svg%3E"); +} +@media print { .grain { display: none; } } + /* ───── reduced motion ───── */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { diff --git a/src/styles/tokens.css b/src/styles/tokens.css index 21f3417..e362b4e 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -1,55 +1,112 @@ /* - * IPTF design tokens — single source of truth. + * EthSystems design tokens — single source of truth. * Every CSS custom property used by the visual system lives here. * No other stylesheet is allowed to declare :root vars. - * Naming convention: semantic (purpose), not positional (--color-text-2 etc). + * + * Two layers: + * 1. BASE — the ethsystems paper/ink/navy system (from ~/Repos/web). + * Fraunces display, Geist sans, Geist Mono. Warm paper surfaces. + * 2. BRIDGE — the legacy semantic names (--color-*) repointed at the + * base layer, so the ~30 existing components reskin through this + * file without touching their call sites. */ :root { + /* ============================================================= * + * 1. BASE — ethsystems system. Values are exact, do not improvise. * + * ============================================================= */ + + /* Surfaces (warm paper, tuned to read expensive) */ + --paper: #F4F1E6; /* canvas */ + --surface: #FBF9F1; /* raised cards, framed panels */ + --sunken: #ECE8DA; /* recessed bands, hairline fills */ + + /* Ink and text */ + --ink: #0A0A0A; /* headlines, primary text */ + --ink-2: #333029; /* secondary text */ + --muted: #5A5347; /* mono labels, captions */ + + /* Accent (rare) */ + --navy: #133E87; /* links, focus, hero glint */ + --navy-2: #0E2E66; /* hover / pressed */ + --blue: #3E6BA8; /* deeper mid blue, decorative or large text only */ + --blue-soft:#608BC1; /* decorative only, never body text */ + --sky: #CBDCEB; /* faint fills only, never text */ + + /* Lines */ + --line: rgba(10, 10, 10, 0.12); + --line-2: rgba(10, 10, 10, 0.20); + + /* Type families. Geist Fallback (metric-matched Arial) sits after Geist + so the font swap causes no layout shift. Defined in fonts.css. */ + --font-display: "Fraunces", Georgia, "Times New Roman", serif; + --font-sans: "Geist", "Geist Fallback", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace; + + /* Fluid type + mono/kicker sizes used by ported chrome (nav, footer, + kicker, HeroMark). */ + --fs-body: clamp(1rem, 0.97rem + 0.15vw, 1.125rem); + --fs-lead: clamp(1.3rem, 1.05rem + 1.05vw, 1.8rem); + --fs-small: 0.875rem; + --fs-mono: 0.75rem; + --fs-kicker: 0.8125rem; + --tr-kicker: 0.22em; + --tr-mono: 0.2em; + + /* Spacing scale (rem) — used by ported chrome/utilities. */ + --s-1: 0.25rem; --s-2: 0.5rem; --s-3: 0.75rem; --s-4: 1rem; + --s-6: 1.5rem; --s-8: 2rem; --s-12: 3rem; --s-16: 4rem; + --s-24: 6rem; --s-32: 8rem; + + /* Radii (new system) */ + --r-sm: 6px; --r: 8px; --r-lg: 12px; --r-pill: 999px; + + /* Shadow (very subtle, warm-neutral) */ + --shadow-1: 0 1px 2px rgba(10, 10, 10, 0.04), 0 8px 24px rgba(10, 10, 10, 0.06); + + /* Motion (new system) */ + --ease: cubic-bezier(0.16, 1, 0.3, 1); + --ease-inout: cubic-bezier(0.65, 0, 0.35, 1); + --dur-1: 0.2s; --dur-2: 0.5s; --dur-3: 0.9s; + + /* ============================================================= * + * 2. BRIDGE — legacy semantic names → base layer. * + * Repointed, not renamed. Components stay untouched. * + * ============================================================= */ + /* brand */ - --color-primary: #1a365d; - --color-primary-light: #2c5282; - - /* Interactive blue. Single source of truth — every interactive - element (button, link, hover border, action icon) idles on - --color-cta and resolves to --color-cta-hover (navy primary) - on hover. --color-on-cta is the text/ink rendered on top of - any blue/navy interactive surface, locked white so on-blue - contrast can never drift back to blue-on-blue. */ - --color-cta: #2257e9; - --color-cta-hover: #1a365d; - - --color-on-cta: #ffffff; - --color-on-primary: #ffffff; - - /* Subtle CTA-blue tints — used as background washes for hover - states, accent panels, and pills that need a brand tone without - full saturation. Three steps so the appropriate weight can be - picked per context (8% / 12% for backgrounds, 25% for borders). */ - --color-cta-tint: rgba(34, 87, 233, 0.08); - --color-cta-tint-strong: rgba(34, 87, 233, 0.12); - --color-cta-border: rgba(34, 87, 233, 0.25); + --color-primary: var(--ink); /* headings / brand text */ + --color-primary-light: var(--blue); + + /* interactive (was CTA-blue) → navy accent */ + --color-cta: var(--navy); + --color-cta-hover: var(--navy-2); + --color-on-cta: var(--paper); + --color-on-primary: var(--paper); + --color-cta-tint: rgba(19, 62, 135, 0.08); + --color-cta-tint-strong: rgba(19, 62, 135, 0.12); + --color-cta-border: rgba(19, 62, 135, 0.28); /* text */ - --color-text: #1a202c; - --color-text-secondary: #4a5568; - --color-text-muted: #718096; - --color-text-faint: #a0aec0; + --color-text: var(--ink); + --color-text-secondary: var(--ink-2); + --color-text-muted: var(--muted); + --color-text-faint: color-mix(in srgb, var(--muted) 60%, var(--paper)); /* surfaces */ - --color-bg: #ffffff; - --color-bg-subtle: #f7fafc; - --color-bg-muted: #edf2f7; + --color-bg: var(--surface); /* raised cards / panels */ + --color-bg-subtle: var(--sunken); /* recessed bands */ + --color-bg-muted: var(--sunken); + --color-bg-card: var(--surface); /* lines and borders */ - --color-border: #e2e8f0; - --color-border-strong: #cbd5e0; - - /* Quality / tone palette — emerald / amber / slate "traffic light" - applied wherever the UI signals a quality level (CROPS axes, - maturity, status, case-study availability). The *-strong variants - are the darker text colours used on top of *-tint backgrounds so - foreground contrast remains WCAG-safe. */ + --color-border: var(--line); + --color-border-strong: var(--line-2); + + /* ---- Quality / tone palette (signal colours) ---- + Hues kept so the traffic-light / CROPS / context signals still read + at a glance; the *-tint fills are translucent so they warm over paper. + Retuned further for the explorer in Phase 3. */ --color-success: #10b981; --color-success-strong: #047857; --color-success-tint: rgba(16, 185, 129, 0.14); @@ -68,34 +125,18 @@ --color-danger-tint: rgba(220, 38, 38, 0.07); --color-danger-tint-strong: rgba(220, 38, 38, 0.22); - /* Context coloring — applied to the two threat-model contexts the - IPTF body distinguishes throughout the guide: - --color-i2i institution-to-institution (indigo) - --color-i2u institution-to-user (teal) - Picked to be visually distinct from the brand blue and from each - other while staying within an enterprise-safe palette. */ + /* Threat-model contexts: i2i (indigo) / i2u (teal) */ --color-i2i: #4f46e5; --color-i2i-tint: rgba(99, 102, 241, 0.10); --color-i2u: #0d9488; --color-i2u-tint: rgba(20, 184, 166, 0.10); - /* Violet — third step of the EntryFacts neutral-palette rotation. - Not a traffic-light tone; purely a visual variant so consecutive - neutral fact-blocks read as distinct cards. */ + /* Violet — EntryFacts neutral rotation */ --color-violet: #6d28d9; --color-violet-tint: rgba(139, 92, 246, 0.06); --color-violet-tint-strong: rgba(139, 92, 246, 0.2); - /* Path-card surface — neutral light gray distinct from the blue-tinted - --color-bg-subtle / --color-bg-muted. Used by the reading-path cards - on the homepage. */ - --color-bg-card: #f4f5f6; - - /* CROPS pie palette — saturated traffic-light specifically scoped - to the pie + tooltip pill + crops-context-table. Deliberately - brighter than the design-system success/warning/danger so the - pie reads as an at-a-glance signal panel. Strong variants are - the darker text colour for tooltip pills on the tinted background. */ + /* CROPS pie palette — brighter, scoped to the pie + tooltip + table. */ --color-crops-full: #50e35f; --color-crops-full-tint: rgba(80, 227, 95, 0.18); --color-crops-full-strong: #15803d; @@ -106,27 +147,20 @@ --color-crops-low-tint: rgba(249, 74, 36, 0.18); --color-crops-low-strong: #b91c1c; - /* type */ - --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - --font-mono: 'JetBrains Mono', 'Fira Code', 'Monaco', monospace; - - /* depth — 4 tiers, mapped from prototype --shadow-1..4 */ - --shadow-sm: 0 1px 2px 0 rgba(13, 20, 36, 0.04); - --shadow-md: 0 1px 3px 0 rgba(13, 20, 36, 0.06), 0 1px 2px -1px rgba(13, 20, 36, 0.04); - --shadow-lg: 0 4px 12px -2px rgba(13, 20, 36, 0.08), 0 2px 4px -2px rgba(13, 20, 36, 0.04); - --shadow-xl: 0 12px 32px -8px rgba(13, 20, 36, 0.12), 0 4px 8px -4px rgba(13, 20, 36, 0.06); + /* ---- Legacy scales kept (radii / shadows / layout / motion) ---- */ + --shadow-sm: 0 1px 2px 0 rgba(10, 10, 10, 0.04); + --shadow-md: 0 1px 3px 0 rgba(10, 10, 10, 0.06), 0 1px 2px -1px rgba(10, 10, 10, 0.05); + --shadow-lg: 0 4px 12px -2px rgba(10, 10, 10, 0.08), 0 2px 4px -2px rgba(10, 10, 10, 0.05); + --shadow-xl: 0 12px 32px -8px rgba(10, 10, 10, 0.12), 0 4px 8px -4px rgba(10, 10, 10, 0.07); - /* radii — 4 tiers, mapped from prototype --r-1..4 */ --radius-sm: 4px; --radius-md: 8px; --radius-lg: 12px; --radius-xl: 16px; - /* layout */ --container-max: 1200px; --container-pad: clamp(1.25rem, 3vw, 2rem); - /* motion */ --easing-soft: cubic-bezier(0.4, 0, 0.2, 1); --duration-fast: 120ms; --duration-base: 180ms;