Commit 2b3f5cb
committed
fix(eth,consensus): gate the proposed-block handler on canonicality and storage
importBlockResults handed the tail of every batch to the proposed-block
handler as long as InsertChain returned nil. A nil error does not mean
the tail is canonical: a fork batch is written as side-chain entries, and
a parked tail is not written at all. The engine did not make up for it:
processQC updates highestQuorumCert, lockQuorumCert and the commit block
before its own existence check, and an existence check cannot tell a
reorged-away block from a canonical one, because the fork stays in the
database. The fetcher and the miner call the same handler and had no gate
at all. The miner is not gated separately: it hands the *core.BlockChain
it already holds to the shared handler, so the engine's canonicality and
storage re-checks cover that path too — a block WriteBlockWithState files
as a side-chain entry is skipped, and no processQC or vote runs on it.
Canonicality is not enough on its own either: it is a property of
the header, and the fast sync header phase marks a height canonical
before its body lands, so a node could processQC and vote for a block
that only ever existed as a header. A master node could therefore end up
voting for, and committing state against, a block it had just reorged
away.
Judge the block once, in one place. consensus.ShouldHandleProposedBlock
reports whether a header is the canonical block at its height and whether
its body is stored, together with the reason it must be skipped and the
canonical hash at that height, both for the skip log. It takes a minimal
CanonicalChain (GetHeaderByNumber alone) that both consensus.ChainReader
and the downloader's BlockChain satisfy, so the callers cannot drift into
two diverging judgments; on the downloader side that costs one addition of
GetHeaderByNumber to its BlockChain interface. The storage half goes
through the optional capability interface consensus.BlockStorer, whose
single method is HasBlock rather than GetBlock: only existence matters,
and GetBlock would read and RLP-decode the whole body of every imported
block. core.BlockChain implements BlockStorer — a compile-time assertion
keeps it from dropping off the type — while a HeaderChain deliberately
does not, so handing a header-only chain to the judgment fails it loudly
as the SkipUnjudgeable skip reason instead of every block silently
failing as not stored. The judgment has no error channel: every outcome
is a skip reason, so each caller collapses to a single !ok branch. The
BlockStorer assertion runs before any chain read, so the unjudgeable
skip is reported at every height: a header-only chain must not get a
silent "not stored" from the canonicality half for the heights it
happens to answer.
The downloader gates importBlockResults on that judgment instead of
open-coding the same rule out of HasBlock and GetCanonicalHash, and logs
the skip with its reason and the canonical hash it observed. The v2 engine
re-checks the same judgment twice inside ProposedBlockHandler: in front of
processQC, and again right before sendVote, because x.lock serialises the
handler but not InsertChain, so a reorg can still land between the two
checks. A judgment that cannot run — a chain type that lost the
BlockStorer half to wrapping or replacement — surfaces as the
SkipUnjudgeable skip, which the handler logs and returns as nil: the
fetcher's import loop treats any handler error as an import failure and
would suppress the broadcast of an already imported block, so a skip
must never surface as an error. That skip means the node stops
processing QCs and voting outright — in production the chain always
implements BlockStorer, so it is a wiring bug. The judgment itself counts
every such skip in a metrics counter (consensus/unjudgeable-proposed-block)
at the single return all callers share, so the liveness halt is observable
beyond the logs on every call site: the downloader's pre-filter hits the
same return before its handler would ever run, where an engine-side-only
counter would stay silent exactly where the wiring bug is easiest to trip.
consensus.SkipLogLevel grades the skips by reason:
SkipUnjudgeable is the one Error — not an observation about the block but
a wiring bug that will skip every block — SkipNonCanonical is a genuine
reorg race and stays at Warn, while the sync-phase skips are routine and
stay at Info so they do not drown the level in noise. The downloader's
own skip log stays at Info on purpose: it is the pre-filter for the
routine cases and correctness rests on the handler's re-checks, so
grading it too would fire a Warn for every fork tail of a sync; its
unjudgeable branch is an explicit Error instead of SkipLogLevel.
The engine's two re-checks now share one counter of their own
(consensus/skipped-proposed-block), incremented at the gate that declined
the block — at most once per block, since a first-gate skip returns
before the second gate. It deliberately counts only the engine gates and
not the downloader's pre-filter: fork tails arriving there are routine
sync noise logged at Info, while a block that got past the pre-filter and
was still declined is exactly the anomaly logs alone could bury. The
reason stays in the skip log; one counter for all reasons keeps the
metric surface flat. And because the same proposal can reach the shared
judgment through both the fetcher and the downloader paths, the
unjudgeable counter's increments are judgment counts, not deduplicated
blocks — its comment says so now. The window comment also names what the
second check leaves open: sendVote itself reads the chain again via
getEpochSwitchInfo before signing and broadcasting, so the residual
window spans that read plus the signature and the broadcast. The fetcher
gate's comment now cross-references the engine side — canonicality is
covered there, so the two gates interlock rather than duplicate — and
points at the post-pivot full import in processFastSyncContent, the
downloader call site the ungated closure's justification rests on.
The fetcher is the one caller whose fast sync calls must not reach the
handler at all. While snapSync runs, it discards propagated blocks before
executing them, so a body already written by the fast sync receipt phase
would pass both halves of the judgment — canonical and stored — and drive
processQC and the vote path on a block whose state transition was never
validated. Its callback therefore carries the same snapSync guard the
inserter and prepare closures already have, instead of asking the
judgment a question the judgment cannot answer. The guard sits in a
fetcher-specific wrapper and not in the shared closure: the same closure
also feeds the downloader, whose fast sync handler calls run after the
pivot commit, on blocks InsertChain has fully executed, and snapSync
stays set until the whole Synchronise returns. Behind that flag the
callback also requires the block's full state, keyed by the block hash
(HasBlockAndFullState) rather than the state root: an empty block's root
can repeat its parent's, so a root-keyed check would leak the parent's
executed state to a block whose import was discarded. Unlike the
judgment's storage half — which reads HasBlock to avoid decoding whole
bodies — this check deliberately pays HasBlockAndFullState's GetBlock
body read plus OpenTrie: proposed blocks arrive at consensus cadence,
orders of magnitude rarer than sync-path blocks, so the fuller check is
affordable here and closes the unexecuted-state hole the cheap HasBlock
check would leave open. Its skips are graded by what they mean: the
fast-sync flag skip is routine and stays at Debug, but a state-half skip
still halts QC processing and voting for that block, so it logs at Warn
and increments its own counter (eth/skipped-proposed-block-state)
— the same logs-alone-could-bury-it discipline as the unjudgeable
counter, so a persistent post-sync liveness stall is observable beyond
the logs.
Tests:
- TestShouldHandleProposedBlock covers the five outcomes of the shared
judgment plus the header-only chain at an absent height, whose
unjudgeable skip is the point of the entry-side assertion; the two
storerless cases also pin that the judgment increments
unjudgeableProposedBlock exactly once per skip, and
TestHeaderChainShouldHandleProposedBlock pins that the header chain's
interface stubs never pass it at any height.
- TestProposedBlockHandlerSkipsNonCanonicalBlock,
TestProposedBlockHandlerSkipsReorgedBlockBeforeProcessQC,
TestProposedBlockHandlerDropsVoteForReorgedBlock and
TestProposedBlockHandlerSkipsBlockWithoutBody cover the two engine
re-checks: highestQuorumCert, lockQC, the timeout certificate, the
voted round and the commit block must stay untouched and no vote may be
broadcast. The two reorg tests inject the reorg through a ChainReader
wrapper that serves a fork header once N judgment gates have completed —
anchored on the gate boundary, where each gate that passes the
canonicality check ends in exactly one HasBlock of the watched height —
so reads added in front of or between the handler's gates stay truthful
and cannot move where the injection lands, instead of coupling the test
to the handler's read ordinals.
- TestProposedBlockHandlerGradesSkipLogLevelByReason pins the log level
of each skip reason.
- TestProposedBlockHandlerSkipsUnjudgeableChain feeds the handler a
ChainReader wrapper without the BlockStorer half — the shape a wrapped
or replaced chain type presents — and pins that the chain skips as
SkipUnjudgeable, logged at Error and returned as nil, not surfaced to a
caller that would treat it as an import failure. No vote may be
broadcast either: that assertion waits out a two-second timeout window
rather than checking BroadcastCh non-blockingly, because a vote travels
through broadcastToBftChannel's own goroutine and an immediate
default-branch check would pass vacuously even if a vote were sent.
- TestFetcherSkipsProposedBlockHandlerDuringFastSync pins the fetcher
gate: with snapSync set the fetcher's callback is inert and logs the
skip, and the same call in full sync reaches the closure without the
skip log. It also reads the handler back off the fetcher through the
test-only HandleProposedBlock accessor and requires it to be the gated
closure itself, so swapping the wiring back to the bare
handleProposedBlock fails loudly instead of staying green. The
state-half skip is pinned with a block forged for real: its header and
body are written straight into the database via rawdb so the
hash-keyed GetBlock half finds it, while its root exists nowhere in
the state database so HasFullState's OpenTrie is the half that fails —
mutating the current header's root alone would change its hash and
fail at the GetBlock half instead, never exercising the state check —
and the counter's one increment per skip is asserted.
- TestImportBlockResultsProposedBlockHandler covers six downloader
shapes: a parked tail, a fully imported batch, a stored fork batch
re-delivered after the local chain grew past it, a head advanced past
the canonical tail by a concurrent import, a heavier fork that stays
canonical, and a fast sync height that is canonical without a body.
- downloadTester gains a canonical number-to-hash table, picked by total
difficulty the way the real chain resolves a reorg, plus hooks to park
a batch tail and to extend it after the insert.
TestStoreBlockCleansStaleCanonicalMarkers,
TestRollbackClearsCanonicalMarkers and
TestInsertChainErrorReportsPosition pin the parts of that table the
downloader tests rely on.
- TestShouldNotSendVoteMsgIfBlockNotExtendedFromAncestor no longer
proposes a forked block, which the new entry re-check now
short-circuits; it proposes a canonical block below the locked ancestor
instead. The fork case moved to
TestShouldNotSendVoteMsgIfCanonicalBlockNotExtendedFromForkedAncestor,
where the parent walk of isExtendingFromAncestor actually runs.1 parent 5d08047 commit 2b3f5cb
13 files changed
Lines changed: 2008 additions & 43 deletions
File tree
- consensus
- XDPoS/engines/engine_v2
- tests/engine_v2_tests
- core
- eth
- downloader
- fetcher
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
30 | 31 | | |
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
34 | 35 | | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
35 | 51 | | |
36 | 52 | | |
37 | 53 | | |
| |||
834 | 850 | | |
835 | 851 | | |
836 | 852 | | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
837 | 883 | | |
838 | 884 | | |
839 | 885 | | |
| |||
856 | 902 | | |
857 | 903 | | |
858 | 904 | | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
859 | 914 | | |
860 | 915 | | |
861 | 916 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
122 | 122 | | |
123 | 123 | | |
124 | 124 | | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
0 commit comments