Skip to content

Commit 3bbf387

Browse files
committed
finesse
1 parent 429ec38 commit 3bbf387

18 files changed

Lines changed: 1190 additions & 146 deletions

docs/architecture.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ It does not call the slashveto.me API, depend on a backend snapshot, or possess
4343
notification credentials. A user can enter their addresses again and recover
4444
the L1 portion of each case when the backend is unavailable.
4545

46+
Execution history is progressive and RPC-aware. Monitor reads present contract
47+
state first, then scans `RoundExecuted` logs newest-to-oldest within the useful
48+
execution window. It starts with small ranges, grows fast successful ranges,
49+
shrinks provider-rejected ranges, and pauses on rate limits while retaining
50+
completed work for the page session. Each refresh scans only new blocks plus a
51+
small reorg overlap.
52+
The page displays exact coverage. Until a receipt is inspected, the case says
53+
that its outcome is still being scanned; it never treats missing coverage as
54+
proof that no `Slashed` log exists. A stronger user-supplied RPC can complete
55+
the same bounded scan faster without changing case semantics.
56+
4657
Its unavoidable limitations are visible:
4758

4859
- an L1-only case starts at voting; it cannot see duty misses or node
@@ -207,6 +218,9 @@ Visible sequencer addresses open the matching mainnet or testnet Dashtec page.
207218
The frontend must show source time and freshness independently. “Confirmed”
208219
means an L1 fact observed at the stated block and confirmation policy; it does
209220
not mean Ethereum-finalized unless the block itself is finalized.
221+
For Monitor, receipt coverage is also explicit: `scanning`, `paused`,
222+
`inspected`, and `complete without finding the expected event` are different
223+
states.
210224

211225
## Canonical contract discovery
212226

docs/v3-plan.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ PINGME receives projected cases. Monitor feeds browser-collected L1
176176
observations through the same reducer. Raw evidence is progressive disclosure:
177177
users need not understand vote words, round offsets, or payload clones to read
178178
the headline, but those facts remain available and linked to Etherscan.
179+
Monitor loads current state first and progressively scans execution receipts
180+
newest-to-oldest. Successful RPC ranges grow, rejected ranges shrink, rate
181+
limits pause without discarding progress, and later polls cover only new blocks
182+
plus a reorg overlap. The UI reports scan coverage and keeps an uninspected
183+
receipt distinct from an inspected receipt with no `Slashed` log.
179184
Visible sequencer addresses link to the matching Dashtec network.
180185
Watchlist cases appear before the public case feed. PINGME places source health
181186
last because it describes the reliability of the whole page.

shared/protocol/cases.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,62 @@ describe('case projection', () => {
101101
});
102102
});
103103

