Skip to content

fix(core): accept SLA rollups with no reports - #596

Merged
rickyrombo merged 1 commit into
mainfrom
fix/sla-rollup-empty-reports
Oct 9, 2026
Merged

rickyrombo merged 1 commit into
mainfrom
fix/sla-rollup-empty-reports

Conversation

@rickyrombo

Copy link
Copy Markdown
Contributor

Problem

isValidRollup compares the reports this node builds with the ones in the proposed rollup using reflect.DeepEqual. createRollup always builds an empty non-nil slice, but the proposed rollup has been through proto unmarshalling, which decodes an empty repeated field as nil. DeepEqual treats those as different.

So when there are no registered validators, the rollup has no reports and fails validation on every node, including the proposer. ProcessProposal rejects it, each round re-proposes the same rollup, and the chain cannot get past the first rollup height.

This never happened on audius-mainnet-alpha-beta because it has always had registered validators. A chain bootstrapped from genesis-writer output starts with an empty core_validators table (the bootstrap validator is in the CometBFT genesis, not in core's registry) and is due its first rollup at its first live block, since the writer's blocks exceed SlaRollupInterval. Booting the 2026-08-25 artifact stalls at height 5840 with Invalid block: invalid rollup and Prevote step: state machine rejected a proposed block every round.

Fix

Move the comparison into a pure rollupsMatch function and treat "no reports on either side" as a match. Non-empty reports keep the original reflect.DeepEqual comparison, so existing chains validate exactly as before.

The empty state is transient on a new chain: with zero registered nodes, registration needs min(0, AttRegistrationMin) = 0 attestations, so the bootstrap node registers itself once blocks are moving, and later rollups carry reports.

Consensus impact

This changes block validation, but only for a case no existing chain can reach. audius-mainnet-alpha-beta always has registered validators, so it always takes the unchanged DeepEqual path. On audius-mainnet-beta no block above the genesis artifact has been committed yet, and every node that syncs it will run a binary with this fix.

Tests

TestRollupsMatch runs each proposed rollup through a real SignedTransaction marshal/unmarshal round trip, the path a proposal takes between PrepareProposal and ProcessProposal. It covers: empty reports (including asserting that decoding yields nil), matching reports, different reports, reports on only one side in both directions, and an off-by-one block range or timestamp.

🤖 Generated with Claude Code

isValidRollup compared the locally built reports with the proposed ones
using reflect.DeepEqual. createRollup builds an empty non-nil slice, but
the proposed rollup has been through proto unmarshalling, which decodes
an empty repeated field as nil, and DeepEqual treats nil and empty as
different. With no registered validators every rollup therefore failed
validation on every node, its proposer included, and the chain could not
advance past the first rollup height.

This never happened on audius-mainnet-alpha-beta because it always had
registered validators. A chain bootstrapped from genesis-writer output
starts with an empty core_validators table and is due a rollup at its
first live block, so the bootstrap node rejects its own proposal forever.

Move the comparison into rollupsMatch and treat empty-on-both-sides as a
match. Non-empty reports keep the original DeepEqual comparison, so
existing chains validate exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rickyrombo
rickyrombo merged commit b91e155 into main Oct 9, 2026
3 checks passed
@rickyrombo
rickyrombo deleted the fix/sla-rollup-empty-reports branch October 9, 2026 05:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant