Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/upgrades/changelog/ChangelogClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
kindLabel,
LIFECYCLE_LABELS,
} from '../library/display';
import { formatShortDate } from '../library/format';
import { formatShortDate, toPlainText } from '../library/format';
import { getLifecycleState } from '../library/lifecycle';
import type { Change, ChangeCategory, ChangeKind, LifecycleState } from '../library/types';

Expand Down Expand Up @@ -92,7 +92,7 @@ export function ChangelogClient() {
if (!matches) return false;
}
if (matcher) {
const haystack = `${changeDisplayTitle(change)} ${change.summary} ${change.category} ${changeRefs(change).join(' ')}`;
const haystack = `${changeDisplayTitle(change)} ${toPlainText(change.summary)} ${change.category} ${changeRefs(change).join(' ')}`;
if (!matcher(haystack)) return false;
}
return true;
Expand Down Expand Up @@ -287,7 +287,7 @@ export function ChangelogClient() {
{change.title}
</Text>
<Text variant="label.regular" tone="muted" className="mt-1 line-clamp-2">
{change.summary}
{toPlainText(change.summary)}
</Text>
<div className="mt-4 flex items-center justify-between gap-2">
<CategoryBadge category={change.category} />
Expand Down
5 changes: 3 additions & 2 deletions app/upgrades/changelog/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Breadcrumb } from '../../components/Breadcrumb';
import { getChangeBySlug } from '../../data/changes';
import { getVibenetChangeById } from '../../data/vibenet';
import { LIFECYCLE_LABELS } from '../../library/display';
import { toPlainText } from '../../library/format';

import { ChangeDetailClient } from './ChangeDetailClient';

Expand All @@ -29,13 +30,13 @@ export async function generateMetadata(props: ChangePageProps): Promise<Metadata
if (!change) return {};
return {
title: `${change.title} | Base Upgrades`,
description: change.summary,
description: toPlainText(change.summary),
alternates: {
canonical: `/upgrades/changelog/${change.slug}`,
},
openGraph: {
title: `${change.title} | Base Upgrades`,
description: change.summary,
description: toPlainText(change.summary),
url: `/upgrades/changelog/${change.slug}`,
},
};
Expand Down
4 changes: 2 additions & 2 deletions app/upgrades/components/FeaturedVibenetCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EmptyState } from '../../components/ui/EmptyState';
import { Text } from '../../components/ui/Text';
import type { VibenetChange } from '../data/vibenet';
import { CATEGORY_METADATA, LIFECYCLE_LABELS } from '../library/display';
import { formatShortDate } from '../library/format';
import { formatShortDate, toPlainText } from '../library/format';