104+
it('keeps a requested amount visible while the browser scans the receipt', () => {
105+
const cases = projectCases([
106+
observation('executed', 'l1_round', {
107+
round: '12',
108+
status: 'executed',
109+
isExecuted: true,
110+
amount: '2000000000000000000000',
111+
executionReceiptStatus: 'scanning',
112+
}, 'ethereum_l1'),
113+
], protocol());
114+
115+
expect(cases[0].state).toMatchObject({
116+
stage: 'executed',
117+
headline: 'Round executed · 2,000 AZTEC requested',
118+
requestedAmount: '2000000000000000000000',
119+
});
120+
expect(cases[0].state.explanation).toContain(
121+
'scanning for its execution receipt',
122+
);
123+
});
124+
125+
it('uses an inspected execution receipt as a terminal non-slash outcome', () => {
126+
const cases = projectCases([
127+
observation('executed', 'l1_round', {
128+
round: '12',
129+
status: 'executed',
130+
isExecuted: true,
131+
amount: '2000000000000000000000',
132+
executionReceiptStatus: 'inspected',
133+
}, 'ethereum_l1'),
134+
{
135+
...observation('receipt', 'l1_execution', {
136+
round: '12',
137+
slashCount: '1',
138+
actionIndex: 0,
139+
}, 'ethereum_l1'),
140+
provenance: {
141+
observedAt: '2026-07-29T12:01:00.000Z',
142+
blockNumber: '123',
143+
blockHash: `0x${'12'.repeat(32)}`,
144+
transactionHash: `0x${'34'.repeat(32)}`,
145+
canonical: true,
146+
},
147+
},
148+
], protocol());
149+
150+
expect(cases[0].state).toMatchObject({
151+
stage: 'resolved',
152+
headline: 'Round executed · 2,000 AZTEC requested',
153+
active: false,
154+
});
155+
expect(cases[0].state.explanation).toContain(
156+
'contains no Rollup Slashed log',
157+
);
158+
});
159+
104160
it('treats escape-hatch exclusion as a terminal non-slash outcome', () => {
105161
const cases = projectCases([
106162
observation('escaped', 'l1_round', {

shared/protocol/cases.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,10 @@ function deriveState(
245245
);
246246
}
247247

248+
const execution = latest(canonical, 'l1_execution');
248249
const round = latest(canonical, 'l1_round');
249250
if (round) {
250-
return roundState(round, reason);
251+
return roundState(round, reason, execution ?? undefined);
251252
}
252253

253254
const offense = latest(canonical, 'node_offense');
@@ -313,7 +314,11 @@ function deriveState(
313314
);
314315
}
315316

316-
function roundState(observation: Observation, reason: CaseReason): CaseState {
317+
function roundState(
318+
observation: Observation,
319+
reason: CaseReason,
320+
execution?: Observation,
321+
): CaseState {
317322
const data = observation.data;
318323
const round = readString(data.round) ?? observation.round ?? null;
319324
const amount = readString(data.amount);
@@ -351,10 +356,55 @@ function roundState(observation: Observation, reason: CaseReason): CaseState {
351356
common,
352357
);
353358
}
359+
if (execution) {
360+
return state(
361+
'resolved',
362+
'normal',
363+
`Round executed · ${formatAztec(amount)} AZTEC requested`,
364+
'The execution receipt was inspected and contains no Rollup Slashed log for this sequencer.',
365+
reason,
366+
false,
367+
common,
368+
);
369+
}
370+
const receiptStatus = readString(data.executionReceiptStatus);
371+
if (receiptStatus === 'scanning') {
372+
return state(
373+
'executed',
374+
'critical',
375+
`Round executed · ${formatAztec(amount)} AZTEC requested`,
376+
'Contract state marks this round executed. This page is scanning for its execution receipt.',
377+
reason,
378+
false,
379+
common,
380+
);
381+
}
382+
if (receiptStatus === 'paused') {
383+
return state(
384+
'executed',
385+
'critical',
386+
`Round executed · ${formatAztec(amount)} AZTEC requested`,
387+
'Contract state marks this round executed. The RPC paused before this page located its execution receipt.',
388+
reason,
389+
false,
390+
common,
391+
);
392+
}
393+
if (receiptStatus === 'unavailable') {
394+
return state(
395+
'executed',
396+
'critical',
397+
`Round executed · ${formatAztec(amount)} AZTEC requested`,
398+
'Contract state marks this round executed, but its receipt was not found inside the completed history window.',
399+
reason,
400+
false,
401+
common,
402+
);
403+
}
354404
return state(
355405
'executed',
356406
'critical',
357-
'Slashing round executed',
407+
`Round executed · ${formatAztec(amount)} AZTEC requested`,
358408
'The action payload was called. A Rollup Slashed log is still required to confirm this sequencer’s deduction.',
359409
reason,
360410
false,

shared/protocol/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type ObservationKind =
77
| 'inactivity_epoch'
88
| 'node_offense'
99
| 'l1_round'
10+
| 'l1_execution'
1011
| 'l1_slash'
1112
| 'stake_status';
1213

src/components/CaseFeed.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export function CaseFeed({
1616
cases,
1717
protocol,
1818
selectedCaseId,
19-
evidenceMode,
2019
onOpenProtocolGuide,
2120
}: {
2221
cases: SlashingCase[];
@@ -38,12 +37,6 @@ export function CaseFeed({
3837
<h2 className="text-3xl font-black text-whisper-white">
3938
Case feed
4039
</h2>
41-
<p className="mt-2 max-w-4xl text-sm font-bold text-whisper-white/70">
42-
{evidenceMode === 'backend'
43-
? 'Node evidence and L1 status from PINGME.'
44-
: 'L1 votes, candidates, execution, and stake outcomes from this browser.'}
45-
{' '}Open cases are followed by recent execution outcomes.
46-
</p>
4740
</div>
4841
<div className="w-full sm:w-auto sm:min-w-72">
4942
<span className="block w-fit border-3 border-brand-black bg-vermillion px-3 py-2 text-sm font-black uppercase text-brand-black sm:ml-auto">
@@ -149,7 +142,7 @@ function PayloadCaseGroup({
149142
{group.payloadAddress
150143
? `${sequencerCount} sequencer${sequencerCount === 1 ? '' : 's'} targeted${
151144
requestedAmount !== null
152-
? ` · ${formatAztec(requestedAmount)} AZTEC requested`
145+
? ` · ${formatAztec(requestedAmount)} AZTEC`
153146
: ''
154147
}`
155148
: primary.state.headline}

src/components/Dashboard.tsx

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import { selectCaseFeed } from '@/lib/caseFeed';
1515
import { urlForWatchlist } from '@/lib/navigation';
1616
import { summarizeNetwork } from '../../shared/protocol/index.ts';
1717
import { useSlashingStore } from '@/store/slashingStore';
18-
import type { MonitorConfigInput } from '@/types/slashing';
18+
import type {
19+
ExecutionHistoryScan,
20+
MonitorConfigInput,
21+
} from '@/types/slashing';
1922
import type { ProtocolSnapshot } from '../../shared/protocol/index.ts';
2023

2124
interface DashboardProps {
@@ -58,7 +61,9 @@ export function Dashboard({
5861
config: store.config,
5962
state: store,
6063
slashings: [...store.detectedSlashings.values()],
64+
confirmedExecutions: store.confirmedExecutions,
6165
confirmedSlashes: store.confirmedSlashes,
66+
executionScan: store.executionScan,
6267
});
6368
})();
6469

@@ -200,7 +205,9 @@ export function Dashboard({
200205
/>
201206
)}
202207

203-
{store.isScanning && (
208+
{store.executionScan.status !== 'idle' ? (
209+
<ExecutionHistoryStatus scan={store.executionScan} />
210+
) : store.isScanning && (
204211
<div className="mb-8 border-5 border-aqua bg-lapis p-5 shadow-brutal-aqua">
205212
<p className="font-black uppercase text-aqua">
206213
Scanning live and historical slashing rounds…
@@ -239,16 +246,6 @@ export function Dashboard({
239246
))}
240247
</div>
241248

242-
{addresses.length === 0 && (
243-
<section className="mb-8 border-5 border-orchid bg-aubergine p-6 shadow-brutal-orchid">
244-
<h2 className="text-2xl font-black text-orchid">Add an address above</h2>
245-
<p className="mt-2 text-sm font-bold text-whisper-white/75">
246-
The network overview is public. Address cards make the protocol
247-
path actionable for your own sequencers.
248-
</p>
249-
</section>
250-
)}
251-
252249
{projected && (
253250
<CaseFeed
254251
cases={projected.cases}
@@ -261,3 +258,61 @@ export function Dashboard({
261258
</main>
262259
);
263260
}
261+
262+
function ExecutionHistoryStatus({ scan }: { scan: ExecutionHistoryScan }) {
263+
const percentage = scan.totalBlocks === 0n
264+
? 100
265+
: Math.min(
266+
100,
267+
Number(scan.scannedBlocks * 10_000n / scan.totalBlocks) / 100,
268+
);
269+
const complete = scan.status === 'complete';
270+
const paused = scan.status === 'paused';
271+
return (
272+
<section className={`mb-8 border-5 p-4 ${
273+
complete
274+
? 'border-chartreuse bg-malachite shadow-brutal-chartreuse'
275+
: paused
276+
? 'border-orchid bg-aubergine shadow-brutal-orchid'
277+
: 'border-aqua bg-lapis shadow-brutal-aqua'
278+
}`}>
279+
<div className="flex flex-wrap items-center justify-between gap-2">
280+
<h2 className={`text-lg font-black ${
281+
complete
282+
? 'text-chartreuse'
283+
: paused ? 'text-orchid' : 'text-aqua'
284+
}`}>
285+
{complete
286+
? 'Execution history complete'
287+
: paused
288+
? 'Execution history paused'
289+
: 'Scanning execution history'}
290+
</h2>
291+
<span className="font-mono text-xs font-black text-whisper-white/65">
292+
{scan.scannedBlocks.toString()} / {scan.totalBlocks.toString()} blocks
293+
</span>
294+
</div>
295+
<div
296+
role="progressbar"
297+
aria-label="Execution history scan coverage"
298+
aria-valuemin={0}
299+
aria-valuemax={100}
300+
aria-valuenow={percentage}
301+
className="mt-3 h-4 border-3 border-whisper-white/40 bg-brand-black"
302+
>
303+
<div
304+
className={`h-full ${
305+
complete
306+
? 'bg-chartreuse'
307+
: paused ? 'bg-orchid' : 'bg-aqua'
308+
}`}
309+
style={{ width: `${percentage}%` }}
310+
/>
311+
</div>
312+
<p className="mt-2 text-xs font-bold text-whisper-white/65">
313+
RPC chunk {scan.chunkSize.toString()} blocks
314+
{paused ? ' · The next refresh will retry, or you can use another RPC.' : ''}
315+
</p>
316+
</section>
317+
);
318+
}

src/components/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function Header({
2222
</div>
2323
<div className="min-w-0">
2424
<h1 className="text-2xl font-black text-chartreuse tracking-tight sm:text-3xl">AZTEC SLASHING MONITOR</h1>
25-
<p className="text-xs text-aqua font-bold uppercase tracking-wider sm:text-sm">Early warning · exact L1 status · operator education</p>
25+
<p className="text-xs text-aqua font-bold uppercase tracking-wider sm:text-sm">Early warning · exact L1 status · operator info</p>
2626
</div>
2727
</div>
2828

src/components/MonitorDetails.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@ export function MonitorDetails({ configInput, network, onResetRpc, onToggleNetwo
117117
<MetadataSection title="Deployment">
118118
<Metadata label="Chain ID" value={configInput.chainId.toString()} />
119119
<Metadata label="Registry" value={configInput.registryAddress} />
120-
<Metadata label="Resolved at block" value={config?.deploymentBlockNumber.toString() ?? unavailable} />
121-
<Metadata label="Resolved at time" value={formatTimestamp(config?.deploymentTimestamp, unavailable)} />
120+
<Metadata label="Resolved at block" value={config?.resolvedAtBlockNumber.toString() ?? unavailable} />
121+
<Metadata label="Resolved at time" value={formatTimestamp(config?.resolvedAtTimestamp, unavailable)} />
122122
<Metadata label="Rollup" value={config?.rollupAddress ?? unavailable} />
123123
<Metadata label="Rollup version" value={config?.rollupVersion.toString() ?? unavailable} />
124124
<Metadata label="Active Slasher" value={config?.slasherAddress ?? unavailable} />

0 commit comments

Comments
 (0)