diff --git a/blogs/AGENTS.md b/blogs/AGENTS.md new file mode 100644 index 00000000..74ce3c5d --- /dev/null +++ b/blogs/AGENTS.md @@ -0,0 +1,51 @@ +# Writing blog posts + +Posts in this folder are rendered at `/blog` (index with category filters) and `/blog/` (post page). Drop a `.md` file here and it ships — no registration step. + +Name post files in kebab-case (`my-post-title.md`). ALL-CAPS `.md` files in this folder (like this one and `DIAGRAMS.md`) are documentation and are excluded from the site. + +To keep a post as a draft (written but not published), move it into the `drafts/` subfolder. Only `.md` files directly in `blogs/` are published; anything in a subfolder is ignored. Move it back up to ship it. + +## Frontmatter (required) + +```yaml +--- +title: "Tuning Reth for payments: how we hit 21,200 TPS" +excerpt: "One or two sentences shown on the index and as the post's lede." +date: 2026-06-02 +category: technical # network-upgrades | events | technical | case-studies +featured: true # optional — pins the post to the hero card on /blog +--- +``` + +`category` must be one of the four slugs above (the build fails loudly otherwise). At most one post should be `featured`; if none is, the newest post takes the hero card. + +## Body + +Standard markdown + GFM (tables, strikethrough). Code blocks are syntax-highlighted at build time by Shiki — always tag the language: + +````markdown +```rust +fn main() {} +``` +```` + +## Images + +- Assets live in `public/blog/`, referenced root-relative: `![Alt text](/blog/my-asset.svg)` +- Alt text is required. An italic-only paragraph directly after an image renders as a caption: + +```markdown +![Sustained TPS by release](/blog/reth-tps-benchmark.svg) + +*Benchmarked on a live network under continuous load.* +``` + +- Prefer SVG for charts and diagrams, photos as compressed JPEG/PNG. + +## Diagrams + +Charts and diagrams must follow the house diagram style — dark monochrome, +mono type, single accent. Before drawing one, read +[`DIAGRAMS.md`](./DIAGRAMS.md) in this folder; it has the full palette, +typography rules, layout system, and copy-paste templates. diff --git a/blogs/DIAGRAMS.md b/blogs/DIAGRAMS.md new file mode 100644 index 00000000..c1ef0bb7 --- /dev/null +++ b/blogs/DIAGRAMS.md @@ -0,0 +1,159 @@ +# Blog diagram style guide + +The house style for every chart and diagram embedded in a blog post. Diagrams +are hand-written static SVGs — no charting library, no runtime component. The +constraint is the point: everything is flat, monochrome, monospaced, and +square, with a single accent color carrying the one idea the diagram exists to +show. + +Reference implementations: `public/blog/reth-tps-benchmark.svg` (chart), +`public/blog/parallel-execution-lanes.svg` (box/lane diagram). + +There is a live playground at [`/diagrams`](/diagrams) (run the dev server and +open it in a browser): edit the style tokens and chart data, preview both +templates, and copy or download ready-to-ship SVG. The values below are the +house defaults; if the playground tokens and this document ever disagree, this +document wins. + +## Rules in one paragraph + +Dark `#0e0e0e` canvas, 840px wide. All text is monospace, labels are +UPPERCASE. Boxes are sharp-cornered rectangles with 1px strokes. Everything is +greyscale except **one** accent color, used only on the element the diagram is +about. No gradients, no shadows, no rounded corners, no icons, no second +accent. + +## Canvas + +| Property | Value | +| --- | --- | +| Width | `840` (fixed — matches the post column) | +| Height | whatever the content needs, typically 360–420 | +| Background | full-bleed `` of `#0e0e0e` | +| Margins | 40px on all sides; content starts at `x=40` | +| Root attrs | `fill="none" xmlns="http://www.w3.org/2000/svg" font-family="ui-monospace, 'JetBrains Mono', monospace"` | + +The site wraps embedded images in a 1px border, so don't draw your own outer +frame. + +## Palette + +Hardcode these hex values — an SVG loaded via `` can't read the site's +CSS variables. They mirror tokens in `app/globals.css`: + +| Role | Value | Mirrors token | +| --- | --- | --- | +| Canvas background | `#0e0e0e` | `--surface-block` | +| Neutral box fill | `#1c1c1c` | — | +| Neutral box stroke | `#2e2e2e` | `--line-strong` | +| Gridlines, dividers | `#181818` | `--line` | +| Dashed annotation stroke | `#2e2e2e`, `stroke-dasharray="4 4"` | — | +| Accent stroke/text | `#65ff54` | `--indicator-green` | +| Accent fill | `#143810` | — | +| Alt accent (rare) | `#5d88ff` stroke, `#10204d` fill | `--accent-blue` | + +Use green by default. Reach for blue only when a single diagram genuinely +needs two accents (almost never). + +## Typography + +All monospace (inherited from the root `font-family`), all caps for labels: + +| Role | Size | Fill | +| --- | --- | --- | +| Title | `13`, `letter-spacing="0.04em"` | `rgba(255,255,255,0.85)` | +| Subtitle | `11` | `rgba(255,255,255,0.4)` | +| Box/data labels | `11` | `rgba(255,255,255,0.6)` neutral, `#65ff54` accent | +| Axis ticks, lane names | `10`–`11` | `rgba(255,255,255,0.3)`–`0.35` | +| Emphasized value or axis label | `11` | `rgba(255,255,255,0.7)` | + +Every diagram opens with a title block at the top left: + +```xml +TITLE OF THE DIAGRAM +ONE-LINE QUALIFIER OR DATA SOURCE +``` + +## Layout + +- Center text in boxes with `text-anchor="middle"`; vertically, place text + baseline ~5px below box center (e.g. 32px-tall box at `y=256` → text + `y=277`). +- Separate stacked sections with a full-width `#181818` divider line. +- Charts: gridlines `#181818` with tick labels on the left, a `#2e2e2e` + baseline, values labeled above each mark. +- Annotate empty/conceptual regions with a dashed `#2e2e2e` rect and a muted + centered label (see "RECLAIMED BLOCKSPACE" in the lanes diagram). + +## Templates + +### Bar chart + +```xml + + + TITLE + QUALIFIER + + + + + + + + + + VALUE + LABEL + + + + VALUE + LABEL + +``` + +### Box / lane diagram + +```xml + + + TITLE + QUALIFIER + + LANE + + + + item + + + + item + + + + ANNOTATION + +``` + +## Shipping checklist + +1. Save to `public/blog/.svg`. +2. **Validate the XML** — SVGs loaded via `` are strict XML. One bad + byte (smart quote, em dash corrupted to a control char, unescaped `&`) + silently breaks the whole image in the browser: + + ```bash + xmllint --noout public/blog/.svg + ``` + + Escape `&` as `&` and `<` as `<` in labels. Em dashes are fine as + UTF-8 `—`, but verify with xmllint after writing. +3. Embed with required alt text and an optional italic caption: + + ```markdown + ![One sentence describing what the diagram shows](/blog/.svg) + + *Optional caption.* + ``` diff --git a/blogs/t6.md b/blogs/t6.md new file mode 100644 index 00000000..186efc9b --- /dev/null +++ b/blogs/t6.md @@ -0,0 +1,82 @@ +--- +title: "T6 network upgrade: Receive policies, admin access keys, and more" +excerpt: "The T6 network upgrade adds two new account-level controls to Tempo: receive policies, which let an account decide which tokens and senders it accepts, and admin access keys, which let an account delegate key management without using the root key." +date: 2026-06-23 +category: network-upgrades +--- + +**The T6 network upgrade adds two new account-level controls to Tempo: receive policies, which let an account decide which tokens and senders it accepts, and admin access keys, which let an account delegate key management without using the root key.** [Read the T6 docs to start integrating →](/docs/protocol/upgrades/t6) + +## Account-level receive policies + +Receive policies let an account specify which TIP-20 tokens it will accept and which addresses can send to it. This is useful for exchanges, custodians, on and off ramps, payment processors, and treasury systems that need to keep unsupported or unwanted assets out of balances, or limit who can send to an account. + +An account opting in configures three things: + +- **Tokens:** an allowlist or blocklist of TIP-20 tokens it will accept. +- **Senders:** an allowlist or blocklist of addresses that can send to it. +- **Recovery authority:** who can move the funds if a send is held. + +Policies are opt-in. An account with no policy works exactly as it does today, and a sender starts a normal transfer or mint without needing to know a policy exists. Policies are set and enforced on the TIP-403 precompile, and any policy a token already enforces still runs first. + +When a transfer is accepted, the funds are credited normally. When it is not, the transfer still succeeds, but delivery is redirected to the `ReceivePolicyGuard` precompile and a receipt records enough context for recovery. The held funds stay attributable and recoverable by the authority the account chose in advance. + +For integrators, this adds a third delivery state to model: + +- **Failed:** token checks revert and the transfer does not go through, as today. +- **Credited:** the receiver accepts the transfer and the funds arrive. +- **Held:** the transfer succeeds, but the policy routes the funds to the guard for recovery. + +![A receive policy adds a third delivery state, held, alongside failed and credited.](/blog/receive-policy-delivery-states.svg) + +*An unaccepted transfer still succeeds; the funds are held by the guard and stay recoverable.* + +Wallets, explorers, and indexers should treat held sends as their own state and listen for the `TransferBlocked` event, since blocked receipts are not enumerable onchain. For deposit flows that use TIP-1022 virtual addresses, the policy is resolved against the master account before the check runs, and receipts preserve the original recipient for attribution. + +In practice, this makes several common payment operations easier to run: + +### Supported-asset controls + +A platform can configure deposit addresses to accept only the stablecoins it supports. If a user sends an unsupported token, the funds are held and recoverable rather than becoming an operational problem. + +### Counterparty controls + +A regulated business can define which addresses are allowed to send to an account. This gives compliance and operations teams a protocol-native control for inbound payments, rather than relying only on monitoring after funds have already arrived. + +Read the [specification](https://tips.sh/1028), [learn more in the docs](/docs/learn/tempo/receive-policies#account-level-receive-policies), and [try the demo](https://tempo.xyz/receive-policies). + +## Admin access keys + +[Access keys](/docs/protocol/transactions#access-keys) on Tempo let users and applications authorize limited actions. They are useful for subscriptions, delegated payments, automated operations, and other flows where a narrowly scoped key is safer than using the root key directly. + +Until now, key management itself still depended on the root key. Admin access keys change that: an account can designate certain access keys as administrative, and those keys can authorize and revoke other keys on the account's behalf. This separates account ownership from day-to-day key administration: + +- An admin key can manage operational keys for services, devices, and automated systems while the root key stays protected. +- An operational key can be rotated or revoked without root-key access. +- Admin keys are for key management only. They cannot carry spending limits, call scopes, or expiry. + +![Admin keys can authorize and revoke other keys while the root key stays protected.](/blog/account-key-roles.svg) + +*Admin keys manage operational keys without using the root key.* + +T6 also gives contracts a canonical way to verify key status. `verifyKeychain` confirms a signature came from an active key on an account, and `verifyKeychainAdmin` confirms it came from the root key or an admin key. Builders get a safe primitive for account-gated workflows instead of rebuilding key-status logic in every contract. Note that `verifyKeychainAdmin` does not bind the account into the signed digest, so callers should include a replay domain such as chain ID, contract address, and account address in whatever they ask a key to sign. + +Read the [specification](https://tips.sh/1049) and [learn more in the docs](/docs/protocol/transactions#access-keys). + +## What integrators should know + +Node operators should run the `v1.9.0` release to stay in sync with mainnet. The full set of changes and compatible package versions are in the release notes. + +See the [v1.9.0 release](https://github.com/tempoxyz/tempo/releases/tag/v1.9.0) and the [T6 upgrade notes](/docs/protocol/upgrades/t6). + +## Resources + +- [T6 upgrade docs](/docs/protocol/upgrades/t6) +- [Receive policies overview](/docs/learn/tempo/receive-policies#account-level-receive-policies) +- [Access keys](/docs/protocol/transactions#access-keys) +- [TIP-1028: receive policies specification](https://tips.sh/1028) +- [TIP-1049: admin access keys specification](https://tips.sh/1049) +- [Receive policies demo](https://tempo.xyz/receive-policies) +- [v1.9.0 release](https://github.com/tempoxyz/tempo/releases/tag/v1.9.0) +- [Tempo on GitHub](https://github.com/tempoxyz) +- [All TIPs](/docs/protocol/tips) diff --git a/package.json b/package.json index cda60aee..73934283 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@iconify-json/lucide": "^1.2.102", "@iconify-json/simple-icons": "^1.2.77", "@monaco-editor/react": "^4.7.0", + "@shikijs/rehype": "^3.23.0", "@takumi-rs/image-response": "0.62.8", "@takumi-rs/wasm": "0.62.8", "@tanstack/react-query": "^5.99.0", @@ -42,10 +43,16 @@ "react": "^19.2.6", "react-dom": "^19.2.6", "react-server-dom-webpack": "~19.2.6", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "shiki": "^3.23.0", "sonner": "^2.0.7", "sql-formatter": "^15.7.3", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.2", + "unified": "^11.0.5", "unplugin-auto-import": "^21.0.0", "unplugin-icons": "^23.0.1", "viem": "^2.52.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78d112ee..f5123be4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,9 @@ importers: '@monaco-editor/react': specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@shikijs/rehype': + specifier: ^3.23.0 + version: 3.23.0 '@takumi-rs/image-response': specifier: 0.62.8 version: 0.62.8 @@ -87,6 +90,21 @@ importers: react-server-dom-webpack: specifier: ~19.2.6 version: 19.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(webpack@5.104.1(esbuild@0.27.7)) + rehype-stringify: + specifier: ^10.0.1 + version: 10.0.1 + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 + remark-parse: + specifier: ^11.0.0 + version: 11.0.0 + remark-rehype: + specifier: ^11.1.2 + version: 11.1.2 + shiki: + specifier: ^3.23.0 + version: 3.23.0 sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -99,6 +117,9 @@ importers: tailwindcss: specifier: ^4.2.2 version: 4.2.2 + unified: + specifier: ^11.0.5 + version: 11.0.5 unplugin-auto-import: specifier: ^21.0.0 version: 21.0.0(@vueuse/core@13.9.0(vue@3.5.38(typescript@5.9.3))) diff --git a/public/blog/account-key-roles.svg b/public/blog/account-key-roles.svg new file mode 100644 index 00000000..720d9d29 --- /dev/null +++ b/public/blog/account-key-roles.svg @@ -0,0 +1,32 @@ + + + + ACCOUNT KEY ROLES + T6 SEPARATES OWNERSHIP FROM KEY ADMINISTRATION + + + + ROOT KEY + owns the account + stays protected + + + + ADMIN KEY + authorizes + revokes + other keys + + + + LIMITED KEY + spend limits, call + scopes, expiry + + + + + + + + MANAGES KEYS + diff --git a/public/blog/parallel-execution-lanes.svg b/public/blog/parallel-execution-lanes.svg new file mode 100644 index 00000000..48e4db04 --- /dev/null +++ b/public/blog/parallel-execution-lanes.svg @@ -0,0 +1,55 @@ + + + + + SERIAL EXECUTION + ONE TRANSACTION AT A TIME, 6 BLOCKS OF WALL TIME + + + + tx 1 + + tx 2 + + tx 3 + + tx 4 + + tx 5 + + tx 6 + + + + + + PARALLEL EXECUTION (ALLEGRO) + DISJOINT STATE RUNS CONCURRENTLY, 2 BLOCKS OF WALL TIME + + + LANE 0 + LANE 1 + LANE 2 + + + + + tx 1 + + tx 4 + + + tx 2 + + tx 5 + + + tx 3 + + tx 6 + + + + + RECLAIMED BLOCKSPACE + diff --git a/public/blog/receive-policy-delivery-states.svg b/public/blog/receive-policy-delivery-states.svg new file mode 100644 index 00000000..2b90d049 --- /dev/null +++ b/public/blog/receive-policy-delivery-states.svg @@ -0,0 +1,35 @@ + + + + RECEIVE POLICY: THREE DELIVERY STATES + WHAT AN INTEGRATOR MUST MODEL + + + + INBOUND TRANSFER + + + + + + + + + + + + + FAILED + token checks revert, no transfer + + + + CREDITED + receiver accepts, funds arrive + + + + HELD + held by ReceivePolicyGuard + recoverable by the chosen authority + diff --git a/public/blog/reth-tps-benchmark.svg b/public/blog/reth-tps-benchmark.svg new file mode 100644 index 00000000..953d46c8 --- /dev/null +++ b/public/blog/reth-tps-benchmark.svg @@ -0,0 +1,45 @@ + + + + SUSTAINED TPS BY RELEASE + LIVE NETWORK, CONTINUOUS LOAD — PERF.TEMPO.XYZ + + + + + + + + + + 20K + 15K + 10K + 5K + + + + + + + + + 8,700 + v1.5 + + + + 12,800 + v1.6 + + + + 17,000 + v1.7 + + + + 21,200 + v1.8 + + diff --git a/src/marketing/BlogPostRoute.tsx b/src/marketing/BlogPostRoute.tsx new file mode 100644 index 00000000..f0d4d42f --- /dev/null +++ b/src/marketing/BlogPostRoute.tsx @@ -0,0 +1,18 @@ +'use client' + +import BlogPostPage from './app/blog/[slug]/page' +import MarketingRoute from './MarketingRoute' + +export default function BlogPostRoute({ + slug, + metadata, +}: { + slug: string + metadata: { title: string; description: string } +}) { + return ( + + + + ) +} diff --git a/src/marketing/MarketingRoute.tsx b/src/marketing/MarketingRoute.tsx index ed85e8c1..5cb2d971 100644 --- a/src/marketing/MarketingRoute.tsx +++ b/src/marketing/MarketingRoute.tsx @@ -40,6 +40,11 @@ const routeMetadata: Record = { title: 'Tempo Diagrams', description: 'A playground for Tempo diagrams, product visuals, and house-style SVG exports.', }, + '/blog': { + title: 'Blog — Tempo Developers', + description: + 'Engineering deep dives, network upgrades, events, and case studies from the Tempo team.', + }, } const prefetchedPaths = new Set() @@ -57,6 +62,8 @@ function prefetchPath(href: string) { document.head.appendChild(link) } +type RouteMetadata = { title: string; description: string } + function scheduleIdleAnalytics(callback: () => void) { let idleId: number | undefined const timeoutId = globalThis.setTimeout(() => { @@ -73,24 +80,26 @@ function scheduleIdleAnalytics(callback: () => void) { } } -function applyRouteMetadata(route: string) { - const metadata = routeMetadata[route] ?? routeMetadata['/'] - document.title = metadata.title - document.querySelector('meta[name="description"]')?.setAttribute('content', metadata.description) +function applyRouteMetadata(route: string, metadata?: RouteMetadata) { + const resolved = metadata ?? routeMetadata[route] ?? routeMetadata['/'] + document.title = resolved.title + document.querySelector('meta[name="description"]')?.setAttribute('content', resolved.description) } export default function MarketingRoute({ children, route, + metadata, }: { children: ReactNode - route: keyof typeof routeMetadata + route: string + metadata?: RouteMetadata }) { const [analyticsReady, setAnalyticsReady] = useState(false) useEffect(() => { - applyRouteMetadata(route) - }, [route]) + applyRouteMetadata(route, metadata) + }, [route, metadata]) useEffect(() => { return scheduleIdleAnalytics(() => setAnalyticsReady(true)) diff --git a/src/marketing/app/_components/BlogSection.tsx b/src/marketing/app/_components/BlogSection.tsx new file mode 100644 index 00000000..769dd1d6 --- /dev/null +++ b/src/marketing/app/_components/BlogSection.tsx @@ -0,0 +1,81 @@ +import Link from 'next/link' +import FeaturedVisual from '../blog/_components/FeaturedVisual' +import { formatDate } from '../blog/_lib/categories' +import { getAllPosts, getFeaturedPost } from '../blog/_lib/posts' +import EdgeMarkers from './EdgeMarkers' +import Reveal from './Reveal' + +export default function BlogSection() { + const posts = getAllPosts() + const featured = getFeaturedPost(posts) + const latest = posts.filter((p) => p.slug !== featured.slug).slice(0, 4) + + return ( +
+ +

