PageSourceSearch

https://ethpandaops.io/assets/js/bf57e562.d9c053d9.js

js ethpandaops.io collected 2026-10-03 21:45:10 UTC 28,655 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkpublic_docs=globalThis.webpackChunkpublic_docs||[]).push([[2389],{508(e,t,a){a.d(t,{A:()=>m});var s=a(96540),n=a(78478),i=a(86025),o=a(46414),l=a(40160),r=a(74848);const c=[{key:"p90",label:"P90",cumPct:90},{key:"p99",label:"P99",cumPct:99},{key:"p99_9",label:"P99.9",cumPct:99.9},{key:"p99_99",label:"P99.99",cumPct:99.99}];function d(){const e=(0,i.Ay)("/data/blog/fcr-simulator/cdf.json"),[t,a]=(0,s.useState)(null),[n,l]=(0,s.useState)(null),[c,d]=(0,s.useState)("log"),[u,m]=(0,s.useState)("p99"),[p,f]=(0,s.useState)({lighthouse:!0,lodestar:!0,grandine:!0,teku:!0});return(0,s.useEffect)(()=>{let t=!1;return fetch(e).then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}`);return e.json()}).then(e=>!t&&a(e)).catch(e=>!t&&l(String(e))),()=>{t=!0}},[e]),n?(0,r.jsxs)("div",{className:o.A.fallback,children:["Failed to load chart data: ",n]}):t?(0,r.jsx)(h,{json:t,scale:c,setScale:d,activePercentile:u,setActivePercentile:m,enabled:p,setEnabled:f}):(0,r.jsx)("div",{className:o.A.fallback,children:"Loading CDF data..."})}function h({json:e,scale:t,setScale:n,activePercentile:i,setActivePercentile:d,enabled:h,setEnabled:m}){const{ResponsiveContainer:p,LineChart:f,Line:x,XAxis:g,YAxis:j,CartesianGrid:b,Tooltip:y,ReferenceLine:v}=a(68025),k=(0,s.useMemo)(()=>{const a=new Set;for(const t of l.VO)for(const s of e.data[t]??[])a.add(s.d);return Array.from(a).sort((e,t)=>e-t).map(a=>{const s={d:a};for(const n of l.VO){const i=(e.data[n]??[]).find(e=>e.d===a);if(i)if("linear"===t)s[n]=i.cum_pct;else{const e=Math.max(100-i.cum_pct,1e-6);s[n]=e}}return s})},[e,t]),A="linear"===t?"Cumulative %":"Unconfirmed % (100 - CDF, log)",w=c.find(e=>e.key===i);return(0,r.jsxs)("div",{className:o.A.card,children:[(0,r.jsxs)("div",{className:o.A.header,children:[(0,r.jsxs)("div",{children:[(0,r.jsx)("p",{className:o.A.title,children:"Confirmation distance distribution"}),(0,r.jsx)("p",{className:o.A.subtitle,children:"% of slots fast-confirmed within X slots of head \u2014 full window per client"})]}),(0,r.jsxs)("div",{className:o.A.controls,children:[(0,r.jsxs)("div",{className:o.A.controlGroup,children:[(0,r.jsx)("span",{className:o.A.controlLabel,children:"Y-axis"}),["linear","log"].map(e=>(0,r.jsx)("button",{type:"button",className:`${o.A.controlButton} ${t===e?o.A.controlButtonActive:""}`,onClick:()=>n(e),children:e},e))]}),(0,r.jsxs)("div",{className:o.A.controlGroup,children:[(0,r.jsx)("span",{className:o.A.controlLabel,children:"Marker"}),c.map(e=>(0,r.jsx)("button",{type:"button",className:`${o.A.controlButton} ${i===e.key?o.A.controlButtonActive:""}`,onClick:()=>
1d(e.key),children:e.label},e.key))]})]})]}),(0,r.jsx)("div",{className:o.A.legend,children:l.VO.map(e=>(0,r.jsxs)("button",{type:"button",className:`${o.A.legendItem} ${h[e]?"":o.A.legendItemOff}`,onClick:()=>m({...h,[e]:!h[e]}),children:[(0,r.jsx)("span",{className:o.A.legendDot,style:{background:l.sU[e]}}),l.by[e]]},e))}),(0,r.jsx)("div",{className:o.A.chartWrap,style:{height:380},children:(0,r.jsx)(p,{width:"100%",height:"100%",children:(0,r.jsxs)(f,{data:k,margin:{top:6,right:16,bottom:8,left:0},children:[(0,r.jsx)(b,{stroke:"var(--ifm-color-emphasis-200)",strokeDasharray:"3 3"}),(0,r.jsx)(g,{dataKey:"d",type:"number",domain:[1,"dataMax"],tick:{fontSize:11,fill:"var(--ifm-color-emphasis-700)"},label:{value:"confirmation_delay_slots",position:"insideBottom",offset:-4,fontSize:11,fill:"var(--ifm-color-emphasis-700)"}}),"linear"===t?(0,r.jsx)(j,{tick:{fontSize:11,fill:"var(--ifm-color-emphasis-700)"},tickFormatter:e=>`${e.toFixed(0)}%`,domain:[0,100],width:48,label:{value:A,angle:-90,position:"insideLeft",fontSize:11,fill:"var(--ifm-color-emphasis-700)",dy:40}}):(0,r.jsx)(j,{tick:{fontSize:11,fill:"var(--ifm-color-emphasis-700)"},tickFormatter:e=>`${e<.01?e.toExponential(0):e.toFixed(2)}%`,scale:"log",domain:[1e-4,100],allowDataOverflow:!0,width:64,label:{value:A,angle:-90,position:"insideLeft",fontSize:11,fill:"var(--ifm-color-emphasis-700)",dy:60}}),"linear"===t&&(0,r.jsx)(v,{y:w.cumPct,stroke:"var(--ifm-color-emphasis-500)",strokeDasharray:"4 3",label:{value:`${w.label} = ${w.cumPct}%`,position:"insideTopRight",fill:"var(--ifm-color-emphasis-700)",fontSize:10}}),(0,r.jsx)(y,{content:(0,r.jsx)(u,{scale:t})}),l.VO.map(e=>h[e]?(0,r.jsx)(x,{type:"stepAfter",dataKey:e,stroke:l.sU[e],strokeWidth:2,dot:!1,activeDot:{r:3},isAnimationActive:!1,connectNulls:!0},e):null)]})})}),(0,r.jsx)("div",{className:o.A.percentileTable,children:l.VO.map(t=>(0,r.jsxs)("div",{className:o.A.percentileCell,children:[(0,r.jsxs)("div",{className:o.A.percentileCellHead,children:[(0,r.jsx)("span",{className:o.A.legendDot,style:{background:l.sU[t],marginRight:6}}),l.by[t]," \xb7 ",w.label]}),(0,r.jsxs)("div",{className:o.A.percentileValue,children:["\u2264 ",e.percentiles[t]?.[i]??"\u2014"," slots"]})]},t))}),(0,r.jsx)("p",{className:o.A.footnote,children:"CDF is computed from every slot in each client's CSV window. Log mode plots the unconfirmed tail (100 - CDF) so heavy tails are visible"})]})}function u({active:e,payload:t,label:a,scale:s}){if(!e||!t||0===t.length)return null;const n=t.filter(e=>"number"==typeof e.value);return(0,r.jsxs)("div",{className:o.A.tooltip,children:[(0,r.jsxs)("div",{className:o.A.tooltipTitle,children:["delay \u2264 ",a," slot",1===a?"":"s"]}),n.map(e=>(0,r.jsxs)("div",{className:o.A.tooltipRow,children:[(0,r.jsxs)("span",{className:o.A.tooltipKey,children:[(0,r.jsx)("span",{className:o.A.legendDot,style:{background:e.stroke}}),l.by[e.dataKey]]}),(0,r.jsx)("span",{className:o.A.tooltipValue,children:"linear"===s?`${e.value.toFixed(3)}%`:`${e.value.toFixed(4)}% unconfirmed`})]},e.dataKey))]})}function m(){return(0,r.jsx)(n.A,{fallback:(0,r.jsx)("div",{className:o.A.fallback,children:"Loading chart..."}),children:()=>(0,r.jsx)(d,{})})}},13574(e,t,a){a.d(t,{A:()=>u});var s=a(96540),n=a(78478),i=a(86025),o=a(46414),l=a(40160),r=a(74848);function c(){const e=(0,i.Ay)("/data/blog/fcr-simulator/monthly.json"),[t,a]=(0,s.use
1State)(null),[n,c]=(0,s.useState)(null),[h,u]=(0,s.useState)({lighthouse:!0,lodestar:!0,grandine:!0,teku:!0});(0,s.useEffect)(()=>{let t=!1;return fetch(e).then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}`);return e.json()}).then(e=>!t&&a(e)).catch(e=>!t&&c(String(e))),()=>{t=!0}},[e]);const m=(0,s.useMemo)(()=>{if(!t)return[];const e=new Set;for(const s of l.VO)for(const a of Object.keys(t[s]??{}))e.add(a);const a=Array.from(e).sort();if(0===a.length)return[];return function(e,t){const[a,s]=e.split("-").map(Number),[n,i]=t.split("-").map(Number),o=[];let l=a,r=s;for(;l<n||l===n&&r<=i;)o.push(`${l.toString().padStart(4,"0")}-${r.toString().padStart(2,"0")}`),r++,13===r&&(r=1,l++);return o}(a[0],a[a.length-1]).map(e=>{const a={month:e};for(const s of l.VO){const n=t[s]?.[e];"number"==typeof n&&(a[s]=n)}return a})},[t]);return n?(0,r.jsxs)("div",{className:o.A.fallback,children:["Failed to load chart data: ",n]}):t?(0,r.jsx)(d,{rows:m,enabled:h,setEnabled:u}):(0,r.jsx)("div",{className:o.A.fallback,children:"Loading monthly FCR data..."})}function d({rows:e,enabled:t,setEnabled:n}){const{ResponsiveContainer:i,LineChart:c,Line:d,XAxis:u,YAxis:m,CartesianGrid:p,Tooltip:f,ReferenceArea:x}=a(68025),g=(0,s.useMemo)(()=>{let t=100,a=0;for(const n of e)for(const e of l.VO){const s=n[e];"number"==typeof s&&(s<t&&(t=s),s>a&&(a=s))}if(!isFinite(t))return[90,100];const s=Math.max(.5,.15*(a-t));return[Math.max(0,Math.floor(10*(t-s))/10),Math.min(100,Math.ceil(10*(a+s))/10)]},[e]),j="2025-12",b=e.some(e=>e.month===j);return(0,r.jsxs)("div",{className:o.A.card,children:[(0,r.jsx)("div",{className:o.A.header,children:(0,r.jsxs)("div",{children:[(0,r.jsx)("p",{className:o.A.title,children:"Monthly 1-slot FCR rate by client"}),(0,r.jsx)("p",{className:o.A.subtitle,children:"% of slots fast-confirmed at delay = 1 slot, grouped by calendar month (UTC)"})]})}),(0,r.jsx)("div",{className:o.A.legend,children:l.VO.map(e=>(0,r.jsxs)("button",{type:"button",className:`${o.A.legendItem} ${t[e]?"":o.A.legendItemOff}`,onClick:()=>n({...t,[e]:!t[e]}),children:[(0,r.jsx)("span",{className:o.A.legendDot,style:{background:l.sU[e]}}),l.by[e]]},e))}),(0,r.jsx)("div",{className:o.A.chartWrap,style:{height:360},children:(0,r.jsx)(i,{width:"100%",height:"100%",children:(0,r.jsxs)(c,{data:e,margin:{top:6,right:16,bottom:8,left:0},children:[(0,r.jsx)(p,{stroke:"var(--ifm-color-emphasis-200)",strokeDasharray:"3 3"}),(0,r.jsx)(u,{dataKey:"month",tick:{fontSize:11,fill:"var(--ifm-color-emphasis-700)"}}),(0,r.jsx)(m,{domain:g,tick:{fontSize:11,fill:"var(--ifm-color-emphasis-700)"},tickFormatter:e=>`${e.toFixed(1)}%`,width:56}),b&&(0,r.jsx)(x,{x1:j,x2:j,strokeOpacity:0,fill:"#ef4444",fillOpacity:.08,ifOverflow:"extendDomain",label:{value:"Fusaka instability",position:"insideTopRight",fill:"#ef4444",fontSize:10}}),(0,r.jsx)(f,{content:(0,r.jsx)(h,{})}),l.VO.map(e=>t[e]?(0,r.jsx)(d,{type:"monotone",dataKey:e,stroke:l.sU[e],strokeWidth:2,dot:{r:2.5},activeDot:{r:4},connectNulls:!1,isAnimationActive:!1},e):null)]})})}),(0,r.jsx)("p",{className:o.A.footnote,children:"Source: per-CSV monthly aggregates from the v0.2.0 fcr-simulator dataset. Months with partial coverage at chunk boundaries include only the slots present in that month."})]})}function h({active:e,payload:t,label:a}){if(!e||!t||0===t.length)return null;const s=t.filter(e=>"number"==typeof e.value).sort((e,t)=>t.value-e.value);return(0,r.jsxs)("div",{className:o.A.tooltip,children:[(0,r.jsx)("div",{className:o.A.tooltipTitle,children:a}),s.map(e=>(0,r.jsxs)("div",{className:o.A.tooltipRow,children:[(0,r.jsxs)("span",{className:o.A.tooltipKey,children:[(0,r.jsx)("span",{className:o.A.legendDot,style:{background:e.stroke}}),l.by[e.dataKey]]}),(0,r.jsxs)("span",{className:o.A.tooltipValue,children:[e.value.toFixed(3),"%"]})]},e.dataKey))]})}function u(){return(0,r.jsx)(n.A,{fallback:(0,r.jsx)("div",{className:o.A.fallback,children:"Loading chart..."}),children:()=>(0,r.jsx)(c,{})})}},27701(e,t,a){a.d(t,{A:()=>m});var s=a(96540),n=a(78478),i=a(86025),o=a(46414),l=a(40160),r=a(74848);const c=[{key:"p50",label:"P50 delay",unit:" slots"},{key:"p95",label:"P95 delay",unit:" slots"},{key:"p99",label:"P99 delay",unit:" slots"},{key:"fc_pct",label:"1-slot FCR rate",unit:"%"}];function d(){const e=(0,i.Ay)("/data/blog/fcr-simulator/timeseries.json"),[t,a]=(0,s.use
1State)(null),[n,l]=(0,s.useState)(null),[c,d]=(0,s.useState)("fc_pct"),[u,m]=(0,s.useState)({lighthouse:!0,lodestar:!0,grandine:!0,teku:!0});return(0,s.useEffect)(()=>{let t=!1;return fetch(e).then(e=>{if(!e.ok)throw new Error(`HTTP ${e.status}`);return e.json()}).then(e=>!t&&a(e)).catch(e=>!t&&l(String(e))),()=>{t=!0}},[e]),n?(0,r.jsxs)("div",{className:o.A.fallback,children:["Failed to load chart data: ",n]}):t?(0,r.jsx)(h,{json:t,metric:c,setMetric:d,enabled:u,setEnabled:m}):(0,r.jsx)("div",{className:o.A.fallback,children:"Loading time series..."})}function h({json:e,metric:t,setMetric:n,enabled:i,setEnabled:d}){const{ResponsiveContainer:h,LineChart:m,Line:p,XAxis:f,YAxis:x,CartesianGrid:g,Tooltip:j,ReferenceLine:b}=a(68025),y=(0,s.useMemo)(()=>{const a=new Set;for(const t of l.VO)for(const s of e.data[t]??[])a.add(s.date);return Array.from(a).sort().map(a=>{const s={date:a};for(const n of l.VO){const i=(e.data[n]??[]).find(e=>e.date===a);i&&(s[n]=i[t])}return s})},[e,t]),v="fc_pct"===t,k=(0,s.useMemo)(()=>{const e=new Set,t=[];for(const a of y){const s=a.date.slice(0,7);e.has(s)||(e.add(s),t.push(a.date))}return t},[y]);return(0,r.jsxs)("div",{className:o.A.card,children:[(0,r.jsxs)("div",{className:o.A.header,children:[(0,r.jsxs)("div",{children:[(0,r.jsx)("p",{className:o.A.title,children:"Confirmation distance over time"}),(0,r.jsx)("p",{className:o.A.subtitle,children:"Daily roll-up. Gaps are days inside a known xatu pipeline outage window (those days are dropped from the underlying data)."})]}),(0,r.jsx)("div",{className:o.A.controls,children:(0,r.jsxs)("div",{className:o.A.controlGroup,children:[(0,r.jsx)("span",{className:o.A.controlLabel,children:"Metric"}),c.map(e=>(0,r.jsx)("button",{type:"button",className:`${o.A.controlButton} ${t===e.key?o.A.controlButtonActive:""}`,onClick:()=>n(e.key),children:e.label},e.key))]})})]}),(0,r.jsx)("div",{className:o.A.legend,children:l.VO.map(e=>(0,r.jsxs)("button",{type:"button",className:`${o.A.legendItem} ${i[e]?"":o.A.legendItemOff}`,onClick:()=>d({...i,[e]:!i[e]}),children:[(0,r.jsx)("span",{className:o.A.legendDot,style:{background:l.sU[e]}}),l.by[e]]},e))}),(0,r.jsx)("div",{className:o.A.chartWrap,style:{height:380},children:(0,r.jsx)(h,{width:"100%",height:"100%",children:(0,r.jsxs)(m,{data:y,margin:{top:6,right:16,bottom:8,left:0},children:[(0,r.jsx)(g,{stroke:"var(--ifm-color-emphasis-200)",strokeDasharray:"3 3"}),(0,r.jsx)(f,{dataKey:"date",ticks:k,tick:{fontSize:10,fill:"var(--ifm-color-emphasis-700)"},tickFormatter:e=>e.slice(0,7)}),(0,r.jsx)(x,{tick:{fontSize:11,fill:"var(--ifm-color-emphasis-700)"},tickFormatter:e=>v?`${e.toFixed(1)}%`:e.toFixed(0),width:56,domain:v?[80,100]:["auto","auto"]}),(0,r.jsx)(b,{x:"2025-12-04",stroke:"#dc2626",strokeWidth:2,strokeDasharray:"5 4",label:{value:"Fusaka instability",position:"insideTopLeft",fill:"#dc2626",fontSize:11,fontWeight:600}}),(0,r.jsx)(j,{content:(0,r.jsx)(u,{metric:t})}),l.VO.map(e=>i[e]?(0,r.jsx)(p,{type:"monotone",dataKey:e,stroke:l.sU[e],strokeWidth:1.5,dot:!1,activeDot:{r:3},isAnimationActive:!1,connectNulls:!1},e):null)]})})}),(0,r.jsx)("p",{className:o.A.footnote,children:"Gaps in the lines are days that fell inside a xatu pipeline outage window."})]})}function u({active:e,payload:t,label:a,metric:s}){if(!e||!t||0===t.length)return null;const n=t.filter(e=>"number"==typeof e.value),i=c.find(e=>e.key===s).unit;return(0,r.jsxs)("div",{className:o.A.tooltip,children:[(0,r.jsx)("div",{className:o.A.tooltipTitle,children:a}),n.map(e=>(0,r.jsxs)("div",{className:o.A.tooltipRow,children:[(0,r.jsxs)("span",{className:o.A.tooltipKey,children:[(0,r.jsx)("span",{className:o.A.legendDot,style:{background:e.stroke}}),l.by[e.dataKey]]}),(0,r.jsxs)("span",{className:o.A.tooltipValue,children:[e.value.toFixed("%"===i?2:1),i]})]},e.dataKey))]})}function m(){return(0,r.jsx)(n.A,{fallback:(0,r.jsx)("div",{className:o.A.fallback,children:"Loading chart..."}),children:()=>(0,r.jsx)(d,{})})}},37983(e,t,a){a.r(t),a.d(t,{assets:()=>h,contentTitle:()=>d,default:()=>p,frontMatter:()=>c,metadata:()=>s,toc:()=>u});var s=a(85339),n=a(74848),i=a(28453),o=a(13574),l=a(508),r=a(27701);const c={slug:"fcr-simulator",title:"Replaying a year of mainnet with the Fast Confirmation Rule",authors:["samcm","savid"],description:"We replayed 12 months of mainnet beacon chain history through 4 consensus-layer implementations of the Fast Confirmation Rule.",tags:["fcr","consensus-layer","xatu","kubernetes","data"],image:"/img/blog/fcr-simulator/header.png",githubRepos:[{name:"fcr-simulator",url:"https://github.com/ethpandaops/fcr-simulator"}]},d=void 0,h={authorsImageUrls:[void 0,void 0]},u=[{value:"The simulator",id:"the-simulator",level:2},{value:"The data",id:"the-data",level:2},{value:"Running the simulations",id:"running-the-simulations",level:2},{value:"Results",id:"results",level:2},{value:"Coverage",id:"coverage",level:3},{value:"What we found",id:"what-we-found",level:3},{value:"Reproducibility",id:"reproducibility",level:2}];function m(e){const t={a:"a",admonition:"admonition",code:"code",em:"em",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsxs)(t.p,{children:["We built ",(0,n.jsx)(t.a,{href:"https://github.com/ethpandaops/fcr-simulator",children:"fcr-simulator"})," to answer a simple question: if the ",(0,n.jsx)(t.a,{href:"https://fastconfirm.it",children:"Fast Confirmation Rule"})," had already been running on mainnet, what would it have done?"]}),"\n",(0,n.jsx)(t.p,{children:"To study this, we replayed historical mainnet beacon chain data through 4 consensus-layer client implementations of FCR. Each client saw the same blocks, the same attestations, and the same cutoff rules. We then compared how each implementation classified each slot."}),"\n",(0,n.jsx)(t.p,{children:"This post describes the simulator, the dataset, the infrastru
1cture we used to run the replays, and the results from the v0.2.0 dataset."}),"\n",(0,n.jsxs)(t.p,{children:["For background on FCR itself, ",(0,n.jsx)(t.a,{href:"https://fastconfirm.it",children:"fastconfirm.it"})," is the best place to start."]}),"\n",(0,n.jsx)(t.h2,{id:"the-simulator",children:"The simulator"}),"\n",(0,n.jsx)(t.p,{children:"The simulator is intentionally narrow. Its job is to feed the same mainnet history and attestation data into multiple FCR implementations, then record the result for every slot."}),"\n",(0,n.jsxs)(t.p,{children:["At the centre of the system is an ",(0,n.jsx)(t.code,{children:"orchestrator"}),". The orchestrator takes a job specification, starts an engine such as ",(0,n.jsx)(t.code,{children:"lighthouse"}),", and provides that engine with the data it needs. The engine calls back to the orchestrator over HTTP."]}),"\n",(0,n.jsx)(t.p,{children:"This separation is a large step for reproducibility. The orchestrator is responsible for fetching blocks and attestations, while each client implementation is responsible for applying its FCR logic. That keeps the multi-client simulator easier to reason about, and it means we can swap or replay datasources without reimplementing data loading separately in every client."}),"\n",(0,n.jsx)(t.p,{children:'We also "warm" the beacon nodes before the simulation run to hydrate their fork choice store and any caches. For the run below, we warmed each beacon node with 10 epochs worth of attestations.'}),"\n",(0,n.jsx)("img",{src:"/img/blog/fcr-simulator/diagram-slot-loop.png",alt:"Diagram: orchestrator slot loop driving an engine subprocess"}),"\n",(0,n.jsx)(t.h2,{id:"the-data",children:"The data"}),"\n",(0,n.jsx)(t.p,{children:"The simulator uses data we already had from other ethpandaops infrastructure:"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:[(0,n.jsx)(t.strong,{children:"Blocks"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:["Canonical blocks from ",(0,n.jsx)(t.code,{children:"era"})," files, using the Nimbus team\u2019s mirror"]}),"\n",(0,n.jsxs)(t.li,{children:["Orphaned blocks from ",(0,n.jsx)(t.a,{href:"https://tracoor.mainnet.ethpandaops.io",children:"ethpandaops/tracoor"})]}),"\n"]}),"\n"]}),"\n",(0,n.jsxs)(t.li,{children:[(0,n.jsx)(t.strong,{children:"Attestations"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsxs)(t.li,{children:["Single Attestations from ",(0,n.jsx)(t.a,{href:"https://github.com/ethpandaops/xatu",children:"ethpandaops/xatu"})," that voted for each block before the 12 second cutoff"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.strong,{children:"The attestation dataset is not a perfect recreation of what a beacon node would have experienced at the time"}),". For some slots, this view of the network may report more attestations than your average beacon node. Conversely, for other slots, this view may have less attestations. This data also completely ignores ",(0,n.jsx)(t.em,{children:"aggregate"})," attestations. We assume that attestation aggregation is steady and reliable on Mainnet as this is where most nodes would get 62/64 of their attestations from."]}),"\n",(0,n.jsx)(t.h2,{id:"running-the-simulations",children:"Running the simulations"}),"\n",(0,n.jsx)(t.p,{children:"Replaying a year of mainnet through fork choice one slot at a time would be too slow to be useful. The slot state transition is expensive, and repeated queries against the Xatu database would add more overhead."}),"\n",(0,n.jsx)(t.p,{children:"To make the experiment practical, we ran the replay as a sharded Kubernetes job. We also ran a one-off query against Xatu and stored the result in our in-house S3 cluster. This gave every run the same input data and avoided repeatedly querying the database during simulation."}),"\n",(0,n.jsx)("img",{src:"/img/blog/fcr-simulator/diagram-sharded-replay.png",alt:"Diagram: sharded replay topology across the k3s cluster"}),"\n",(0,n.jsx)(t.p,{children:"We ran the simulations in 1 to 3 month epoch ranges. This was mostly a practical choice: if a job failed near the end of a long run, we wanted to limit how much work could be lost or need to be discarded."}),"\n",(0,n.jsxs)(t.p,{children:["After benchmarking, we ran 96 beacon nodes across 6 ",(0,n.jsx)(t.a,{href:"https://www.hetzner.com/dedicated-rootserver/ax162-r/configurator/",children:"Hetzner AX162"})," servers. Each server ran 16 beacon nodes. We also mounted each client database into RAMFS to minimize the expensive disk reads and writes."]}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.strong,{children:"The workload was heavily memory bound, but this setup still gave us a roughly 25x to 35x speedup compared with a sequential replay."})}),"\n",(0,n.jsx)(t.h2,{id:"results",children:"Results"}),"\n",(0,n.jsx)(t.admonition,{title:"Caveat",type:"warning",children:(0,n.jsx)(t.p,{children:"About 31 hours of slots, or 0.36% of the dataset, are excluded from every chart and number below. This consists of 9 Xatu pipeline issues totalling about 22 hours, and one 8.4 hour simulator feeder bug on 2026-04-01."})}),"\n",(0,n.jsx)(t.h3,{id:"coverage",children:"Coverage"}),"\n",(0,n.jsxs)(t.table,{children:[(0,n.jsx)(t.thead,{children:(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.th,{children:"Client"}),(0,n.jsx)(t.th,{children:"Epoch range"}),(0,n.jsx)(t.th,{children:"Dates (UTC)"}),(0,n.jsx)(t.th,{children:"Slots"})]})}),(0,n.jsxs)(t.tbody,{children:[(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"Lighthouse"}),(0,n.jsx)(t.td,{children:"369,000 to 449,000"}),(0,n.jsx)(t.td,{children:"2025-05-29 to 2026-05-20"}),(0,n.jsx)(t.td,{children:"2,560,000"})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"Lodestar"}),(0,n.jsx)(t.td,{children:"417,500 to 449,000"}
1),(0,n.jsx)(t.td,{children:"2025-12-31 to 2026-05-20"}),(0,n.jsx)(t.td,{children:"1,008,000"})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"Grandine"}),(0,n.jsx)(t.td,{children:"423,800 to 449,000"}),(0,n.jsx)(t.td,{children:"2026-01-28 to 2026-05-20"}),(0,n.jsx)(t.td,{children:"806,400"})]}),(0,n.jsxs)(t.tr,{children:[(0,n.jsx)(t.td,{children:"Teku"}),(0,n.jsx)(t.td,{children:"423,800 to 449,000"}),(0,n.jsx)(t.td,{children:"2026-01-28 to 2026-05-20"}),(0,n.jsx)(t.td,{children:"806,400"})]})]})]}),"\n",(0,n.jsx)(o.A,{}),"\n",(0,n.jsx)(l.A,{}),"\n",(0,n.jsx)(r.A,{}),"\n",(0,n.jsx)(t.h3,{id:"what-we-found",children:"What we found"}),"\n",(0,n.jsxs)(t.p,{children:["The most important number first: ",(0,n.jsx)(t.strong,{children:"zero false confirmations"}),". Across 5.15 million slot evaluations and all four clients, no implementation ever fast-confirmed a block that turned out to be non-canonical on the finalized chain. The simulator checks each engine's ",(0,n.jsx)(t.code,{children:"confirmed_root"})," against the ERA-file canonical block at that slot during the merge step, and across the entire 12-month period (including the December 4 2025 Fusaka mainnet instability) there are zero mismatches."]}),"\n",(0,n.jsxs)(t.p,{children:["Headline rate: across 800,000 mainnet slots in the comparable window, roughly ",(0,n.jsx)(t.strong,{children:"96 out of every 100 slots would have been fast-confirmed within 12 seconds"}),"."]}),"\n",(0,n.jsxs)(t.p,{children:["Three of the four clients agree almost exactly on which slots qualify. Lighthouse and Grandine report identical 1-slot rates to four decimal places (",(0,n.jsx)(t.strong,{children:"95.8165%"}),") and agree on every single slot in the shared window. Lodestar lands at ",(0,n.jsx)(t.strong,{children:"95.8164%"}),", which is a single-slot difference across ~800k. Effectively identical."]}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.strong,{children:"Teku is the odd one out."})," It reports ",(0,n.jsx)(t.strong,{children:"97.3926%"}),", marking ~12,500 ",(0,n.jsx)(t.em,{children:"more"})," slots as fast-confirmed than the others over the same window."]}),"\n",(0,n.jsx)(t.h2,{id:"reproducibility",children:"Reproducibility"}),"\n",(0,n.jsx)(t.p,{children:"Every CSV in the v0.2.0 release is pinned to a specific engine image tag. Each engine image tag is pinned to an upstream git ref."}),"\n",(0,n.jsxs)(t.p,{children:["The full mapping is in the release ",(0,n.jsx)(t.code,{children:"METADATA.json"}),", including:"]}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"schema"}),"\n",(0,n.jsx)(t.li,{children:"run parameters"}),"\n",(0,n.jsx)(t.li,{children:"attestation source mode"}),"\n",(0,n.jsx)(t.li,{children:"per-CSV image SHA"}),"\n",(0,n.jsx)(t.li,{children:"per-engine upstream commit"}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:"You can download the dataset with:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"curl -L https://github.com/ethpandaops/fcr-simulator/releases/download/v0.2.0/fcr-data-2026-05-26.tar -o fcr-data.tar\ntar -xf fcr-data.tar\n"})}),"\n",(0,n.jsxs)(t.p,{children:["The orchestrator and engines are available at ",(0,n.jsx)(t.a,{href:"https://github.com/ethpandaops/fcr-simulator",children:"https://github.com/ethpandaops/fcr-simulator"})]}),"\n",(0,n.jsx)(t.p,{children:"Love,"}),"\n",(0,n.jsx)(t.p,{children:"EthPandaOps Team"})]})}function p(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(m,{...e})}):m(e)}},40160(e,t,a){a.d(t,{VO:()=>s,by:()=>n,sU:()=>i});const s=["lighthouse","lodestar","grandine","teku"],n={lighthouse:"Lighthouse",lodestar:"Lodestar",grandine:"Grandine",teku:"Teku"},i={lighthouse:"#4d8df6",lodestar:"#f59e0b",grandine:"#a855f7",teku:"#ef4444"}},46414(e,t,a){a.d(t,{A:()=>s});const s={card:"card_ruE0",header:"header_zR3_",title:"title_uVkw",subtitle:"subtitle_VnlO",controls:"controls_D4lI",controlGroup:"controlGroup_gr5n",controlLabel:"controlLabel_ZWVl",controlButton:"controlButton_DGeQ",controlButtonActive:"controlButtonActive_JoZb",legend:"legend_blfK",legendItem:"legendItem_Z68t",legendItemOff:"legendItemOff_YMPg",legendDot:"legendDot_Fwds",tooltip:"tooltip_Rb8y",tooltipTitle:"tooltipTitle_xHdU",tooltipRow:"tooltipRow_maAY",tooltipKey:"tooltipKey_a7r6",tooltipValue:"tooltipValue_hZS4",footnote:"footnote_vBYq",chartWrap:"chartWrap_QUeW",percentileTable:"percentileTable_WFC9",percentileCell:"percentileCell_WNL0",percentileCellHead:"percentileCellHead_JIaP",percentileValue:"percentileValue_mV_K",fallback:"fallback_nFhQ"}},85339(e){e.exports=JSON.parse('{"permalink":"/posts/fcr-simulator","source":"@site/blog/2026-05-26-fcr-simulator/index.md","title":"Replaying a year of mainnet with the Fast Confirmation Rule","description":"We replayed 12 months of mainnet beacon chain history through 4 consensus-layer implementations of the Fast Confirmation Rule.","date":"2026-05-26T00:00:00.000Z","tags":[{"inline":true,"label":"fcr","permalink":"/posts/tags/fcr"},{"inline":true,"label":"consensus-layer","permalink":"/posts/tags/consensus-layer"},{"inline":true,"label":"xatu","permalink":"/posts/tags/xatu"},{"inline":true,"label":"kubernetes","permalink":"/posts/tags/kubernetes"},{"inline":true,"label":"data","permalink":"/posts/tags/data"}],"readingTime":5.13,"hasTruncateMarker":false,"authors":[{"name":"samcm","title":"DevOps Engineer","bio":"DevOps Engineer","url":"https://github.com/samcm","github":"https://github.com/samcm","t
1witter":"https://x.com/samcmAU","imageURL":"/img/team/samcm.jpeg","key":"samcm","page":null},{"name":"savid \ud83d\ude82","title":"DevOps Engineer","bio":"DevOps Engineer","url":"https://github.com/savid","github":"https://github.com/savid","twitter":"https://x.com/savid","imageURL":"/img/team/savid.jpeg","key":"savid","page":null}],"frontMatter":{"slug":"fcr-simulator","title":"Replaying a year of mainnet with the Fast Confirmation Rule","authors":["samcm","savid"],"description":"We replayed 12 months of mainnet beacon chain history through 4 consensus-layer implementations of the Fast Confirmation Rule.","tags":["fcr","consensus-layer","xatu","kubernetes","data"],"image":"/img/blog/fcr-simulator/header.png","githubRepos":[{"name":"fcr-simulator","url":"https://github.com/ethpandaops/fcr-simulator"}]},"unlisted":false,"prevItem":{"title":"Panda: one tool for the whole data stack","permalink":"/posts/panda"},"nextItem":{"title":"Validator Report: Investigate Validator Performance","permalink":"/posts/validator-report"}}')}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.