diff --git a/.claude/diagrams.md b/.claude/diagrams.md new file mode 100644 index 0000000..5c30815 --- /dev/null +++ b/.claude/diagrams.md @@ -0,0 +1,75 @@ +# Diagrams + +When and how to draw diagrams in technical docs. + +## When to draw a diagram + +A diagram earns its place when it shows a topology, a flow, or a state machine that prose would describe more poorly. Decorative diagrams that just rephrase the surrounding paragraph are noise — cut them. + +Three patterns that genuinely need a diagram: +- **Topology** — components and their static relationships (architecture overviews, layered models). +- **Sequence** — message flow across actors over time, especially with branching. +- **State / pipeline** — staged process with conditional transitions. + +Anything else, prefer prose. + +## Tooling: SVG vs Mermaid vs static images + +**SVG (handcrafted)** — preferred for architecture diagrams. Predictable rendering at any container width, no plugin dependency, exact control over typography and color, diff-able as text. The cost is one-time authoring effort. + +**Mermaid** — convenient for quick draft diagrams, but rendering quality depends on the host site's CSS, container width, and theme. Text overflows boxes, edge labels truncate, and the rendering engine evolves between versions. For a docs site where rendering control matters, Mermaid is a footgun in production. + +**Static images (PNG/JPG)** — reasonable for screenshots, wireframes, and illustrations that aren't generated from code. Bad for architecture diagrams because they don't respond to dark/light theme, can't be edited by a contributor without the source file, and bloat the repo. + +Default to SVG for architecture and pipeline work. Use Mermaid only if the page is throwaway or the diagram is so simple its rendering is uncontroversial. + +## The draft → final pattern + +Mermaid (or even ASCII art) is a useful **drafting medium** even when the final diagram will be SVG. Mermaid lets you iterate on structure with the user quickly — boxes, arrows, branching — without paying SVG authoring cost up front. Once the structure is locked, convert to handcrafted SVG with the project's palette. + +The pattern: + +1. Sketch structure in Mermaid in chat or the markdown source. Get user agreement on what goes in the diagram and how it branches. +2. Author the SVG with the agreed structure plus the project's standard palette, marker, and viewBox. +3. Replace the Mermaid block with `![alt](/img/
/.svg)`. + +Skipping the draft step often produces SVGs that need to be re-authored after user feedback on structure. Keep the cheap iteration loop cheap. + +## Style consistency within a section + +Every diagram in a section should share: +- **Background**: transparent (`fill="none"` on the root ``) so dark-themed sites show through. +- **Stroke palette**: one neutral color for box and arrow strokes, never multiple competing colors. Reserve color for emphasis only. +- **Text hierarchy**: titles at one weight/size, subtitles at one weight/size lighter. Two tiers max. +- **Marker definition**: one shared arrowhead marker referenced by every line, not per-arrow inline definitions. +- **Stroke width**: consistent — typically 1.5px for everything. + +Inconsistency reads as low-effort. + +## Conditional vs sequential flows + +Solid lines are for the happy path. Dashed lines (typically `stroke-dasharray="5,5"`) are for conditional, alternative, or unresolved transitions. Label both — readers should not have to infer which is which. + +For state-machine diagrams, sequential transitions are solid; "if this fails" or "if disputed" branches are dashed. + +## Sequence diagrams + +For sequence diagrams done in raw SVG: +- Actors as boxes across the top, lifelines as dashed verticals running down. +- Messages as horizontal arrows with the message label centered above. +- Self-loops as a three-segment path going right, down, back to the lifeline with arrowhead. +- Branching alternatives separated by a dashed horizontal line, each branch labeled in italic. + +Don't try to mimic UML alt/opt boxes precisely — the visual overhead beats the clarity gain. + +## Sizing for site container + +Pick a viewBox sized for typical content container width. Most docs sites render content in a ~720–900px column. A diagram with viewBox `0 0 1000 500` will scale to ~80% in that container, shrinking text from 14pt to ~11pt — readable but tight. + +If text becomes hard to read at site width, the diagram has too much in it. Cut elements or split into two diagrams. Don't fight the container by making the SVG larger. + +## What to never put in a diagram + +- Constants and addresses that change between releases — they belong in the surrounding prose where a single edit fixes them. +- Long descriptive paragraphs in box labels — boxes hold names and short qualifiers, not sentences. +- Color-coded states without a legend — readers won't infer what red vs green means. diff --git a/.claude/voice.md b/.claude/voice.md new file mode 100644 index 0000000..4c23c3c --- /dev/null +++ b/.claude/voice.md @@ -0,0 +1,50 @@ +# Voice + +Tone and style rules for technical documentation written for engineers and auditors. + +## Target audience and tone + +Public technical docs are read by infra/protocol engineers, security auditors, and careful application developers. Write as a senior engineer who has shipped the system in question — direct, calm authority, no marketing flex. Not a tutorial author explaining concepts from first principles, not a developer advocate selling a feature. + +The reader does not need to be told they are about to read documentation. They do not need a "what this section covers" preamble. They need facts, mechanism, and the boundaries that hold the mechanism together. + +## Anti-patterns to strip + +These are the LLM tells that make documentation read as machine-generated. Strip every instance during writing or in a dedicated revision pass. + +1. **Meta-narration** — "this section explains…", "the point of this is…", "what matters here is…", "the important thing to understand…". +2. **Reflexive triads** — "X, Y, and Z" when one or two items would do; symmetric three-bullet lists at every section end. +3. **Filler hedges** — "it is worth noting", "it is important to understand", "one key thing", "the subtle thing to remember". +4. **False summaries** — "in essence", "ultimately", "in short", "simply put", "at a high level". +5. **Corporate abstractions** — "uniformly", "coherently", "seamlessly", "robustly". +6. **Hype adjectives** — "powerful", "elegant", "beautiful", "novel", "cutting-edge". +7. **Overused copulative patterns** — "X is what Y" where an active verb works; "the {noun} is {verb}ing {noun}" rhythm across consecutive sentences. +8. **Self-referential navigation** — "the rest of this section explains…", "this page walks you through…" when the sidebar already does that work. +9. **Pseudo-authoritative hedging** — "it should be noted that…", "one could argue that…", "generally speaking…". +10. **LLM signposts** — "let's explore…", "consider the following…", "imagine that…". +11. **Repetition for rhetoric** — saying the same point three times with different wording. Say it once. + +## Concrete grounding + +Narrative prose is fine; ungrounded narrative is not. Every meaningful claim should land on a concrete anchor — a constant name, an address, a method signature, a chain ID, a magic value. The anchor is what makes the page useful for reference; the prose is what makes it readable. + +Bad: "The bridge has a deposit deadline that admins can configure." +Better: "The L1-owned `_depositProcessingWindow` is snapshotted into each outbound deposit at send time and bounded at `MAX_DEPOSIT_PROCESSING_WINDOW = 50_400` blocks." + +If a fact comes from contract source, prefer the literal name (`commitBatch`, `_rollupCorrupted`, `FUEL_DENOM_RATE = 20`) over a paraphrase. Code rot beats narrative rot — when the constant is renamed, the diff is obvious. + +## ASCII conventions + +Use ASCII apostrophes (`'`) and ASCII quotes (`"`) throughout. Smart punctuation (`'`, `'`, `"`, `"`) breaks search, copy-paste of code-adjacent text, and creates noisy diffs when editors auto-correct. The exception is when the source you are quoting must be preserved character-faithful — then call it out explicitly. + +## Cross-link patterns + +A trailing "See [X]" link at the end of an entry is more useful than inline links scattered through prose. It signals "for the full picture, here". Inline links work when the linked page genuinely interrupts the current sentence ("the [interruption protocol](...) handles this"), but should be rare. + +Glossary-style entries should close with one canonical link to the deeper page. Never link to multiple destinations from a single entry — the reader picks none. + +## Length discipline + +Length is a budget, not a target. Every paragraph should earn its existence. Default question before adding a paragraph: "if I removed this, would the reader miss anything they couldn't get from the prior paragraph or a linked page?" If no, cut it. + +Short pages with high information density beat long pages padded with explanation. Treat 1500 words as the upper bound for a single architecture page; cut harder if you can. diff --git a/.claude/workflow.md b/.claude/workflow.md new file mode 100644 index 0000000..2fc39b3 --- /dev/null +++ b/.claude/workflow.md @@ -0,0 +1,61 @@ +# Workflow + +How to approach a docs task end-to-end. + +## Source-of-truth grounding before drafting + +Before writing prose, locate the authoritative source for what you are documenting. For protocol/contract docs that means the contract source itself, plus any upstream docs the team treats as canonical (often a separate repo synced locally for reference). Read the source first, draft second. + +If the authoritative source is in flux or contradicts the docs you are about to write, stop. Document divergence is worse than no documentation — it actively misleads. + +When the source lives outside the repo and is not committed (e.g. a local sync of an upstream docs/), reference that constraint explicitly so the agent knows facts come from there, not from the public site. + +## Voice as a separate revision pass + +Drafting and voice-polishing are different skills and should be different passes. First draft: get the facts right, get the structure right, hit the concrete anchors. Don't worry about LLM tells. + +Then run a dedicated **de-AI pass** with the anti-pattern list from `voice.md` open as a checklist. Strip meta-narration, hedges, false summaries, hype. This pass is voice-only — facts, structure, admonitions, links, diagrams, frontmatter all stay byte-identical. + +Combining the passes produces neither — facts get bent to fit the prose, prose gets bent to fit the facts. + +## Fact-check after voice pass + +A voice rewrite that touches every paragraph is exactly when you can accidentally weaken a claim ("could commit" → "commits") or drop a precision ("X is the load-bearing field" → "X is the field"). After de-AI, do a fact-check pass against the diff: is every concrete anchor preserved? Did any modal verb get stronger or weaker than the source supports? + +This is the cheapest place to catch drift. Once committed, drift compounds. + +## Build verification gate + +Run the site's build command after every meaningful change. For Docusaurus and similar generators, broken internal links and missing assets fail the build only when explicitly configured (`onBrokenLinks: "throw"`). Confirm that gate is on, then run the build before claiming any docs change is done. + +A build that "should work" because nothing structurally changed has lost the team an hour of bisecting later. Run it. + +## Single-concept commits + +A commit that adds a new page, fixes voice across three other pages, and renumbers the sidebar mixes three reviewable concerns into one diff. Split: + +- New content → its own commit. +- Voice/style revisions → its own commit. +- Mechanical reshuffles (sidebar positions, file renames) → either folded into the change that requires them, or their own commit. + +Reviewers evaluate one concern per commit; mixed commits force them to evaluate all three at once and approve none confidently. + +## Tone preservation when revising existing pages + +When editing a page someone else wrote, preserve their voice unless the user has asked for a global tone change. Resist the impulse to "improve" prose that already works just because it's not exactly how you would have written it. Editors who rewrite for taste burn maintainer trust faster than they add value. + +The exception is documented anti-patterns (LLM tells, false summaries, hype) — those are bugs, not stylistic preferences. + +## Cross-reference audit after adding or renaming content + +A new page or a renamed slug does not exist in isolation. After the change, walk through the predictable places that might need to point at it: + +- **Sidebar and navigation metadata** — does the new page have a `sidebar_position` that does not collide with siblings? Did renaming break a parent folder's category config? +- **Landing pages and discovery cards** — front pages, "what's next" cards, hero-section links often hardcode their targets. The new page is invisible if nothing on the entry-point pages routes a reader to it. +- **Glossary** — terms introduced in the new page may already have entries elsewhere; if not, consider adding short entries that point at the new page. Existing glossary entries on related concepts should mention the new page in their trailing "See [X]" link if it now offers a deeper home. +- **Adjacent topic pages** — pages on neighbouring topics often reference each other. A new page on topic X should be discoverable from pages on topic Y where a reader would naturally want the link. +- **External tooling that depends on URL stability** — search-index configs, redirect rules, social-share previews. A renamed slug breaks these silently because the build's broken-link checker only sees what is in the repo. + +The build's broken-link checker catches dead links. It does not catch *missing* links — places where a reader would expect a reference but none exists. Audit by hand: read the affected pages from the perspective of someone arriving at them via the most likely entry point, and ask whether they would discover the new content. + +For renames specifically, grep the entire `docs/` tree for the old slug before assuming it is only referenced where you remember it. diff --git a/.gitignore b/.gitignore index 1dfd16a..a593794 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,10 @@ yarn-debug.log* yarn-error.log* # IDE -.idea \ No newline at end of file +.idea + +# Claude Code +# Per-developer state stays local; shared standards (.claude/*.md) and CLAUDE.md are committed. +.claude/tasks/ +.claude/settings.local.json +fluentbase-docs/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a7ce7a7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,68 @@ +# Fluent Docs (Docusaurus) + +Public documentation site for Fluent, published at https://docs.fluent.xyz. Built with Docusaurus 3.9. + +## Build & Test + +- `npm run start` — local dev server on port 8000 +- `npm run build` — production build. **Required gate before commit**: fails on broken internal links via `onBrokenLinks: "throw"` in `docusaurus.config.js`. +- `npm run serve` — preview the production build locally +- `npm run clear` — clear Docusaurus cache when the build behaves oddly + +## On every task + +- Read `.claude/voice.md`, `.claude/workflow.md`, `.claude/diagrams.md` + +These are committed to the repo so every contributor (and their AI assistants) follows the same rules. If you maintain personal cross-project standards in `~/.claude/standards/docs/`, the in-repo files are the project-canonical version — drift from your personal copy is allowed and expected. + +This is a docs-only repository. Generic code standards are not auto-loaded — almost every task here is content/structure, not application code. If a task does touch `docusaurus.config.js`, custom plugins, or `src/` SCSS, ask whether broader code-style rules apply. + +### Site facts + +- **Published URL**: https://docs.fluent.xyz +- **Theme**: dark only — `colorMode.defaultMode: "dark"` and `disableSwitch: true` in `docusaurus.config.js`. Diagrams and content must read on a dark background. +- **Sidebar**: auto-generated from the `docs/` filesystem (`sidebars.js`). Ordering is controlled by `sidebar_position` frontmatter on individual pages and `_category_.json` `position` per folder. Verify positions don't collide with siblings when adding entries. +- **Source-of-truth for protocol/contract behavior**: `fluentbase-docs/` directory at repo root, mirrored from `https://github.com/fluentlabs-xyz/fluentbase/tree/devel/docs`. Not committed (gitignored). Use as the authoritative reference when documenting rollup, bridge, or runtime mechanics. + +### Filename and URL conventions + +- **Bare slugs only** for content files in `docs/` — no `NN-` numeric prefixes (e.g. `overview.md`, not `01-overview.md`). Sidebar order goes through frontmatter; the URL is the file slug. +- New section folder: include `_category_.json` with `label`, `position`, and `collapsed`. Verify the `position` against siblings. +- New top-level page: bump `sidebar_position` on trailing pages (glossary, resources, contribute) to keep numbering contiguous. + +### Diagrams + +- **Mermaid is installed** (`@docusaurus/theme-mermaid@3.9.2`, `markdown.mermaid: true`) but **not used** for architecture diagrams. Mermaid renders poorly at the site's container width — node and edge labels truncate. Use handcrafted SVG instead. +- SVGs live in `static/img/
/`, referenced from markdown as `/img/
/.svg`. +- Style palette tuned for this dark theme: stroke `#cccccc` 1.5px, primary text `#ffffff` font-weight 600, subtitles/metadata `#a0a0a0` (italic where appropriate), `fill="none"` on every shape so the dark background shows through. One shared arrowhead marker per file. +- Dashed `stroke-dasharray="5,5"` for conditional / unresolved transitions. Solid lines for the happy path. +- **Workflow for new diagrams**: sketch first in Mermaid (or ASCII art) for fast structural iteration with the user. Once the structure is approved, convert to handcrafted SVG with the palette above. Don't ship Mermaid — only use it as a draft medium because the live render at this site's container width truncates labels. +- See `~/.claude/standards/docs/diagrams.md` for general diagram principles. + +### Admonitions + +Whitelisted keywords (from `docusaurus.config.js:48-59`): `tip`, `prerequisite`, `warning`, `info`, `danger`, `best-practice`, `summary`. Use only when the admonition carries real meaning — not as cosmetic emphasis. + +### Cross-link conventions + +Internal links use relative `.md` paths. Within a section: `[Execution Model](./execution-model.md)`. Across sections: `[Blended 101](../knowledge-base/blended-101.md)`. Glossary entries close with one trailing `See [X]` link to the deeper page. + +### Cross-reference audit (after adding or renaming a page) + +The build's `onBrokenLinks: "throw"` catches dead links but not *missing* references. After every content change, walk these predictable hotspots in this repo: + +- `docs/get-started.md` — landing-page cards (table block + div-view-cards block) hardcode their targets. A new top-level section is invisible if nothing here routes to it. +- `docs/glossary.md` — terms introduced by the new page may already have entries; if not, consider adding one. Existing entries on adjacent concepts should point at the new page in their trailing `See [X]` link if it is now the deeper home. +- `docs/knowledge-base/blended-101.md` and `docs/knowledge-base/fluent-overview.md` — concept pages that frequently reference architecture pages. +- Sibling pages inside the same section — system-architecture pages cross-reference each other heavily. +- `_category_.json` files — when adding a new top-level folder, verify `position` against siblings to avoid Docusaurus tie-break ordering. + +When renaming a slug, `grep -rn "" docs/` before assuming you have caught every reference. + +See `.claude/workflow.md` for the underlying principle. + +## Security + +- Never read files listed in .gitignore, except `.claude/` directory +- Never read .env, .env.*, *.pem, *.key, *secret*, *credential* files +- If a task requires secrets — ask the human to provide only the specific value needed diff --git a/docs/glossary.md b/docs/glossary.md index 654ddcd..c08a1b9 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -31,7 +31,7 @@ Fluentbase is optimized for proving efficiency and integrates with modular compo ## Ownable Account -An ownable account is how Fluent attaches execution logic to an account without storing runtime bytecode inside every account that uses it. Every contract on Fluent lives in an account whose code field is a small wrapper carrying a magic header, an `owner_address` pointing to a delegated runtime, and runtime-specific metadata. When the account is called, REVM loads the executable code from the owner, not from the account itself — while keeping the original account as the storage target. +An ownable account is how Fluent attaches execution logic to an account without storing runtime bytecode inside every account that uses it. Most contracts on Fluent (Solidity, Universal Token) live in an account whose code field is a small wrapper carrying a magic header (`0xEF44`), an `owner_address` pointing to a delegated runtime, and runtime-specific metadata. When the account is called, REVM loads the executable code from the owner, not from the account itself — while keeping the original account as the storage target. Wasm contracts are stored differently: their compiled rWasm sits at the account directly (magic prefix `0xEF52`) without the wrapper indirection. This separation — account identity is local, execution logic is delegated — is what lets one state machine host EVM, Wasm, and SVM contracts together. A Solidity contract and a Rust contract end up as two ownable accounts pointing at two different delegated runtimes. They share the state trie, can call each other atomically, and the host mediates every privileged operation between them with the same rules. @@ -43,7 +43,7 @@ A delegated runtime is the execution code that an ownable account points at. Eac Delegated runtimes are protocol-owned: their bytecode lives at fixed system addresses, is installed and replaced through a governed upgrade path, and is shared by every account that opts into its execution class. A bug fix or behaviour change in the delegated EVM runtime affects every EVM contract on Fluent at once — which is why runtime upgrades are treated as fork-critical change management. -Delegated runtime addresses are not callable directly as normal contracts; the router blocks that path to keep user flows going through ownable-account semantics. See [Runtime Upgrade](system-architecture/runtime-upgrade.md). +Delegated runtime addresses are not callable directly as normal contracts; the router blocks that path to keep user flows going through ownable-account semantics. See [Runtime Routing and Ownable Accounts](system-architecture/runtime-routing-and-ownable-accounts.md). ## Interruption Protocol diff --git a/docs/system-architecture/bridge.md b/docs/system-architecture/bridge.md index a4038c1..2798e3b 100644 --- a/docs/system-architecture/bridge.md +++ b/docs/system-architecture/bridge.md @@ -1,6 +1,6 @@ --- title: Bridge Architecture -sidebar_position: 10 +sidebar_position: 11 --- Fluent's bridge is the two-way interface between the L2 and Ethereum. It carries two kinds of traffic under two different trust models: **deposits** (L1 → L2) are optimistic — they become spendable on the L2 once a rollup batch has consumed them — and **withdrawals** (L2 → L1) are Merkle-proven against the rollup's batch root. The bridge is a family of contracts deployed symmetrically on both chains, layered on top of the [rollup's batch lifecycle](./rollup-architecture.md). @@ -123,3 +123,44 @@ A few practical notes for operators: - `feeTreasury` must accept plain ETH transfers. The L2 outbound fee transfer uses a bare `call` — if the treasury address reverts on receive, `sendMessage` reverts with `FailedToDeductFee` and the user cannot bridge. - Rotate `RELAYER_ROLE` through the same operational process as the sequencer key. A compromised relayer cannot forge messages (the hash and proofs are public), but it can censor delivery order on L2 by stalling `receiveMessage`. - UUPS upgrades on the bridges, gateways, and safety registries are consensus-grade in the same sense as runtime upgrades (see [Runtime Upgrade](./runtime-upgrade.md)): deterministic artifacts, multisig authority, and coherent rollout. + +## Mainnet addresses + +Most bridge contracts use CREATE2 deterministic deployment — the same salt and bytecode produce the same address on L1 and L2. Implementations behind UUPS proxies are upgrade-target infrastructure and are not listed here. + +### L1 (Ethereum) + +| Contract | Address | +|---|---| +| FluentBridge | `0x9CAcf613fC29015893728563f423fD26dCdB8Ddc` | +| Native gateway | `0x8976Ca4E0c8467097Da675399fB7DB454a1b56dd` | +| ERC-20 gateway | `0xFD4C62647A34FF6d6802092F5fbe176099223B61` | +| Token factory | `0xF6d49E874Cb64b8ee56D6F99BD340134B30AB225` | +| Token factory beacon (UUPS) | `0xdd283a04cc711ab9c08d79e665835821beef710b` | +| WETH gateway (proxy) | `0x1e1f5Df9D48e8E88C037e9255a769e77c9fe587b` | +| FastWithdrawalList (proxy) | `0x3eFc3c84ecf259Da36E33692f2a107A0AB88D30E` | +| Blacklist (proxy) | `0x05C5d46a5e6f92fB9CdA9A8b03E4440A175D1484` | + +### L2 (Fluent, chainId 25363) + +| Contract | Address | +|---|---| +| FluentBridge | `0x9CAcf613fC29015893728563f423fD26dCdB8Ddc` | +| Native gateway | `0x8976Ca4E0c8467097Da675399fB7DB454a1b56dd` | +| ERC-20 gateway | `0xFD4C62647A34FF6d6802092F5fbe176099223B61` | +| Token factory | `0xF6d49E874Cb64b8ee56D6F99BD340134B30AB225` | +| L1BlockOracle | `0x19e1b30C792E417BC1827f5E2F288052b5c05e8F` | +| L1GasOracle | `0x207FBb4AC5227Ab598B8072BdC1E150dF687AC5B` | +| WETH gateway (proxy) | `0x1e1f5Df9D48e8E88C037e9255a769e77c9fe587b` | + +The L2 FluentBridge address coincides with `PRECOMPILE_ROLLUP_BRIDGE` (see [Precompiles](./precompiles/)) — the bridge is genesis-deployed on Fluent via `PRECOMPILE_ROLLUP_BRIDGE_DEPLOYER` using the same CREATE2 salt as the L1 deployment. + +Pegged tokens minted by the ERC-20 gateway on Fluent L2 use the [Universal Token runtime](./precompiles/universal-token-runtime.md) (`PRECOMPILE_UNIVERSAL_TOKEN_RUNTIME = 0x0000000000000000000000000000000000520008`) as their implementation — each pegged token is an ownable account pointing at that runtime. + +### Operator EOAs + +| Role | Address | +|---|---| +| Relayer (`RELAYER_ROLE`) | `0x4A0e88275dC08a15Bad0d12e7805574Ca0853A48` | +| L1BlockOracle submitter | `0xf1af41d33CfFdc8d08107713c0c2DF5De7f2Bd5c` | +| L1GasOracle submitter | `0x1Bee0BD77E76aD9692F6A3b4388DdE371b69fdD7` | diff --git a/docs/system-architecture/execution-model.md b/docs/system-architecture/execution-model.md index 802a5f5..c63fc94 100644 --- a/docs/system-architecture/execution-model.md +++ b/docs/system-architecture/execution-model.md @@ -62,6 +62,6 @@ Envelope decoding is part of the consensus surface. A malformed envelope or a mi ## The address map is part of consensus -One subtlety: the set of addresses that triggers system mode is fixed in shared constants. That set includes the delegated runtime owners for each supported VM family (EVM, Wasm, SVM, Universal Token) and the protocol-owned contracts that run under privilege — runtime upgrade, fee manager, bridge, and so on. +One subtlety: the set of addresses that triggers system mode is fixed in shared constants. That set includes the delegated runtime owners for each supported VM family (EVM, Wasm, SVM, Universal Token) and the protocol-owned contracts that run under privilege — runtime upgrade, fee manager, bridge, and so on. The full inventory is in [Precompiles](./precompiles/). Changing this map changes routing. Adding an address pulls a new runtime into the privileged set; removing one breaks every deployment that relied on it. That's why the address map is versioned at the protocol level and never modified as an incidental change. diff --git a/docs/system-architecture/gas-and-fuel.md b/docs/system-architecture/gas-and-fuel.md index afecb54..257ed8e 100644 --- a/docs/system-architecture/gas-and-fuel.md +++ b/docs/system-architecture/gas-and-fuel.md @@ -1,6 +1,6 @@ --- title: Gas and Fuel -sidebar_position: 5 +sidebar_position: 6 --- Fluent charges work in two accounting units. **Gas** is what users pay with — the same EVM-visible unit wallets quote, explorers display, and transactions settle in ETH. **Fuel** is what the rWasm runtime consumes while executing. Every runtime step is metered in fuel; every transaction is paid in gas; a fixed deterministic conversion links them. @@ -62,7 +62,7 @@ Not every system runtime meters fuel the same way. **Engine-metered runtimes** let the execution engine meter configured precompiles automatically. The runtime doesn't charge itself; the engine inserts fuel accounting at compilation time. -The Universal Token runtime is currently in the engine-metered set. This is a per-runtime policy decision, not a global behavior — which runtimes are engine-metered is part of the protocol's runtime classification and is versioned accordingly. +The Universal Token runtime is classified as engine-metered. This is a per-runtime policy decision, not a global behavior — which runtimes are engine-metered is part of the protocol's runtime classification and is versioned accordingly. The full classification table lives in [Precompiles](./precompiles/). ## Calldata surcharge diff --git a/docs/system-architecture/interruption-and-syscalls.md b/docs/system-architecture/interruption-and-syscalls.md index 535a64b..2d6b425 100644 --- a/docs/system-architecture/interruption-and-syscalls.md +++ b/docs/system-architecture/interruption-and-syscalls.md @@ -1,6 +1,6 @@ --- title: Interruption and Syscalls -sidebar_position: 4 +sidebar_position: 5 --- Runtime execution on Fluent isn't a single uninterrupted run. When a contract needs to read storage, emit an event, create a nested call, or touch shared state in any way, it doesn't do it. It yields back to the host, the host performs the operation, and the runtime resumes from where it stopped. That pattern — the **interruption protocol** — is how every privileged operation on Fluent is performed. diff --git a/docs/system-architecture/overview.md b/docs/system-architecture/overview.md index bd66bfc..ab50ebb 100644 --- a/docs/system-architecture/overview.md +++ b/docs/system-architecture/overview.md @@ -37,6 +37,7 @@ Day to day, app developers don't see this. You write Solidity or Rust, you deplo - [Execution Model](./execution-model.md) — normal call lifecycle, contract vs system modes, and the structured envelopes system runtimes use to hand state changes to the host. - [Runtime Routing and Ownable Accounts](./runtime-routing-and-ownable-accounts.md) — how one state machine hosts many execution environments without duplicating runtime logic per account. +- [Precompiles](./precompiles/) — the full set of system precompiles installed at genesis: delegated runtimes, system contracts, and standard EVM precompiles. - [Interruption and Syscalls](./interruption-and-syscalls.md) — the `exec` / `resume` handshake and the two syscall surfaces on top of it. - [Gas and Fuel](./gas-and-fuel.md) — why Fluent has two metering units and how they settle against each other. - [State and RPC Compatibility](./state-and-rpc-compatibility.md) — shared state, ownable-account wrapping, and the two RPC views the node exposes. diff --git a/docs/system-architecture/precompiles/_category_.json b/docs/system-architecture/precompiles/_category_.json new file mode 100644 index 0000000..5c972ef --- /dev/null +++ b/docs/system-architecture/precompiles/_category_.json @@ -0,0 +1,5 @@ +{ + "label": "Precompiles", + "position": 4, + "collapsed": true +} diff --git a/docs/system-architecture/precompiles/create2-factory.md b/docs/system-architecture/precompiles/create2-factory.md new file mode 100644 index 0000000..44b2464 --- /dev/null +++ b/docs/system-architecture/precompiles/create2-factory.md @@ -0,0 +1,35 @@ +--- +title: CREATE2 Factory +sidebar_position: 6 +--- + +A deterministic deployment proxy installed at the same address Fluent shares with most EVM-compatible chains. A deployer broadcasts a single transaction with init bytecode and a 32-byte salt; the factory deploys the contract via `CREATE2` and returns its address. Used for cross-chain address parity — identical bytecode plus identical salt yields the same address on any chain that has this factory at the same location. + +## Address + +| Constant | Address | +|---|---| +| `PRECOMPILE_CREATE2_FACTORY` | `0x4e59b44847b379578588920cA78FbF26c0B4956C` | +| `PRECOMPILE_CREATE2_FACTORY_DEPLOYER` | `0x3fab184622dc19b6109349b94811493bf2a45362` | + +## Provenance + +This is [Arachnid's deterministic deployment proxy](https://github.com/Arachnid/deterministic-deployment-proxy). Fluent embeds it at genesis using the same bytecode and the same address as Ethereum mainnet, so addresses derived against this factory match on all chains that include it. The factory has no Rust contract crate — the artifacts are checked-in Yul source plus the compiled binary at `contracts/create2-factory/`. + +## Calling convention + +The factory is a Yul-level contract with no Solidity ABI. Calldata format: + +```text +| 32 bytes salt | N bytes init code | +``` + +The factory hashes the calldata, runs `CREATE2(value=msg.value, salt, init_code)`, and returns the deployed address. Reverts if `CREATE2` fails. + +## Storage representation + +The factory is an EVM contract — its on-chain state is `Bytecode::OwnableAccount(owner = PRECOMPILE_EVM_RUNTIME, metadata = EthereumMetadata payload)`. The payload wraps the 73-byte Yul-compiled bytecode. Calls to the factory dispatch through the EVM runtime like any other Solidity contract — see [EVM Runtime](./evm-runtime.md). + +## Source + +`fluentbase/contracts/create2-factory/` — `deterministic-deployment-proxy.bin` (binary), `deterministic-deployment-proxy.yul` (source). diff --git a/docs/system-architecture/precompiles/eip2935.md b/docs/system-architecture/precompiles/eip2935.md new file mode 100644 index 0000000..e47e855 --- /dev/null +++ b/docs/system-architecture/precompiles/eip2935.md @@ -0,0 +1,39 @@ +--- +title: EIP-2935 Block Hash Service +sidebar_position: 7 +--- + +A ring buffer of historical block hashes accessible from contracts. Implements [EIP-2935](https://eips.ethereum.org/EIPS/eip-2935) — the Prague-era replacement for the `BLOCKHASH` opcode that lets contracts read further back than 256 blocks. + +## Address + +| Constant | Address | +|---|---| +| `PRECOMPILE_EIP2935` | `0x0000F90827F1C53a10cb7A02335B175320002935` | + +## Behavior + +Two paths gated by caller: + +- **Read path** (any caller). Calldata is a 32-byte big-endian block number. Returns the 32-byte block hash from the ring slot `block_number % 8191`. Block numbers older than the window or greater than `current_block - 1` revert. +- **Write path** (only callable by the system address `0xfffffffffffffffffffffffffffffffffffffffe`). The protocol calls into this path at every block transition to store the new hash at slot `(current_block - 1) % 8191`. + +The window is fixed at `EIP2935_HISTORY_SERVE_WINDOW = 8191` slots. + +## Gas + +Gas charges are precise per-branch, matching the EVM control flow specified by the EIP. Each value is in EVM gas units; fuel is charged as `gas * FUEL_DENOM_RATE`. + +| Branch | EVM gas | +|---|---| +| Bad input length | 47 | +| Future block (`block_number > current - 1`) | 79 | +| Block too old (`block_number < current - 8191`) | 106 | +| Successful read | 125 | +| Successful write (system caller) | 43 | + +A successful read costs 125 gas = 2,500 fuel. + +## Source + +`fluentbase/contracts/eip2935/` diff --git a/docs/system-architecture/precompiles/evm-runtime.md b/docs/system-architecture/precompiles/evm-runtime.md new file mode 100644 index 0000000..a986c12 --- /dev/null +++ b/docs/system-architecture/precompiles/evm-runtime.md @@ -0,0 +1,40 @@ +--- +title: EVM Runtime +sidebar_position: 1 +--- + +The delegated EVM runtime dispatches calls and creates targeting Solidity-style contracts on Fluent. The address itself holds rWasm bytecode that wraps an EVM interpreter; every Solidity contract on Fluent is an ownable account whose `owner_address` field points here. + +## Address + +| Constant | Address | +|---|---| +| `PRECOMPILE_EVM_RUNTIME` | `0x0000000000000000000000000000000000520001` | + +## Routing + +Init code with no recognised magic prefix routes to this runtime — it is the default class. The dispatch rule lives in [Runtime Routing and Ownable Accounts](../runtime-routing-and-ownable-accounts.md). Direct calls to the runtime address are rejected by the executor's `is_delegated_runtime_address` check. + +## Behavior + +The runtime exposes two entry points used by REVM: + +- `deploy_entry` runs at account creation. It runs the init code under the EVM interpreter, validates the deployed bytecode against EIP-3541 (no `0xEF` prefix) and EIP-170 (≤ 24 KB), charges `CODEDEPOSIT` per output byte, and writes the resulting deployed bytecode as `EthereumMetadata::Analyzed` into the account's ownable-account metadata. +- `main_entry` runs on every subsequent call. It reads the metadata, decodes it as `EthereumMetadata`, runs the EVM interpreter against the call input, syncs gas and fuel via `FUEL_DENOM_RATE = 20` at every interruption boundary, and returns the EVM-shaped result. + +Nested calls and host operations (storage reads, balance queries, log emission) flow through the [interruption protocol](../interruption-and-syscalls.md) like any other system runtime. EVM bytecode is never the executing rWasm bytecode — it is always carried as metadata under the ownable-account wrapper. + +## Storage representation + +Solidity contracts deploy as `Bytecode::OwnableAccount(owner = PRECOMPILE_EVM_RUNTIME, metadata = serialized EthereumMetadata)`. The wrapper layout is `0xEF44 || 0x00 || owner_address (20 bytes) || metadata (N bytes)`. The metadata payload begins with a 32-byte version header — `ETHEREUM_METADATA_VERSION_ANALYZED` signals an analyzed bytecode payload (bincode-encoded `(hash, len, padded_bytecode, jump_table)`); other values are treated as a legacy code hash followed by raw bytecode. + +## Errors + +- `CreateContractStartingWithEF` — deployed bytecode begins with `0xEF` (EIP-3541 violation). +- `CreateContractSizeLimit` — deployed bytecode exceeds 24 KB (EIP-170 violation). +- `OutOfFuel` — `CODEDEPOSIT` charge fails or call-level fuel exhaustion. +- Mapped EVM instruction results — standard EVM revert/halt classes propagated from the interpreter. + +## Source + +`fluentbase/contracts/evm/` diff --git a/docs/system-architecture/precompiles/fee-manager.md b/docs/system-architecture/precompiles/fee-manager.md new file mode 100644 index 0000000..e51b982 --- /dev/null +++ b/docs/system-architecture/precompiles/fee-manager.md @@ -0,0 +1,55 @@ +--- +title: Fee Manager +sidebar_position: 5 +--- + +The system contract that holds accumulated protocol fees. The owner can withdraw the contract's full balance to a recipient address. There is no other operation; fees are paid into this address by the protocol and accumulate until withdrawn. + +## Address + +| Constant | Address | +|---|---| +| `PRECOMPILE_FEE_MANAGER` | `0x0000000000000000000000000000000000520fee` | + +## Authority + +| Constant | Default address | +|---|---| +| `DEFAULT_FEE_MANAGER_AUTH` | `0xa7bf6a9168fe8a111307b7c94b8883fe02b30934` | + +The owner defaults to the genesis-init value. Live networks rotate it. The contract reads its stored owner first and falls back to the default only when the slot is zero. `changeOwner(address)` transfers ownership (zero-address rejected); `renounceOwnership()` sets the owner to the system address (`0xfffffffffffffffffffffffffffffffffffffffe`). + +## Interface + +```solidity +function withdraw(address recipient) external; +function changeOwner(address newOwner) external; +function owner() external view returns (address); +function renounceOwnership() external; +``` + +`withdraw` is owner-only and transfers the contract's full balance to `recipient`. Reverts if the contract balance is zero (`fee-manager: nothing to withdraw`). + +## Events + +```solidity +event FeeWithdrawn(address recipient, uint256 amount); +event OwnerChanged(address newOwner); +``` + +## Errors + +All failure paths use Rust `panic!` and surface as EVM revert. Messages: + +- `fee-manager: incorrect caller` +- `fee-manager: nothing to withdraw` +- `fee-manager: can't send funds to recipient` +- `fee-manager: can't obtain self balance` + +## Routing note + +`PRECOMPILE_FEE_MANAGER` is not in `EXECUTE_USING_SYSTEM_RUNTIME_ADDRESSES`. The contract is pre-deployed at genesis and executes as a normal rWasm contract — no system-runtime envelope, no per-runtime storage prefetch. Standard `CALL` semantics apply. + +## Source + +`fluentbase/contracts/fee-manager/` diff --git a/docs/system-architecture/precompiles/index.md b/docs/system-architecture/precompiles/index.md new file mode 100644 index 0000000..a935882 --- /dev/null +++ b/docs/system-architecture/precompiles/index.md @@ -0,0 +1,91 @@ +--- +title: Precompiles +sidebar_position: 0 +--- + +Fluent installs a fixed set of system precompiles at genesis. Each entry below is a callable address with a specific role: dispatching execution to a delegated runtime, verifying a cryptographic primitive, gating a privileged operation, or providing a standard EVM service. The constants come from `fluentbase/crates/types/src/genesis.rs`; the contract code that backs each address lives in `fluentbase/contracts//`. + +User contracts on Fluent — Solidity, Rust/Wasm, Universal Token — route their execution into one of the delegated runtime dispatchers below. Solidity and Universal Token contracts are stored as **ownable accounts** (`0xEF44` magic prefix) that hold a pointer to the dispatcher plus runtime metadata; Wasm contracts are stored as compiled rWasm directly (`0xEF52` magic prefix) after the executor's deploy-time rewrite. The dispatcher's address holds the runtime bytecode either way. See [Runtime Routing and Ownable Accounts](../runtime-routing-and-ownable-accounts.md) for the full mechanism. + +Addresses are identical across testnet, mainnet, and devnet. Authority addresses default to the values shown below at genesis, but live networks rotate them — treat the defaults as starting points, not current values. + +:::tip +On-chain state for any precompile address can be inspected on [Fluentscan](https://fluentscan.xyz). The protocol source-of-truth for these constants is `fluentbase/crates/types/src/genesis.rs`. +::: + +## Delegated runtime dispatchers + +Calls to these addresses are not handled by deployed bytecode at the address itself. They are dispatchers: the executor loads the runtime's code, runs it, and the call's storage domain stays at the caller's account. See [Runtime Routing and Ownable Accounts](../runtime-routing-and-ownable-accounts.md) for the mechanism. + +| Address | Description | +|---|---| +| `0x0000000000000000000000000000000000520001` | EVM runtime — see [EVM Runtime](./evm-runtime.md) | +| `0x0000000000000000000000000000000000520003` | SVM runtime — reserved, feature-gated, not currently active in mainnet build | +| `0x0000000000000000000000000000000000520008` | Universal Token runtime — see [Universal Token Runtime](./universal-token-runtime.md) | +| `0x0000000000000000000000000000000000520009` | WASM runtime — see [WASM Runtime](./wasm-runtime.md) | + +## Verifier precompiles + +Reserved addresses for cryptographic verification primitives that have not yet been activated. Each address has stub bytecode installed at genesis but a call returns an unreachable-code error today. Documentation will follow activation. + +| Address | Description | +|---|---| +| `0x0000000000000000000000000000000000520005` | WebAuthn verifier — reserved, not yet active | +| `0x0000000000000000000000000000000000520006` | OAuth2 verifier — reserved, not yet active | +| `0x0000000000000000000000000000000000520007` | Nitro verifier — reserved, not yet active | + +## System contracts + +Privileged contracts that gate runtime upgrades, fee withdrawal, deterministic deployment, and cross-domain settlement. Each has a Solidity-style ABI and an authority owner. + +| Address | Description | +|---|---| +| `0x0000000000000000000000000000000000520010` | Runtime upgrade — replaces the bytecode of a delegated runtime. See [Runtime Upgrade Precompile](./runtime-upgrade.md) | +| `0x0000000000000000000000000000000000520fee` | Fee manager — withdraws accumulated protocol fees. See [Fee Manager](./fee-manager.md) | +| `0x9CAcf613fC29015893728563f423fD26dCdB8Ddc` | Rollup bridge — cross-chain settlement. See [Bridge Architecture](../bridge.md) | +| `0x4e59b44847b379578588920cA78FbF26c0B4956C` | CREATE2 factory — deterministic deployment proxy. See [CREATE2 Factory](./create2-factory.md) | + +## EIP-deployed system contracts + +EIP-track precompiles deployed at protocol-specified addresses. Implementations live in `fluentbase/contracts/` like the rest, but their addresses are dictated by the EIP, not by Fluent's `0x520xxx` reservation range. + +| Address | Description | +|---|---| +| `0x0000F90827F1C53a10cb7A02335B175320002935` | EIP-2935 — historical block-hash ring buffer (Prague). See [EIP-2935 Block Hash Service](./eip2935.md) | +| `0x0000000000000000000000000000000000000100` | EIP-7951 — secp256r1 (P-256) signature verification. See [secp256r1 Signature Verification](./secp256r1.md) | + +## Standard EVM precompiles + +Fluent ships every standard Ethereum precompile from `0x01` to `0x11` — ecrecover, sha256, ripemd160, identity, modular exponentiation, BN254 curve operations, BLAKE2 compression, KZG point evaluation, and the BLS12-381 family. See [Standard EVM Precompiles](./standard-evm-precompiles.md) for the full list and any Fluent-specific notes. + +## Authority addresses + +Two privileged keys exist at genesis. Both default to `0xa7bf6a9168fe8a111307b7c94b8883fe02b30934` and are intended to be rotated immediately on a live chain (typically to a multisig). The defaults are documented for reference, not as current operational values. + +- **Runtime upgrade owner** (constant `DEFAULT_UPDATE_GENESIS_AUTH`) — initial caller permitted to invoke `upgradeTo` on the [Runtime Upgrade Precompile](./runtime-upgrade.md). +- **Fee manager owner** (constant `DEFAULT_FEE_MANAGER_AUTH`) — initial caller permitted to invoke `withdraw` on the [Fee Manager](./fee-manager.md). + +Both contracts read their stored owner first and only fall back to the default when the slot is zero. The constants live in `fluentbase/crates/types/src/genesis.rs`. + +## Bytecode model + +Every account on Fluent stores its code as one of four `Bytecode` variants. Knowing which variant a contract uses determines whether metadata syscalls work, whether REVM dispatches through an owner address, and what the leading bytes of `account.code` look like. + +| Variant | Magic prefix | Used by | +|---|---|---| +| `LegacyAnalyzed` | (no Fluent magic) | Plain analyzed EVM bytecode (legacy chain accounts). | +| `Eip7702` | `0xEF01` | EIP-7702 account-code delegation (EOAs that delegate to a contract). | +| `OwnableAccount` | `0xEF44` | Solidity contracts (owner = EVM Runtime), Universal Token contracts (owner = UT Runtime), and the EVM-style system precompiles. | +| `Rwasm` | `0xEF52` | Wasm contracts (after the deploy-time wrapper rewrite), and system contracts pre-deployed at genesis as raw rWasm (Runtime Upgrade, Fee Manager, EIP-2935, secp256r1, etc.). | + +`OwnableAccount` carries `owner_address` plus runtime-specific `metadata` bytes; the runtime is dispatched through on every call. `Rwasm` accounts execute their bytes directly with no owner-address indirection. + +## Magic bytes reference + +| Constant | Value | Module | +|---|---|---| +| `WASM_MAGIC_BYTES` | `0x0061736d` (standard `\0asm`) | `crates/types/src/lib.rs` | +| `UNIVERSAL_TOKEN_MAGIC_BYTES` | `0x45524320` (`"ERC "`) | `crates/types/src/lib.rs` | +| `RWASM_MAGIC_BYTES` | `0xef52` | `revm-bytecode/.../rwasm.rs` | +| `OWNABLE_ACCOUNT_MAGIC_BYTES` | `0xef44` | `revm-bytecode/.../ownable_account.rs` | +| `EIP7702_MAGIC_BYTES` | `0xef01` | `revm-bytecode/.../eip7702.rs` | diff --git a/docs/system-architecture/precompiles/runtime-upgrade.md b/docs/system-architecture/precompiles/runtime-upgrade.md new file mode 100644 index 0000000..1ae26bc --- /dev/null +++ b/docs/system-architecture/precompiles/runtime-upgrade.md @@ -0,0 +1,67 @@ +--- +title: Runtime Upgrade Precompile +sidebar_position: 4 +--- + +The contract-side of Fluent's runtime upgrade flow. The conceptual mechanism — governance owner, host enforcement, the privileged syscall — is documented in [Runtime Upgrade](../runtime-upgrade.md). + +## Address + +| Constant | Address | +|---|---| +| `PRECOMPILE_RUNTIME_UPGRADE` | `0x0000000000000000000000000000000000520010` | + +## Authority + +| Constant | Default address | +|---|---| +| `DEFAULT_UPDATE_GENESIS_AUTH` | `0xa7bf6a9168fe8a111307b7c94b8883fe02b30934` | + +The owner is initialized to the default at genesis. Live networks rotate it to a multisig immediately — treat the default as a starting point, not the current value. The contract reads its stored owner first and falls back to the default only when the slot is zero. Ownership transfer goes through `changeOwner(address)` (zero-address rejected); `renounceOwnership()` sets the owner to the system address (`0xfffffffffffffffffffffffffffffffffffffffe`), freezing future upgrades through the owner-based path. + +## Interface + +Solidity-style 4-byte selector dispatch. + +```solidity +function upgradeTo(address target, uint256 genesisHash, string genesisVersion, bytes wasmBytecode) external; +function changeOwner(address newOwner) external; +function owner() external view returns (address); +function renounceOwnership() external; +``` + +The function selector for `upgradeTo` is published as a constant: + +```text +UPDATE_GENESIS_PREFIX = 0x288fb3b8 = keccak256("upgradeTo(address,uint256,string,bytes)")[:4] +``` + +`upgradeTo` validates the input WASM starts with the WASM magic preamble, compiles it to rWasm via `compile_rwasm_maybe_system`, invokes `SYSCALL_ID_UPGRADE_RUNTIME` with the target address and serialized rWasm, then reads the installed code hash and emits `RuntimeUpgraded`. Failure at any step panics with a specific message that surfaces as an EVM revert — see Errors. + +## Events + +```solidity +event RuntimeUpgraded(address indexed targetAddress, bytes32 indexed genesisHash, string genesisVersion, bytes32 codeHash); +event OwnerChanged(address newOwner); +``` + +`RuntimeUpgraded` is emitted on every successful `upgradeTo` after the host installs the new bytecode at `targetAddress`; `codeHash` is the hash of the installed bytecode. `OwnerChanged` is emitted by `changeOwner` and `renounceOwnership`. + +## Errors + +All failure paths use Rust `panic!` and surface as EVM revert. Messages: + +- `runtime-upgrade: incorrect caller` +- `runtime-upgrade: malformed wasm bytecode` +- `runtime-upgrade: failed to compile bytecode` +- `runtime-upgrade: failed to upgrade` +- `runtime-upgrade: can't obtain code hash` +- `runtime-upgrade: can't set owner to zero address` + +## Routing note + +`PRECOMPILE_RUNTIME_UPGRADE` is not in `EXECUTE_USING_SYSTEM_RUNTIME_ADDRESSES`. The contract is pre-deployed at genesis and executes as a normal rWasm contract — no system-runtime envelope, no per-runtime storage prefetch. Standard `CALL` semantics apply. + +## Source + +`fluentbase/contracts/runtime-upgrade/`. CLI driver: `fluentbase/bins/runtime-upgrade/main.rs`. diff --git a/docs/system-architecture/precompiles/secp256r1.md b/docs/system-architecture/precompiles/secp256r1.md new file mode 100644 index 0000000..5843409 --- /dev/null +++ b/docs/system-architecture/precompiles/secp256r1.md @@ -0,0 +1,39 @@ +--- +title: secp256r1 (P-256) Signature Verification +sidebar_position: 8 +--- + +A signature verification precompile for the NIST P-256 curve (secp256r1). Tracked as [EIP-7951](https://eips.ethereum.org/EIPS/eip-7951) on the EIP track and [RIP-7212](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md) on the rollup track. Same curve used by WebAuthn, FIDO2, and most non-Ethereum chains for ECDSA — distinct from secp256k1, which underlies Ethereum's `ecrecover`. + +## Address + +| Constant | Address | +|---|---| +| `PRECOMPILE_EIP7951` | `0x0000000000000000000000000000000000000100` | + +## Calling convention + +Input: 160 bytes total, big-endian. + +```text +| msgHash (32) | r (32) | s (32) | pubKeyX (32) | pubKeyY (32) | +``` + +Output: +- Valid signature: 1 byte `0x01`. +- Invalid signature, malformed coordinates, or input length other than 160 bytes: empty bytes. + +Invalid signatures and malformed coordinates are **not** errors — the precompile returns success with empty output. The contract is a thin wrapper around `revm_precompile::secp256r1::p256_verify`. + +## Gas + +Flat cost per call: `P256VERIFY_BASE_GAS_FEE = 3,450 gas`. Charged unconditionally before verification runs. + +## Errors + +- `OutOfFuel` — `revm_precompile` returned `PrecompileError::OutOfGas`. +- `PrecompileError` — any other underlying precompile error. + +## Source + +`fluentbase/contracts/eip7951/`. Underlying implementation: `revm_precompile::secp256r1`. diff --git a/docs/system-architecture/precompiles/standard-evm-precompiles.md b/docs/system-architecture/precompiles/standard-evm-precompiles.md new file mode 100644 index 0000000..91ec752 --- /dev/null +++ b/docs/system-architecture/precompiles/standard-evm-precompiles.md @@ -0,0 +1,37 @@ +--- +title: Standard EVM Precompiles +sidebar_position: 9 +--- + +Fluent ships every standard Ethereum precompile at addresses `0x01` through `0x11`. Each is implemented as a system contract that wraps the corresponding `revm_precompile` function — semantics are byte-for-byte identical to mainnet Ethereum, with one minor implementation note in BN254. + +| Address | Description | +|---|---| +| `0x0000000000000000000000000000000000000001` | `ecrecover` — secp256k1 signature recovery (Frontier) | +| `0x0000000000000000000000000000000000000002` | SHA-256 hash (Frontier) | +| `0x0000000000000000000000000000000000000003` | RIPEMD-160 hash (Frontier) | +| `0x0000000000000000000000000000000000000004` | Identity / data copy (Frontier) | +| `0x0000000000000000000000000000000000000005` | Modular exponentiation — EIP-198 (Berlin gas schedule) | +| `0x0000000000000000000000000000000000000006` | BN254 G1 addition — EIP-196 | +| `0x0000000000000000000000000000000000000007` | BN254 G1 scalar multiplication — EIP-196 | +| `0x0000000000000000000000000000000000000008` | BN254 pairing check — EIP-197 | +| `0x0000000000000000000000000000000000000009` | BLAKE2 F compression — EIP-152 | +| `0x000000000000000000000000000000000000000a` | KZG point evaluation — EIP-4844 | +| `0x000000000000000000000000000000000000000b` | BLS12-381 G1 addition — EIP-2537 | +| `0x000000000000000000000000000000000000000c` | BLS12-381 G1 multi-scalar multiplication — EIP-2537 | +| `0x000000000000000000000000000000000000000d` | BLS12-381 G2 addition — EIP-2537 | +| `0x000000000000000000000000000000000000000e` | BLS12-381 G2 multi-scalar multiplication — EIP-2537 | +| `0x000000000000000000000000000000000000000f` | BLS12-381 pairing — EIP-2537 | +| `0x0000000000000000000000000000000000000010` | BLS12-381 map FP → G1 — EIP-2537 | +| `0x0000000000000000000000000000000000000011` | BLS12-381 map FP² → G2 — EIP-2537 | + +Implementations live under `fluentbase/contracts//`, where `` matches the precompile: `ecrecover`, `sha256`, `ripemd160`, `identity`, `modexp`, `bn256`, `blake2f`, `kzg`, `bls12381`. Address constants are defined in `fluentbase/crates/types/src/genesis.rs` as `PRECOMPILE_` via `Address::with_last_byte(0xNN)`. + +## Notes on implementation + +- **BN254 (BN256) endianness**: the implementation converts between Ethereum's big-endian inputs and SP1's little-endian internal representation at the boundary (`bn256/src/lib.rs` — `point_be_to_le` / `point_le_to_be`). The external interface remains EIP-196/197 compliant; the conversion is invisible to callers. +- **One contract crate, several addresses**: the BN254 family shares one `bn256` contract crate dispatched by caller address; the BLS12-381 family shares one `bls12381` crate the same way. The genesis build installs the same compiled rWasm at every address in each family. + +## Source + +Implementations delegate to `revm_precompile` for the underlying math, so any divergence from mainnet semantics would be visible in the wrapper code. diff --git a/docs/system-architecture/precompiles/universal-token-runtime.md b/docs/system-architecture/precompiles/universal-token-runtime.md new file mode 100644 index 0000000..4f7c362 --- /dev/null +++ b/docs/system-architecture/precompiles/universal-token-runtime.md @@ -0,0 +1,142 @@ +--- +title: Universal Token Runtime +sidebar_position: 3 +--- + +A shared ERC-20 implementation that every Universal Token deployment routes through. One contract, many tokens — each token is an ownable account pointing at this runtime, with its name, symbol, decimals, balances, and (optional) wrapped-token state stored under the account's own address. + +## Address + +| Constant | Address | +|---|---| +| `PRECOMPILE_UNIVERSAL_TOKEN_RUNTIME` | `0x0000000000000000000000000000000000520008` | + +## Routing + +Init code starting with `UNIVERSAL_TOKEN_MAGIC_BYTES = 0x45524320` (ASCII `"ERC "`) routes to this runtime at deploy time. Direct calls to the runtime address are rejected; user contracts reach it only via the ownable-account indirection. + +## Constructor payload + +Deployment input is the magic prefix followed by the SolidityABI-encoded `InitialSettings` struct: + +```text +0x45524320 ++ abi.encode(token_name, token_symbol, decimals, initial_supply, minter, pauser, wrapped) +``` + +| Field | ABI type | Notes | +|---|---|---| +| `token_name` | `bytes32` | Fixed 32-byte field, not a `string` (see below). | +| `token_symbol` | `bytes32` | Fixed 32-byte field. | +| `decimals` | `uint8` | Standard ERC-20 decimals. | +| `initial_supply` | `uint256` | Credited to deployer if non-zero. | +| `minter` | `address` | Stored even when zero; gates `mint` / `burn`. | +| `pauser` | `address` | Stored even when zero; gates `pause` / `unpause`. | +| `wrapped` | `bool` | Marks this token as a wrapped-asset deployment that gates `deposit` / `withdraw`. | + +Wrapped tokens cannot have a non-zero `minter` — deploys with `wrapped == true && minter != address(0)` are rejected with `ERR_UST_NOT_MINTABLE`. + +### `bytes32` name and symbol — not `string` + +Name and symbol are 32-byte fixed values. The runtime reads the leading bytes of each `bytes32`, stops at the first `0x00`, and validates UTF-8 (malformed input is rejected with `MalformedBuiltinParams`). Encode human-readable text in the leading bytes; leave the tail zero-padded. + +```solidity +function stringToBytes32(string memory s) internal pure returns (bytes32 out) { + require(bytes(s).length <= 32, "string too long"); + assembly { + out := mload(add(s, 32)) + } +} +``` + +## Deploy behavior + +On a successful deploy the runtime: + +- stores `name`, `symbol`, `decimals`, `minter`, `pauser`, and the `wrapped` flag in account metadata, +- if `initial_supply > 0`: credits the deployer's balance, sets `totalSupply`, and emits `Transfer(address(0), deployer, initial_supply)`. + +A minimal Solidity-side deploy: + +```solidity +bytes4 constant UNIVERSAL_TOKEN_MAGIC = 0x45524320; // "ERC " + +bytes memory initCode = bytes.concat( + UNIVERSAL_TOKEN_MAGIC, + abi.encode(name32, symbol32, decimals, initialSupply, minter, pauser, wrapped) +); + +address token; +assembly { token := create(0, add(initCode, 0x20), mload(initCode)) } +``` + +`CREATE2` works the same way with a salt argument. + +## Interface + +Selectors are dispatched Solidity-style by 4-byte function ID. The runtime exposes the standard ERC-20 surface: + +| Selector | Returns | +|---|---| +| `name()` | `string` | +| `symbol()` | `string` | +| `decimals()` | `uint8` | +| `totalSupply()` | `uint256` | +| `balanceOf(address)` | `uint256` | +| `transfer(address,uint256)` | `bool` | +| `transferFrom(address,address,uint256)` | `bool` | +| `approve(address,uint256)` | `bool` | +| `allowance(address,address)` | `uint256` | + +Plus one Fluent-specific helper: + +| Selector | Returns | +|---|---| +| `balance()` | `uint256` (caller's balance — equivalent to `balanceOf(msg.sender)`) | + +Privileged selectors active only when the corresponding role was set at deploy: + +| Selector | Role required | +|---|---| +| `mint(address,uint256)` | `minter` | +| `burn(address,uint256)` | `minter` | +| `pause()` | `pauser` | +| `unpause()` | `pauser` | + +Wrapped-asset selectors active only on tokens deployed with `wrapped == true`: + +| Selector | Behavior | +|---|---| +| `deposit()` | Mints the caller a wrapped balance equal to the `msg.value` sent. | +| `withdraw(uint256 wad)` | Burns the caller's wrapped balance and transfers the underlying value back. | + +Unknown selectors return `ERR_UST_UNKNOWN_METHOD`. + +## Events + +```solidity +event Transfer(address indexed from, address indexed to, uint256 amount); +event Approval(address indexed owner, address indexed spender, uint256 amount); +event Paused(address pauser); +event Unpaused(address pauser); +event Deposit(address indexed dst, uint256 wad); +event Withdrawal(address indexed src, uint256 wad); +``` + +## Errors + +Role and state checks return distinct error classes: + +- `ERR_UST_NOT_MINTABLE`, `ERR_UST_MINTER_MISMATCH` — mint or burn against a token deployed without a minter, or by the wrong caller. +- `ERR_UST_NOT_PAUSABLE`, `ERR_UST_PAUSER_MISMATCH` — pause or unpause against a non-pausable token, or by the wrong caller. +- `ERR_UST_NOT_WRAPPED` — `deposit` / `withdraw` against a token deployed without `wrapped == true`. +- `ERR_PAUSABLE_ENFORCED_PAUSE`, `ERR_PAUSABLE_EXPECTED_PAUSE` — pause when already paused, unpause when not paused, or transfer / mint / burn while paused. +- `ERR_ERC20_INVALID_RECEIVER`, `ERR_ERC20_INVALID_SENDER` — mint to or burn from the zero address. +- `ERR_ERC20_INSUFFICIENT_BALANCE`, `ERR_ERC20_INSUFFICIENT_ALLOWANCE` — transfer or approve more than the caller has, or spend more than was approved. + +## Storage + +Each Universal Token contract is stored as `Bytecode::OwnableAccount(owner = PRECOMPILE_UNIVERSAL_TOKEN_RUNTIME, metadata = original constructor input)`. The metadata payload is the SolidityABI-encoded `InitialSettings` struct itself; per-token configuration (name, symbol, decimals, minter, pauser, wrapped flag) is read from this metadata at runtime. Balances, allowances, and totalSupply live in the deployed account's regular storage slots. + +## Source + +`fluentbase/contracts/universal-token/` diff --git a/docs/system-architecture/precompiles/wasm-runtime.md b/docs/system-architecture/precompiles/wasm-runtime.md new file mode 100644 index 0000000..76dcb44 --- /dev/null +++ b/docs/system-architecture/precompiles/wasm-runtime.md @@ -0,0 +1,46 @@ +--- +title: WASM Runtime +sidebar_position: 2 +--- + +The delegated WASM runtime dispatches calls and creates targeting Wasm contracts. Init code starting with the WASM magic preamble is routed here at deploy time. + +## Address + +| Constant | Address | +|---|---| +| `PRECOMPILE_WASM_RUNTIME` | `0x0000000000000000000000000000000000520009` | + +## Routing + +Init code starting with `WASM_MAGIC_BYTES = 0x0061736d` (the standard Wasm 4-byte preamble) routes to this runtime. The dispatcher logic lives in `resolve_precompiled_runtime_from_input` in `fluentbase/crates/types/src/genesis.rs`. Direct calls to the runtime address are rejected. + +## Behavior + +`deploy_entry` compiles the input Wasm to rWasm via `RwasmModule::compile`, validates the result against `RWASM_MAX_CODE_SIZE = 12 MiB`, and returns the compiled module plus any constructor parameters. Compilation is metered: + +```text +WASM_COMPILATION_OVERHEAD_FUEL_PER_BYTE = 50 * FUEL_DENOM_RATE = 1000 fuel/byte = 50 gas/byte +``` + +A 100 KB Wasm payload pays roughly 5,000,000 gas just to compile. Estimate deploy cost accordingly. + +After compilation, the executor installs the rWasm bytes at the deployed address (replacing the temporary ownable wrapper). The account's code field then starts with the rWasm magic prefix `0xEF52`, and subsequent calls execute the rWasm directly at that address — `main_entry` on this dispatcher returns `UnreachableCodeReached` and is never hit on a properly deployed contract. See the wrapper-rewrite path in [Runtime Routing and Ownable Accounts](../runtime-routing-and-ownable-accounts.md). + +## Errors + +- `MalformedBuiltinParams` — `RwasmModule::compile` rejected the input. +- `CreateContractSizeLimit` — compiled rWasm exceeds `RWASM_MAX_CODE_SIZE`. +- `UnreachableCodeReached` — only path through `main_entry`; should not be reached on a properly deployed contract. + +## Metadata syscalls and Wasm contracts + +Metadata syscalls operate only on `Bytecode::OwnableAccount`. Because Wasm contracts are stored as `Bytecode::Rwasm` after the deploy rewrite, the syscalls behave as follows when called against a Wasm contract: + +- `METADATA_SIZE` returns `size = 0` (graceful, no halt). +- `METADATA_ACCOUNT_OWNER` returns `Address::ZERO` (graceful, no halt). +- `METADATA_WRITE` and `METADATA_COPY` halt with `MalformedBuiltinParams`. + +## Source + +`fluentbase/contracts/wasm/` diff --git a/docs/system-architecture/rollup-architecture.md b/docs/system-architecture/rollup-architecture.md index 186bf52..5bda21e 100644 --- a/docs/system-architecture/rollup-architecture.md +++ b/docs/system-architecture/rollup-architecture.md @@ -1,6 +1,6 @@ --- title: Rollup Architecture -sidebar_position: 9 +sidebar_position: 10 --- Fluent is an Ethereum-aligned L2 rollup. Every block produced on Fluent eventually settles to Ethereum under a cryptographic integrity story, and the way that story is composed — fast preconfirmation plus slow cryptographic adjudication — is what makes the chain usable and safe at the same time. @@ -144,6 +144,25 @@ Fluent's rollup is operated by a set of explicit roles, each with a scoped respo "No centralized override over the state transition function" is not the same as "zero trust." Fluent reduces unilateral-override risk by combining immutable batch and data commitments, bonded adversarial participation in challenges, cryptographic proof verification for disputed transitions, and explicit role separation — but governance and role-based trust in upgrade and emergency controls remain part of the model. The accurate framing is **structured, compartmentalized trust with cryptographic fault containment**, not trustlessness. +## Mainnet addresses + +### L1 contracts + +| Contract | Address | +|---|---| +| Rollup | `0x1cF53Fd9CD0b713be29F2b41cA17A943f138727f` | +| NitroVerifier | `0xFdB04b67ecD8352bA3885F66fFfddf1f5f25292F` | +| Timelock | `0x7846C001835d889A29ba659f67A5B7ac98E73bF4` | + +### Operator EOAs + +| Role | Address | +|---|---| +| `SEQUENCER` | `0xFd58Bc438d910088C413b889Eaa0aded5C0d1c26` | +| `FINALIZER` | `0x2caB823ed5bfDB8d9ea4AD6d34E07A499F8983e6` | +| `PROVER` | `0xB9E6f78a0F35F96b806D0359AbB251117aCe255C` | +| `SP1_ENCLAVE_ATTESTER` | `0xef9Dc1F87BAA090a35B985DAad9c8096440F2012` | + ## Economic framing The pipeline is designed to exploit cheap data availability (EIP-4844 blobs) while keeping dispute-grade verification available on demand. In steady state, marginal transaction cost is low because expensive proof generation is shifted to adversarial or accelerated paths rather than required for every block up front. diff --git a/docs/system-architecture/runtime-routing-and-ownable-accounts.md b/docs/system-architecture/runtime-routing-and-ownable-accounts.md index 2aa9086..87eb031 100644 --- a/docs/system-architecture/runtime-routing-and-ownable-accounts.md +++ b/docs/system-architecture/runtime-routing-and-ownable-accounts.md @@ -9,7 +9,7 @@ This routing mechanism — ownable accounts — is what makes cross-VM composabi ## The ownable account format -Every contract deployed on Fluent lives in an account whose code field is an **ownable-account wrapper**. The wrapper carries three things: +Most contracts deployed on Fluent live in an account whose code field is an **ownable-account wrapper** (magic prefix `0xEF44`). Wasm contracts are the exception: their account code is the compiled rWasm directly (magic prefix `0xEF52`), with the wrapper replaced during deploy — see [The Wasm-wrapper deploy rewrite](#the-wasm-wrapper-deploy-rewrite) below. For wrapped accounts, the wrapper carries three things: - a **magic and version header** identifying the account as runtime-owned, - an `owner_address` — the delegated runtime that should execute this account, @@ -23,12 +23,12 @@ Which runtime owns a new account is decided at deployment. When init code arrive | Init code prefix | Delegated runtime | |---|---| -| Wasm / rWasm magic | Wasm delegated runtime | -| SVM ELF payload *(feature-gated)* | SVM delegated runtime | -| `UNIVERSAL_TOKEN_MAGIC_BYTES = 0x45524320` (`"ERC "`) | Universal Token runtime | -| Anything else | Delegated EVM runtime | +| Wasm / rWasm magic | [Wasm delegated runtime](./precompiles/wasm-runtime.md) | +| SVM ELF payload *(feature-gated)* | SVM delegated runtime *(reserved — see [Precompiles](./precompiles/))* | +| `UNIVERSAL_TOKEN_MAGIC_BYTES = 0x45524320` (`"ERC "`) | [Universal Token runtime](./precompiles/universal-token-runtime.md) | +| Anything else | [Delegated EVM runtime](./precompiles/evm-runtime.md) | -Two things happen next. The new account's code is set to the ownable-account wrapper pointing at the chosen runtime, and the original init payload is passed to the delegated runtime for whatever deploy-time logic that runtime defines — constructor execution, storage initialization, role assignment. +Two things happen next. The new account's code is set to the ownable-account wrapper pointing at the chosen runtime, and the original init payload is passed to the delegated runtime for whatever deploy-time logic that runtime defines — constructor execution, storage initialization, role assignment. Wasm contracts undergo one extra step at the end of this flow that swaps the wrapper for compiled rWasm — see [The Wasm-wrapper deploy rewrite](#the-wasm-wrapper-deploy-rewrite) below. After that, the account's execution class is frozen. There's no later toggle that switches a Solidity contract to the Wasm runtime. Ownership is established at deploy and is part of the account's identity. @@ -69,9 +69,14 @@ Static-context mutations — anything invoked from a `STATICCALL` frame — are ## The Wasm-wrapper deploy rewrite -One special path worth knowing about if you're auditing the deployment flow. The Wasm runtime's deploy output can contain a compiled rWasm payload followed by a constructor tail. In that case, the deployed code is rewritten: the account's code is set to the compiled rWasm bytecode directly, not to the ownable-account wrapper, and the constructor tail runs with the remaining deployment parameters. +Wasm contracts take a different storage path. After the Wasm runtime's `deploy_entry` returns the compiled rWasm module (plus any constructor tail), the executor replaces the temporary ownable wrapper at the deployed address with the rWasm bytes directly. The on-chain code at that address starts with the rWasm magic prefix `0xEF52` instead of the wrapper's `0xEF44`. The constructor tail then runs with the remaining deployment parameters. -This supports Wasm-wrapper deployment flows where the final on-chain representation is the compiled artifact rather than a pointer to a delegated runtime. From an execution-semantics perspective the account is still routed consistently; the rewrite is a storage-level detail. +Execution semantics are unchanged — the rWasm runs under the same engine that ownable accounts dispatch into. The difference is representational: + +- **Wrapped accounts** (`0xEF44`): code field is `wrapper(owner_address, metadata)`; REVM looks up the runtime via `owner_address` on every call. +- **Direct rWasm accounts** (`0xEF52`): code field is the compiled rWasm itself; REVM executes it without an owner-address indirection. Metadata syscalls (`METADATA_SIZE`, `METADATA_ACCOUNT_OWNER`, `METADATA_WRITE`) recognize only the wrapped form and return null or halt against direct rWasm. + +The rewrite happens because the Wasm runtime's compiled output is already a complete rWasm module — wrapping it with another indirection layer would only add lookup cost. For Solidity and Universal Token contracts the wrapper stays, because their EVM/UT bytecode lives inside the wrapper's `metadata` field, not at the account address itself. ## Why this model diff --git a/docs/system-architecture/runtime-upgrade.md b/docs/system-architecture/runtime-upgrade.md index dbc469f..1c43b08 100644 --- a/docs/system-architecture/runtime-upgrade.md +++ b/docs/system-architecture/runtime-upgrade.md @@ -1,6 +1,6 @@ --- title: Runtime Upgrade -sidebar_position: 7 +sidebar_position: 8 --- Fluent's delegated runtimes — the EVM runtime, the Wasm runtime, the Universal Token runtime, and others — are protocol-owned bytecode. They decide how every ownable account in their class behaves. Fixing bugs and evolving behavior in those runtimes without a full node rewrite is a design goal. Making absolutely sure no one else can do it is another. @@ -28,7 +28,7 @@ The upgrade contract exposes a minimal surface: - `owner()` — returns the current owner. - `renounceOwnership()` — sets the owner to a designated system address, effectively freezing upgrades through the owner-based path while leaving a deterministic default. -A default-owner fallback is defined for the unset state, so the contract always has a well-formed owner to check against. +A default-owner fallback is defined for the unset state, so the contract always has a well-formed owner to check against. The address, calldata format, and authority constants live on the [Runtime Upgrade Precompile](./precompiles/runtime-upgrade.md) page. ## Host-side enforcement diff --git a/docs/system-architecture/security-invariants.md b/docs/system-architecture/security-invariants.md index b0c709f..2c38d46 100644 --- a/docs/system-architecture/security-invariants.md +++ b/docs/system-architecture/security-invariants.md @@ -1,6 +1,6 @@ --- title: Security Invariants -sidebar_position: 8 +sidebar_position: 9 --- The other pages in this section describe how Fluent works. This page is the list of things that cannot stop being true without the chain breaking. These are the consensus-critical invariants — break any one and the failure mode isn't a bug, it's a consensus split, a privilege escalation, or a host-level instability that affects every account on the chain. diff --git a/docs/system-architecture/state-and-rpc-compatibility.md b/docs/system-architecture/state-and-rpc-compatibility.md index aed188b..c48ef5f 100644 --- a/docs/system-architecture/state-and-rpc-compatibility.md +++ b/docs/system-architecture/state-and-rpc-compatibility.md @@ -1,6 +1,6 @@ --- title: State and RPC Compatibility -sidebar_position: 6 +sidebar_position: 7 --- Fluent's state model is deliberately Ethereum-shaped on the outside: one trie, account addresses, balances, nonces, code hashes. But some contracts are stored in the runtime-managed **ownable-account wrapper** (see [Runtime Routing](./runtime-routing-and-ownable-accounts.md)), so what a wallet or indexer reads from a Fluent node isn't always the bytes sitting in storage. The wrapper carries execution metadata that would confuse Ethereum tooling, so the node exposes two RPC views — one normalized for compatibility, one raw for infrastructure.