+ Dive deeper into Tempo's engineering +

+

+ Engineering deep dives, network upgrades, events, and case studies{' '} + from the team building Tempo. +

+
+ + + +
+ +
+
+

+ {featured.title} +

+

+ {featured.excerpt} +

+

+ {formatDate(featured.date)} +

+
+ +
+ +
+ +
    + {latest.map((post, i) => ( +
  • + + + + {post.title} + + + + {formatDate(post.date)} + + + + +
  • + ))} +
+ + + View all blogs + + +
+
+ ) +} diff --git a/src/marketing/app/_components/Footer.tsx b/src/marketing/app/_components/Footer.tsx index f833ee82..c47307ab 100644 --- a/src/marketing/app/_components/Footer.tsx +++ b/src/marketing/app/_components/Footer.tsx @@ -83,6 +83,7 @@ const columns: FooterColumn[] = [ { header: 'Resources', links: [ + { label: 'Blog', href: '/blog' }, { label: 'Performance', href: '/performance' }, { label: 'Open source', href: '/#open-source' }, { label: 'Contact', href: CONTACT_URL }, diff --git a/src/marketing/app/_components/Header.tsx b/src/marketing/app/_components/Header.tsx index 09f6abea..536fcf8a 100644 --- a/src/marketing/app/_components/Header.tsx +++ b/src/marketing/app/_components/Header.tsx @@ -156,6 +156,7 @@ const menu: MenuItem[] = [ { label: 'Build', href: '/#protocol', mega: protocolMenu }, { label: 'Resources', href: '/docs', mega: developersMenu }, { label: 'Performance', href: '/performance' }, + { label: 'Blog', href: '/blog' }, { label: 'Docs', href: '/docs' }, ] diff --git a/src/marketing/app/_components/HomeBelowFold.tsx b/src/marketing/app/_components/HomeBelowFold.tsx index 126f4556..f887d438 100644 --- a/src/marketing/app/_components/HomeBelowFold.tsx +++ b/src/marketing/app/_components/HomeBelowFold.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react' import { fetchPerfRuns } from '../performance/_lib/runs' +import BlogSection from './BlogSection' import Footer from './Footer' import HomeShowcases from './HomeShowcases' import OpenSourceSection from './OpenSourceSection' @@ -54,6 +55,9 @@ export default function HomeBelowFold() {
+
+ +