diff --git a/.github/workflows/mip-upstream.yml b/.github/workflows/mip-upstream.yml new file mode 100644 index 0000000..2619c67 --- /dev/null +++ b/.github/workflows/mip-upstream.yml @@ -0,0 +1,23 @@ +name: MIP upstream review + +on: + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Check upstream specifications + run: node scripts/check-mip-upstream.mjs + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/README.md b/README.md index 9ccc621..b1f8320 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ npm run dev ## Contributing +Upstream source changes are checked daily and on demand with +`pnpm check:mip-upstream`. See [the review and baseline guide](docs/mip-upstream-review.md). + See [CONTRIBUTING.md](CONTRIBUTING.md). Every PR requires an approved Issue first. ## License diff --git a/content/mips/upstream-sources.json b/content/mips/upstream-sources.json new file mode 100644 index 0000000..85b8be8 --- /dev/null +++ b/content/mips/upstream-sources.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "sources": [ + { + "slug": "mip-3", + "path": "MIPs/MIP-3.md", + "reviewedCommit": "d2fe40811999387de9ac371692198bb8f72c10fb", + "reviewedBlobSha": "7c7e97160d48144d8ecf940708294c8310e1d479" + }, + { + "slug": "mip-4", + "path": "MIPs/MIP-4.md", + "reviewedCommit": "d2fe40811999387de9ac371692198bb8f72c10fb", + "reviewedBlobSha": "ec296779ccd3774cc5a6ef61d411d19d794cb347" + }, + { + "slug": "mip-7", + "path": "MIPs/MIP-7.md", + "reviewedCommit": "d2fe40811999387de9ac371692198bb8f72c10fb", + "reviewedBlobSha": "4504a0dc1637eb4b4b8cbab155cbf509a5f0c259" + }, + { + "slug": "mip-8", + "path": "MIPs/MIP-8.md", + "reviewedCommit": "d2fe40811999387de9ac371692198bb8f72c10fb", + "reviewedBlobSha": "32832485ce69a62b0fdac9c36d559816b0b118d8" + }, + { + "slug": "mip-12", + "path": "MIPs/MIP-12.md", + "reviewedCommit": "d2fe40811999387de9ac371692198bb8f72c10fb", + "reviewedBlobSha": "3b63df10520b8f21e588fe0061e2b0231ef43065" + } + ] +} diff --git a/docs/mip-upstream-review.md b/docs/mip-upstream-review.md new file mode 100644 index 0000000..d52ce91 --- /dev/null +++ b/docs/mip-upstream-review.md @@ -0,0 +1,55 @@ +# Reviewing upstream MIP changes + +Run `pnpm check:mip-upstream` with Node 22. The command checks the five MIPs +covered by the Markdown knowledge bundle against `monad-crypto/MIPs`. +`GITHUB_TOKEN` is optional locally and avoids the unauthenticated API rate limit. + +The checker resolves upstream `main` once, then checks each canonical file at +that immutable commit. Each request has a 10-second deadline, including its +response body. The report links the recorded baseline and current source. + +Exit codes: **0** means all file hashes match, **1** means an explainer review +is needed, **2** means a check failed (including missing files, malformed data +and API failures). Errors take precedence when some files also changed. + +The daily/manual **MIP upstream review** workflow publishes the same report in +its job summary, uses read-only permissions and fails on either review or error. +It does not edit knowledge, move baselines or open issues. Ordinary PR tests +stay offline; they also check manifest coverage against `MARKDOWN_MIP_SLUGS`. +The existing required CI job remains `test`. + +## Initial baseline + +`content/mips/upstream-sources.json` starts with a **conservative historical +source snapshot**, commit `d2fe40811999387de9ac371692198bb8f72c10fb` (June 15, +2026), rather than certifying a new review of today's explainers. +At that revision MIP-12 is Draft, matching the legacy local copy; upstream +changed it to Final on August 12 in `2a7e18894f1e55fb043080cb8cef15c7f5647768`. +The first live run should therefore request review, including that change. +The other covered files also changed since this snapshot, so they are reported +for review too. This check does not make the content corrections tracked in +issue #51 or the chat-instruction changes in #52. + +## Record a reviewed baseline + +1. Follow the report's source links and review the change against the HTML + explainer and `content/mips/mip-N.md`. Interpret its impact and update the + affected explanation through the usual reviewed PR process. +2. Copy the report's full **current specification revision**. For each reviewed + file, get its blob hash at that exact revision: + + ```bash + gh api "repos/monad-crypto/MIPs/contents/MIPs/MIP-12.md?ref=" --jq .sha + ``` + +3. Update that entry's `reviewedCommit` and `reviewedBlobSha` together. + `reviewedCommit` is the immutable commit used by the source link; + `reviewedBlobSha` is the Contents API's file-content hash, not a commit. + Keep unreviewed entries at their earlier baselines. +4. Run `pnpm test` and the checker, then include the review rationale in the PR. + Changes after the inspected revision will still be detected. + +These baselines track **specifications only**. An unchanged spec does not mean +unchanged rollout or network activation. Establish activation separately from +dated, network-specific evidence; do not infer it from a Final status or a +matching source hash. diff --git a/package.json b/package.json index 8dd5b9d..5835426 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build": "next build", "start": "next start", "lint": "eslint", - "test": "vitest run" + "test": "vitest run", + "check:mip-upstream": "node scripts/check-mip-upstream.mjs" }, "dependencies": { "@ai-sdk/react": "^3.0.186", diff --git a/scripts/check-mip-upstream.mjs b/scripts/check-mip-upstream.mjs new file mode 100644 index 0000000..ccc1d86 --- /dev/null +++ b/scripts/check-mip-upstream.mjs @@ -0,0 +1,159 @@ +import { appendFile, readFile, readdir } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +const REPO = "monad-crypto/MIPs"; +const WEB = `https://github.com/${REPO}`; +const CONTENT_DIR = new URL("../content/mips/", import.meta.url); +const isSha = (value) => typeof value === "string" && /^[a-f0-9]{40}$/.test(value); +const errorMessage = (error) => error instanceof Error && error.message + ? error.message : "GitHub API check failed."; + +export function validateManifest(manifest, coveredSlugs) { + if (manifest?.version !== 1 || !Array.isArray(manifest.sources)) { + throw new Error("Invalid upstream manifest: expected version 1 and sources."); + } + const slugs = []; + for (const source of manifest.sources) { + if ( + !source || typeof source.slug !== "string" || + !/^mip-[1-9]\d*$/.test(source.slug) || + source.path !== `MIPs/${source.slug.toUpperCase()}.md` || + !isSha(source.reviewedCommit) || !isSha(source.reviewedBlobSha) + ) { + throw new Error("Invalid upstream manifest: canonical paths and full commit/blob hashes are required."); + } + slugs.push(source.slug); + } + if ( + !Array.isArray(coveredSlugs) || coveredSlugs.length === 0 || + new Set(slugs).size !== slugs.length || + [...slugs].sort().join(",") !== [...coveredSlugs].sort().join(",") + ) { + throw new Error("Upstream manifest must cover every MIP Markdown source exactly once."); + } + return manifest.sources; +} + +async function githubJson(path, { fetchImpl, token, timeoutMs }) { + const controller = new AbortController(); + let timer; + // Keep the deadline active through body decoding, not just response headers. + const deadline = new Promise((_, reject) => { + timer = setTimeout(() => { + controller.abort(); + reject(new Error(`GitHub API request timed out after ${timeoutMs} ms.`)); + }, timeoutMs); + }); + try { + return await Promise.race([ + (async () => { + const response = await fetchImpl(`https://api.github.com/repos/${REPO}/${path}`, { + headers: { + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "mipland-upstream-check", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + signal: controller.signal, + }); + if (!response.ok) { + throw new Error(`GitHub API returned HTTP ${response.status}.`); + } + return await response.json(); + })(), + deadline, + ]); + } finally { + clearTimeout(timer); + } +} + +export async function checkUpstream( + manifest, + coveredSlugs, + { fetchImpl = fetch, token, timeoutMs = 10_000 } = {}, +) { + try { + const sources = validateManifest(manifest, coveredSlugs); + if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) { + throw new Error("The GitHub API timeout must be a positive number."); + } + const options = { fetchImpl, token, timeoutMs }; + const head = await githubJson("commits/main", options); + if (!isSha(head?.sha)) { + throw new Error("Malformed GitHub response: no upstream commit SHA."); + } + const currentCommit = head.sha; + const results = await Promise.all(sources.map(async (source) => { + try { + const file = await githubJson(`contents/${source.path}?ref=${currentCommit}`, options); + if (file?.type !== "file" || file.path !== source.path || !isSha(file.sha)) { + throw new Error("Malformed GitHub response: expected the requested file and its blob SHA."); + } + return { + ...source, + currentBlobSha: file.sha, + status: file.sha === source.reviewedBlobSha ? "unchanged" : "review", + }; + } catch (error) { + return { ...source, status: "error", error: errorMessage(error) }; + } + })); + const exitCode = results.some((r) => r.status === "error") ? 2 + : results.some((r) => r.status === "review") ? 1 : 0; + return { currentCommit, results, exitCode }; + } catch (error) { + return { results: [], exitCode: 2, error: errorMessage(error) }; + } +} + +export function formatReport(report) { + const verdict = ["Unchanged", "Review needed", "ERROR: source check incomplete"][report.exitCode]; + const lines = [`# MIP upstream check — ${verdict}`, ""]; + if (report.error) lines.push(report.error, ""); + if (report.currentCommit) { + lines.push(`Current specification revision: [${report.currentCommit}](${WEB}/commit/${report.currentCommit})`, ""); + } + if (report.results.length) { + lines.push("| MIP | Result | Reviewed source | Current source |", "| --- | --- | --- | --- |"); + for (const result of report.results) { + const baseline = `[${result.reviewedBlobSha.slice(0, 7)}](${WEB}/blob/${result.reviewedCommit}/${result.path})`; + const current = report.currentCommit + ? `[${result.currentBlobSha?.slice(0, 7) ?? "unavailable"}](${WEB}/blob/${report.currentCommit}/${result.path})` + : "unavailable"; + const status = result.status === "error" ? `ERROR: ${(result.error || "GitHub API check failed.").replace(/[|\r\n]/g, " ")}` + : result.status === "review" ? "Review needed" : "Unchanged"; + lines.push(`| ${result.slug.toUpperCase()} | ${status} | ${baseline} | ${current} |`); + } + } + lines.push("", "Specification changes require human review. Matching specs do not establish network activation.", ""); + return lines.join("\n"); +} + +async function main() { + let report; + try { + const manifest = JSON.parse(await readFile(new URL("upstream-sources.json", CONTENT_DIR), "utf8")); + const coveredSlugs = (await readdir(CONTENT_DIR)) + .filter((name) => /^mip-\d+\.md$/.test(name)) + .map((name) => name.slice(0, -3)); + report = await checkUpstream(manifest, coveredSlugs, { token: process.env.GITHUB_TOKEN }); + } catch { + report = { results: [], exitCode: 2, error: "Cannot read the upstream manifest or MIP Markdown sources." }; + } + const summary = formatReport(report); + console.log(summary); + process.exitCode = report.exitCode; + if (process.env.GITHUB_STEP_SUMMARY) { + try { + await appendFile(process.env.GITHUB_STEP_SUMMARY, summary); + } catch { + console.error("Cannot write the GitHub Actions job summary."); + process.exitCode = 2; + } + } +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + await main(); +} diff --git a/scripts/check-mip-upstream.test.ts b/scripts/check-mip-upstream.test.ts new file mode 100644 index 0000000..88884b7 --- /dev/null +++ b/scripts/check-mip-upstream.test.ts @@ -0,0 +1,184 @@ +import { createHash } from "node:crypto"; +import { readFileSync, readdirSync } from "node:fs"; + +import { afterEach, describe, expect, it, vi } from "vitest"; + +import manifest from "../content/mips/upstream-sources.json"; +import { MARKDOWN_MIP_SLUGS } from "../src/lib/mip-routes"; +import { checkUpstream, formatReport, validateManifest } from "./check-mip-upstream.mjs"; + +const HEAD = "0123456789abcdef0123456789abcdef01234567"; +type Source = (typeof manifest.sources)[number]; +const contents = (source: Source, sha = source.reviewedBlobSha) => + Response.json({ type: "file", path: source.path, sha }); + +function sourceFetch( + respond: (source: Source, init?: RequestInit) => Response | Promise = (source) => contents(source), +) { + return vi.fn(async (input, init) => { + const url = new URL(String(input)); + if (url.pathname.endsWith("/commits/main")) return Response.json({ sha: HEAD }); + const source = manifest.sources.find((entry) => url.pathname.endsWith(`/contents/${entry.path}`)); + if (!source) throw new Error(`Unexpected fixture request: ${url.pathname}`); + expect(url.searchParams.get("ref")).toBe(HEAD); + return respond(source, init); + }); +} + +const check = (fetchImpl: typeof fetch) => + checkUpstream(manifest, [...MARKDOWN_MIP_SLUGS], { fetchImpl }); + +afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); +}); + +describe("reviewed manifest", () => { + it("covers the route list and actual Markdown files exactly once", () => { + const files = readdirSync(new URL("../content/mips/", import.meta.url)) + .filter((name) => /^mip-\d+\.md$/.test(name)).map((name) => name.slice(0, -3)); + expect(files.sort()).toEqual([...MARKDOWN_MIP_SLUGS].sort()); + expect(validateManifest(manifest, [...MARKDOWN_MIP_SLUGS])).toEqual(manifest.sources); + }); + + it.each(["missing", "extra", "duplicate", "path", "commit", "blob", "version"])( + "rejects a %s manifest defect before fetching", async (defect) => { + const invalid = structuredClone(manifest); + if (defect === "missing") invalid.sources.pop(); + if (defect === "extra") invalid.sources.push({ ...invalid.sources[0], slug: "mip-99", path: "MIPs/MIP-99.md" }); + if (defect === "duplicate") invalid.sources.push(invalid.sources[0]); + if (defect === "path") invalid.sources[0].path = "MIPs/MIP-12.md"; + if (defect === "commit") invalid.sources[0].reviewedCommit = "short"; + if (defect === "blob") invalid.sources[0].reviewedBlobSha = "invalid"; + if (defect === "version") invalid.version = 2; + const fetchImpl = sourceFetch(); + const report = await checkUpstream(invalid, [...MARKDOWN_MIP_SLUGS], { fetchImpl }); + expect(report.exitCode).toBe(2); + expect(report.error).toBeTruthy(); + expect(fetchImpl).not.toHaveBeenCalled(); + }, + ); +}); + +describe("upstream source changes", () => { + it("accepts matching content even when the repository commit changes", async () => { + const original = structuredClone(manifest); + const fetchImpl = sourceFetch(); + const report = await check(fetchImpl); + expect(report.exitCode).toBe(0); + expect(report.currentCommit).toBe(HEAD); + expect(report.results.map((result: { status: string }) => result.status)).toEqual(Array(5).fill("unchanged")); + expect(fetchImpl).toHaveBeenCalledTimes(6); + expect(manifest).toEqual(original); + }); + + it("detects the real MIP-12 Draft-to-Final edit from offline content fixtures", async () => { + // Exact CC0 sources: monad-crypto/MIPs/MIPs/MIP-12.md at d2fe40811999387de9ac371692198bb8f72c10fb + // and 2a7e18894f1e55fb043080cb8cef15c7f5647768; only the status line changed. + const draft = readFileSync(new URL("./fixtures/mip-12-draft.md", import.meta.url)); + const final = readFileSync(new URL("./fixtures/mip-12-final.md", import.meta.url)); + const blobSha = (body: Buffer) => createHash("sha1").update(`blob ${body.length}\0`).update(body).digest("hex"); + expect(draft.toString().replace("status: Draft", "status: Final")).toBe(final.toString()); + const source = manifest.sources.find((entry) => entry.slug === "mip-12")!; + expect(blobSha(draft)).toBe(source.reviewedBlobSha); + expect(blobSha(final)).toBe("114c7e0a3c48b2e964a3d89e48e57bc9060127f8"); + const report = await check(sourceFetch((entry) => contents(entry, entry.slug === "mip-12" ? blobSha(final) : entry.reviewedBlobSha))); + expect(report.exitCode).toBe(1); + expect(report.results.find((entry: Source) => entry.slug === "mip-12")).toMatchObject({ status: "review", currentBlobSha: blobSha(final) }); + expect(formatReport(report)).toContain("MIP-12 | Review needed"); + }); + + it("errors dominate drift while still reporting every checked file", async () => { + const report = await check(sourceFetch((source) => { + if (source.slug === "mip-3") return contents(source, "f".repeat(40)); + if (source.slug === "mip-4") return new Response(null, { status: 404 }); + return contents(source); + })); + expect(report.exitCode).toBe(2); + expect(report.results.map((result: { status: string }) => result.status)).toEqual(["review", "error", "unchanged", "unchanged", "unchanged"]); + const summary = formatReport(report); + expect(summary).toContain("MIP-3 | Review needed"); + expect(summary).toContain("MIP-4 | ERROR:"); + }); + + it("uses immutable commit links for reviewed and current sources", async () => { + const report = await check(sourceFetch((source) => contents(source, "f".repeat(40)))); + const summary = formatReport(report); + for (const source of manifest.sources) { + expect(summary).toContain(`/blob/${source.reviewedCommit}/${source.path}`); + expect(summary).toContain(`/blob/${HEAD}/${source.path}`); + expect(summary).not.toContain(`/blob/${source.reviewedBlobSha}/`); + } + expect(summary).not.toContain("/blob/main/"); + expect(summary).toContain("Matching specs do not establish network activation."); + }); +}); + +describe("upstream failures", () => { + it.each([404, 403, 500])("reports HTTP %s as an error, including a removed file", async (status) => { + const report = await check(sourceFetch((source) => source.slug === "mip-12" ? new Response(null, { status }) : contents(source))); + expect(report.exitCode).toBe(2); + expect(report.results.find((source: Source) => source.slug === "mip-12")).toMatchObject({ status: "error", error: `GitHub API returned HTTP ${status}.` }); + }); + + it.each([new Error("network unavailable"), new Error(""), "network failure"])( + "reports fetch rejection as ERROR, including empty and non-Error failures", async (failure) => { + const report = await check(sourceFetch((source) => { + if (source.slug === "mip-12") throw failure; + return contents(source); + })); + expect(report.exitCode).toBe(2); + expect(report.results.find((source: Source) => source.slug === "mip-12")).toMatchObject({ status: "error" }); + expect(formatReport(report)).toContain("MIP-12 | ERROR:"); + }, + ); + + it.each([null, {}, { sha: "short" }, { sha: "G".repeat(40) }])("refuses malformed HEAD data %j", async (body) => { + const fetchImpl = vi.fn(async () => Response.json(body)); + const report = await check(fetchImpl); + expect(report).toMatchObject({ exitCode: 2, results: [] }); + expect(report.error).toContain("upstream commit SHA"); + expect(fetchImpl).toHaveBeenCalledTimes(1); + }); + + it.each([ + null, [], {}, + { type: "dir", path: "MIPs/MIP-12.md", sha: HEAD }, + { type: "file", path: "MIPs/MIP-3.md", sha: HEAD }, + { type: "file", path: "MIPs/MIP-12.md", sha: "short" }, + ])("refuses malformed Contents data %j", async (body) => { + const report = await check(sourceFetch((source) => source.slug === "mip-12" ? Response.json(body) : contents(source))); + expect(report.exitCode).toBe(2); + expect(report.results.find((source: Source) => source.slug === "mip-12")).toMatchObject({ status: "error" }); + }); + + it.each(["HEAD", "Contents"])("rejects malformed JSON from %s", async (stage) => { + const broken = () => new Response("{invalid JSON", { headers: { "content-type": "application/json" } }); + const fetchImpl = stage === "HEAD" ? vi.fn(async () => broken()) : sourceFetch(() => broken()); + const report = await check(fetchImpl); + expect(report.exitCode).toBe(2); + if (stage === "HEAD") expect(report.error).toBeTruthy(); + else expect(report.results.every((result: { status: string }) => result.status === "error")).toBe(true); + }); + + it.each(["headers", "body"])("bounds the %s deadline and aborts the request", async (stage) => { + vi.useFakeTimers(); + const signals: AbortSignal[] = []; + const remember = (init?: RequestInit) => signals.push(init!.signal!); + const fetchImpl = stage === "headers" + ? vi.fn(async (_input, init) => { remember(init); return new Promise(() => {}); }) + : sourceFetch((_source, init) => { + remember(init); + const response = Response.json({}); + vi.spyOn(response, "json").mockImplementation(() => new Promise(() => {})); + return response; + }); + const pending = checkUpstream(manifest, [...MARKDOWN_MIP_SLUGS], { fetchImpl, timeoutMs: 20 }); + await vi.advanceTimersByTimeAsync(20); + const report = await pending; + expect(report.exitCode).toBe(2); + expect(signals).toHaveLength(stage === "headers" ? 1 : 5); + expect(signals.every((signal) => signal.aborted)).toBe(true); + expect(formatReport(report)).toContain("timed out after 20 ms"); + }); +}); diff --git a/scripts/fixtures/README.md b/scripts/fixtures/README.md new file mode 100644 index 0000000..2715dea --- /dev/null +++ b/scripts/fixtures/README.md @@ -0,0 +1,10 @@ +# MIP-12 regression sources + +These files preserve the exact bytes of the canonical specification, including +its original relative copyright link, so the tests can reproduce Git blob hashes. + +- [Draft source](https://github.com/monad-crypto/MIPs/blob/d2fe40811999387de9ac371692198bb8f72c10fb/MIPs/MIP-12.md) +- [Final source](https://github.com/monad-crypto/MIPs/blob/2a7e18894f1e55fb043080cb8cef15c7f5647768/MIPs/MIP-12.md) +- [Upstream CC0 dedication](https://github.com/monad-crypto/MIPs/blob/2a7e18894f1e55fb043080cb8cef15c7f5647768/LICENSE.md) + +No network access is needed to read these fixtures or run the tests. diff --git a/scripts/fixtures/mip-12-draft.md b/scripts/fixtures/mip-12-draft.md new file mode 100644 index 0000000..3b63df1 --- /dev/null +++ b/scripts/fixtures/mip-12-draft.md @@ -0,0 +1,52 @@ +--- +mip: 12 +title: Decrease Block Time +description: Decrease consensus vote pace from 400ms to 300ms +author: Category Labs +discussions-to: https://forum.monad.xyz/t/mip-12-decrease-vote-pace/488 +status: Draft +type: Standards Track +category: Core +created: 2026-06-01 +--- + +## Abstract + +Decrease the block time by decreasing the consensus vote pace from 400ms to 300ms. Proportionally, decrease the following block parameters: transaction limit, proposal gas limit and proposal byte limit. Proportionally, also decrease the block reward. + +## Specification + +We propose the following changes to the chain parameters on the consensus client: + +### Chain Parameters + +| Chain Parameter | Current Value | Proposed Value | +| --- | --- | --- | +| vote_pace (ms) | 400 | 300 | +| tx_limit | 5,000 | 3,750 | +| proposal_gas_limit | 200,000,000 | 150,000,000 | +| proposal_byte_limit | 2,000,000 | 1,500,000 | + +We also propose the following change to the staking parameters, to roughly account for the more frequent block times: + +### Staking Parameters + +| Staking Parameter | Current Value | Proposed Value | +| --- | --- | --- | +| block_reward (MON)| 25 | 18 | + +### Consensus and Execution Layer Impact + +This change should not affect the execution client in any way. + +The consensus client will vote on proposals 100 milliseconds faster than it does currently, leading to faster quorums and faster block times. + +## Backwards Compatibility + +This change is backward compatible with the execution client. + +However, since the block parameters are used for consensus block validation, this change is not backward compatible with the consensus client and will require a hard fork on a round. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/scripts/fixtures/mip-12-final.md b/scripts/fixtures/mip-12-final.md new file mode 100644 index 0000000..114c7e0 --- /dev/null +++ b/scripts/fixtures/mip-12-final.md @@ -0,0 +1,52 @@ +--- +mip: 12 +title: Decrease Block Time +description: Decrease consensus vote pace from 400ms to 300ms +author: Category Labs +discussions-to: https://forum.monad.xyz/t/mip-12-decrease-vote-pace/488 +status: Final +type: Standards Track +category: Core +created: 2026-06-01 +--- + +## Abstract + +Decrease the block time by decreasing the consensus vote pace from 400ms to 300ms. Proportionally, decrease the following block parameters: transaction limit, proposal gas limit and proposal byte limit. Proportionally, also decrease the block reward. + +## Specification + +We propose the following changes to the chain parameters on the consensus client: + +### Chain Parameters + +| Chain Parameter | Current Value | Proposed Value | +| --- | --- | --- | +| vote_pace (ms) | 400 | 300 | +| tx_limit | 5,000 | 3,750 | +| proposal_gas_limit | 200,000,000 | 150,000,000 | +| proposal_byte_limit | 2,000,000 | 1,500,000 | + +We also propose the following change to the staking parameters, to roughly account for the more frequent block times: + +### Staking Parameters + +| Staking Parameter | Current Value | Proposed Value | +| --- | --- | --- | +| block_reward (MON)| 25 | 18 | + +### Consensus and Execution Layer Impact + +This change should not affect the execution client in any way. + +The consensus client will vote on proposals 100 milliseconds faster than it does currently, leading to faster quorums and faster block times. + +## Backwards Compatibility + +This change is backward compatible with the execution client. + +However, since the block parameters are used for consensus block validation, this change is not backward compatible with the consensus client and will require a hard fork on a round. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md).