import { KindBadge, StatusPill } from './Badges';
import { Carousel } from './Carousel';
Expand Down Expand Up @@ -53,7 +53,7 @@ export function FeaturedVibenetCarousel({ changes }: FeaturedVibenetCarouselProp
{change.title}
</Text>
<Text variant="label.regular" tone="muted" className="line-clamp-3">
{change.summary}
{toPlainText(change.summary)}
</Text>
<div className="mt-auto flex items-center justify-between gap-3 border-t border-bds-gray-10 pt-4 dark:border-white/10">
<StatusPill variant={change.vibenet.status}>
Expand Down
138 changes: 15 additions & 123 deletions app/upgrades/data/changes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,16 +154,16 @@ export const changes: Change[] = [
{
kind: 'base',
id: 'base-0001',
slug: 'proof-system',
slug: 'multiproofs',
baseNumber: '0001',
owner: 'Proofs team',
title: 'Proof System',
title: 'Multiproofs',
category: 'proofs',
upgrade: 'azul',
summary:
'Introduces a multi-proof system for L2 checkpoints, where AggregateVerifier can verify one or two proofs for the same proposal before withdrawals rely on it.',
"<p class='font-bold'>Proof System</p><p class='mt-2 pb-4'>Introduces a multi-proof system for L2 checkpoints, where AggregateVerifier can verify one or two proofs for the same proposal before withdrawals rely on it.</p><p class='font-bold'>New/Changed Onchain Components</p><p class='mt-2 pb-4'>Adds AggregateVerifier plus proof-specific verifier contracts, reduces DelayedWETH to a 1-day withdrawal delay, and moves legacy finality timing out of OptimismPortal2 and AnchorStateRegistry.</p><p class='font-bold'>Proposer</p><p class='mt-2 pb-4'>Turns safe or finalized Base L2 checkpoints into L1 AggregateVerifier games by requesting TEE proofs, verifying output roots, and submitting proposals with the required bond.</p><p class='font-bold'>Challenger</p><p class='mt-2 pb-4'>Checks in-progress AggregateVerifier games against canonical L2 state and disputes incorrect claims through the permissionless ZK proof challenge path.</p><p class='font-bold'>TEE Provers</p><p class='mt-2 pb-4'>Adds AWS Nitro Enclave-backed TEE provers for the common proposal path. The enclave re-executes requested L2 block ranges and signs resulting checkpoint outputs.</p><p class='font-bold'>ZK Provers</p><p class='mt-2 pb-4'>Adds permissionless ZK provers for dispute verification, especially to challenge invalid TEE-backed proposals or invalidate bad ZK claims.</p><p class='font-bold'>Prover Registrar</p><p class='mt-2 pb-4'>Keeps the onchain TEEProverRegistry in sync with the live set of Nitro prover signers, attesting active signer identities and removing orphaned signers.</p>",
migrationNotes:
'Update withdrawal monitors and bridge backends to track AggregateVerifier games and their proof-backed settlement path.',
'Refer to our docs site for full details on the proofs system migration.',
lastUpdated: '2026-04-12',
relatedRepos: [],
githubIssues: [],
Expand All @@ -172,80 +172,8 @@ export const changes: Change[] = [
{
kind: 'base',
id: 'base-0002',
slug: 'new-changed-onchain-components',
baseNumber: '0002',
owner: 'Proofs team',
title: 'New/Changed Onchain Components',
category: 'proofs',
upgrade: 'azul',
summary:
'Adds AggregateVerifier plus proof-specific verifier contracts, reduces DelayedWETH to a 1-day withdrawal delay, and moves legacy finality timing out of OptimismPortal2 and AnchorStateRegistry.',
migrationNotes:
'Read proof game timing from AggregateVerifier and remove assumptions that OptimismPortal2 or AnchorStateRegistry add a separate 3.5-day delay.',
lastUpdated: '2026-04-10',
relatedRepos: [],
githubIssues: [],
specUrl: azulProofsUrl('newchanged-onchain-components'),
},
{
kind: 'base',
id: 'base-0003',
slug: 'proposer',
baseNumber: '0003',
owner: 'Proofs team',
title: 'Proposer',
category: 'proofs',
upgrade: 'azul',
summary:
'Turns safe or finalized Base L2 checkpoints into L1 AggregateVerifier games by requesting TEE proofs, verifying output roots, and submitting proposals with the required bond.',
migrationNotes:
'Monitor proposal creation against canonical L2 checkpoint ranges and track the required AggregateVerifier game bond.',
lastUpdated: '2026-04-09',
relatedRepos: [],
githubIssues: [],
specUrl: azulProofsUrl('proposer'),
},
{
kind: 'base',
id: 'base-0004',
slug: 'challenger',
baseNumber: '0004',
owner: 'Proofs team',
title: 'Challenger',
category: 'proofs',
upgrade: 'azul',
summary:
'Checks in-progress AggregateVerifier games against canonical L2 state and disputes incorrect claims through the permissionless ZK proof challenge path.',
migrationNotes:
'Run or monitor challenger coverage for in-progress games and alert on disputes that identify invalid checkpoint claims.',
lastUpdated: '2026-04-08',
relatedRepos: [],
githubIssues: [],
specUrl: azulProofsUrl('challenger'),
},
{
kind: 'base',
id: 'base-0005',
slug: 'tee-provers',
baseNumber: '0005',
owner: 'Proofs team',
title: 'TEE Provers',
category: 'proofs',
upgrade: 'azul',
summary:
'Adds AWS Nitro Enclave-backed TEE provers for the common proposal path. The enclave re-executes requested L2 block ranges and signs resulting checkpoint outputs.',
migrationNotes:
'Document that TEE-backed proposals follow the common path and still use the long settlement window unless matched by ZK proof support.',
lastUpdated: '2026-04-07',
relatedRepos: [],
githubIssues: [],
specUrl: azulProofsUrl('tee-provers'),
},
{
kind: 'base',
id: 'base-0006',
slug: 'remove-account-balances-receipts',
baseNumber: '0006',
baseNumber: '0002',
owner: 'Flashblocks team',
title: 'Remove Account Balances & Receipts',
category: 'networking',
Expand All @@ -261,9 +189,9 @@ export const changes: Change[] = [
},
{
kind: 'base',
id: 'base-0007',
id: 'base-0003',
slug: 'basev0-protocol-id-discv5',
baseNumber: '0007',
baseNumber: '0003',
owner: 'Node team',
title: 'Use basev0 protocol ID for discv5',
category: 'networking',
Expand All @@ -279,45 +207,9 @@ export const changes: Change[] = [
},
{
kind: 'base',
id: 'base-0008',
slug: 'zk-provers',
baseNumber: '0008',
owner: 'Proofs team',
title: 'ZK Provers',
category: 'proofs',
upgrade: 'azul',
summary:
'Adds permissionless ZK provers for dispute verification, especially to challenge invalid TEE-backed proposals or invalidate bad ZK claims.',
migrationNotes:
'Surface proof status carefully: ZK proofs provide the permissionless override path, while normal proposal creation does not depend on ZK provers in Azul.',
lastUpdated: '2026-04-12',
relatedRepos: [],
githubIssues: [],
specUrl: azulProofsUrl('zk-provers'),
},
{
kind: 'base',
id: 'base-0009',
slug: 'prover-registrar',
baseNumber: '0009',
owner: 'Proofs team',
title: 'Prover Registrar',
category: 'proofs',
upgrade: 'azul',
summary:
'Keeps the onchain TEEProverRegistry in sync with the live set of Nitro prover signers, attesting active signer identities and removing orphaned signers.',
migrationNotes:
'Use the registry as the source of accepted TEE signer identities instead of hard-coding Nitro prover signers.',
lastUpdated: '2026-04-12',
relatedRepos: [],
githubIssues: [],
specUrl: azulProofsUrl('prover-registrar'),
},
{
kind: 'base',
id: 'base-0010',
id: 'base-0004',
slug: 'engine-api-usage',
baseNumber: '0010',
baseNumber: '0004',
owner: 'Node team',
title: 'Engine API Usage',
category: 'rpc',
Expand All @@ -333,9 +225,9 @@ export const changes: Change[] = [
},
{
kind: 'base',
id: 'base-0011',
id: 'base-0005',
slug: 'b20',
baseNumber: '0011',
baseNumber: '0005',
owner: 'Protocol team',
title: 'B20',
category: 'precompile',
Expand All @@ -356,9 +248,9 @@ export const changes: Change[] = [
},
{
kind: 'base',
id: 'base-0012',
id: 'base-0006',
slug: 'reducing-canonical-withdrawal-delay',
baseNumber: '0012',
baseNumber: '0006',
owner: 'Bridge team',
title: 'Reducing Canonical Withdrawal Delay',
category: 'bridging',
Expand All @@ -374,9 +266,9 @@ export const changes: Change[] = [
},
{
kind: 'base',
id: 'base-0013',
id: 'base-0007',
slug: 'reth-2-2',
baseNumber: '0013',
baseNumber: '0007',
owner: 'Node team',
title: 'Reth V2',
category: 'execution',
Expand Down
40 changes: 40 additions & 0 deletions app/upgrades/library/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,43 @@
export function sentenceJoin(parts: string[]): string {
return parts.filter(Boolean).join(' ');
}

const NAMED_ENTITIES: Record<string, string> = {
amp: '&',
lt: '<',
gt: '>',
quot: '"',
apos: "'",
nbsp: ' ',
};

function decodeEntities(value: string): string {
return value.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, entity: string) => {
if (entity[0] === '#') {
const codePoint =
entity[1] === 'x' || entity[1] === 'X'
? parseInt(entity.slice(2), 16)
: parseInt(entity.slice(1), 10);
return Number.isNaN(codePoint) ? match : String.fromCodePoint(codePoint);
}
const named = NAMED_ENTITIES[entity.toLowerCase()];
return named ?? match;
});
}

/**
* Strips HTML tags and decodes entities so a rich-text `summary` can be reused
* as plain text in compact previews, search haystacks, and SEO metadata. DOM
* free so it runs in both server and client components.
*/
export function toPlainText(html: string): string {
return decodeEntities(
html
// Treat block-ish boundaries as spaces so words don't collapse together.
.replace(/<\/(p|div|li|ul|ol|br|h[1-6])>/gi, ' ')
.replace(/<br\s*\/?>/gi, ' ')
.replace(/<[^>]*>/g, ''),
Comment on lines +61 to +65

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will not be an issue, as all change data is defined by us. We also do not use <script> tags in changes, as we simply use normal html tags to style the change summaries on the detail pages.

)
.replace(/\s+/g, ' ')
.trim();
}
45 changes: 11 additions & 34 deletions app/upgrades/library/upgrades.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,11 @@ describe('upgrades route helpers', () => {
'eip-7939',
'eip-7951',
]);
expect(networking?.changeIds).toEqual(['eip-7642', 'base-0006', 'base-0007']);
expect(networking?.changeIds).toEqual(['eip-7642', 'base-0002', 'base-0003']);
expect(flashblocks?.changeIds).toEqual([]);
expect(rpc?.changeIds).toEqual(['eip-7910', 'base-0010']);
expect(proofs?.changeIds).toEqual([
'base-0001',
'base-0002',
'base-0003',
'base-0004',
'base-0005',
'base-0008',
'base-0009',
]);
expect(rpc?.changeIds).toEqual(['eip-7910', 'base-0004']);
// The Azul proofs changes are consolidated into a single "Multiproofs" entry.
expect(proofs?.changeIds).toEqual(['base-0001']);
});

it('matches the Azul overview execution and proofs titles', () => {
Expand All @@ -91,8 +84,8 @@ describe('upgrades route helpers', () => {
titlesById['eip-7951'],
titlesById['eip-7642'],
titlesById['eip-7910'],
titlesById['base-0006'],
titlesById['base-0007'],
titlesById['base-0002'],
titlesById['base-0003'],
]).toEqual([
'Upper-Bound MODEXP',
'Transaction Gas Limit Cap',
Expand All @@ -105,29 +98,13 @@ describe('upgrades route helpers', () => {
'Use basev0 protocol ID for discv5',
]);

expect([
titlesById['base-0001'],
titlesById['base-0002'],
titlesById['base-0003'],
titlesById['base-0004'],
titlesById['base-0005'],
titlesById['base-0008'],
titlesById['base-0009'],
]).toEqual([
'Proof System',
'New/Changed Onchain Components',
'Proposer',
'Challenger',
'TEE Provers',
'ZK Provers',
'Prover Registrar',
]);
expect(titlesById['base-0001']).toBe('Multiproofs');
});

it('applies a per-change activation override without affecting other networks', () => {
const b20 = getChangeById('base-0011');
const b20 = getChangeById('base-0005');
const beryl = getUpgradeById('beryl');
if (!b20) throw new Error('base-0011 not found');
if (!b20) throw new Error('base-0005 not found');
if (!beryl) throw new Error('beryl not found');
const lifecycle = getLifecycleForChange(b20);
// Mainnet is turned on after Beryl; Sepolia inherits the upgrade date.
Expand All @@ -136,9 +113,9 @@ describe('upgrades route helpers', () => {
});

it('keeps upgrade-inherited lifecycle for changes without an override', () => {
const rethV2 = getChangeById('base-0013');
const rethV2 = getChangeById('base-0007');
const beryl = getUpgradeById('beryl');
if (!rethV2) throw new Error('base-0013 not found');
if (!rethV2) throw new Error('base-0007 not found');
if (!beryl) throw new Error('beryl not found');
expect(getLifecycleForChange(rethV2)).toEqual(beryl.lifecycle);
});
Expand Down
Loading