fix(consensus): score misbehavior for blocks with duplicate transactions - #11157
Open
jiehuo100net wants to merge 4 commits into
Open
Conversation
`BlockError::misbehavior_score()` returned 0 for `DuplicateTransaction`, because that variant was not in the explicit scoring arms and fell through to the `_other => 0` catch-all. A block with duplicate transaction hashes is the CVE-2012-2459 Merkle-malleability case: it is unambiguously invalid, and honest nodes never produce one. Both verifiers run `merkle_root_validity()`, so a peer could advertise such a block on either path without being scored. Score it as definitive misbehavior, alongside the other unambiguous block-validity violations.
…score # Conflicts: # CHANGELOG.md
…-transaction-misbehavior-score # Conflicts: # zebra-consensus/CHANGELOG.md
…-transaction-misbehavior-score
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Closes #10688.
BlockError::misbehavior_score()returns 0 forDuplicateTransaction: the variant is not in theexplicit scoring arms and falls through to the
_other => 0catch-all. The syncer only forwards ascore to the misbehaviour channel when it is non-zero, so a peer that advertises a block containing
duplicate transactions is not scored at all, while every other definitive block-validity violation
scores 100.
A block with duplicate transaction hashes is the Merkle malleability case from CVE-2012-2459:
duplicating the trailing transaction of an odd-length list can reproduce the honest block's Merkle
root, which is why
merkle_root_validity()checks for duplicates right after theBadMerkleRootcheck. An honest node never produces such a block, so it is exactly the kind of unambiguous signal
the 100-point arms are for.
Both verifiers reach that check — the semantic verifier in
block.rs, and the checkpoint verifierbefore it queues a block — so the gap applies to the whole sync range, not only to full verification
near the tip.
A scoring arm consistent with that policy already exists, but it is unreachable on the current error
path:
VerifyCheckpointError::DuplicateTransactionscores 100, yet that variant is never constructedanywhere in the repository. The error from
merkle_root_validity()reachesVerifyCheckpointErrorthrough
From<BlockError>, which wraps it asVerifyBlock(..)and forwards the score straight backto
BlockError::misbehavior_score()— so the 100 in that arm is dead today.Solution
Add
DuplicateTransactionto the 100-point arm, as the issue's "Suggested fix" section proposes.The issue also asks for the rest of the match to be audited rather than fixing this one arm. All 14
BlockErrorvariants, ondcf12fe84:MissingHeight,MaxHeight,InvalidDifficulty,TargetDifficultyLimit,DifficultyFilter,NoTransactions,BadMerkleRoot,WrongTransactionConsensusBranchId,TooManyTransparentSignatureOperationsTransaction(err)forwards toTransactionError::mempool_misbehavior_score()_other => 0:DuplicateTransaction— scored hereAlreadyInChain— correctly 0: it is a duplicate request, andis_duplicate_request()relies onthat classification
Other(String)— correctly 0 as an unclassified catch-allSummingMinerFees— left unchanged, but it looks inconsistent, see belowSummingMinerFeeswraps theamount::Errorfrom accumulating the block's miner fees, and scores 0.The summed total is then passed to
check::miner_fees_are_valid(), and related overflows in thataccounting path are converted to
SubsidyError::Overflow, which scores 100 viaTransaction(Subsidy(_));impl From<amount::Error> for BlockErrorroutes anamount::Errorthesame way. So amount-arithmetic failures on a peer-provided block score 0 or 100 depending on which
call site produced them. That is a behaviour decision rather than a completeness bug, so this PR does
not change it — happy to follow up if you want it scored.
Two things deliberately not touched:
VerifyCheckpointError::DuplicateTransactionandVerifyCheckpointError::BadMerkleRootvariants. Removing a variant from a public enum is abreaking change, and it is unrelated to the scoring gap.
#10616. Until the inbound gossip path downcasts to the error type it actually receives, itforwards no score at all, so this fix takes effect on the syncer path only.
Tests
zebra-consensus/src/block/tests.rs:block_error_misbehavior_scores— one assertion added for the bareBlockErrorvalue.duplicate_transaction_scores_misbehavior_through_both_verifier_paths— builds a block withduplicate transaction hashes the same way the existing
merkle_root_validity_rejects_duplicate_transaction_hashtest does (duplicate coinbasetransaction, Merkle root recomputed from the duplicated list so the block passes the
BadMerkleRootcheck and reaches the duplicate check), takes the real error out ofmerkle_root_validity(), wraps thatBlockErrorexactly as the semantic and checkpoint paths do,and asserts the score is 100 after each wrapper:
VerifyBlockErrorthenRouterErrorfor thesemantic path, and
VerifyCheckpointErrorthenRouterErrorfor the checkpoint path. It does notstart either verifier service; it exercises the wrapper chain the syncer reads the score from,
because asserting only on the bare
BlockErrorwould not show that the score survives to thechannel.
Gates run locally on this branch, in debug:
Control: with the one-line change reverted and the tests kept, both
block_error_misbehavior_scoresandduplicate_transaction_scores_misbehavior_through_both_verifier_pathsfail (0 where 100 isexpected). So the new assertions are bound to this change rather than passing for an unrelated
reason.
Specifications & References
BlockError::DuplicateTransactionscores zero misbehavior #10688, including its "Suggested fix" sectionblock::check::merkle_root_validity()MAX_PEER_MISBEHAVIOR_SCOREinzebra-network/src/constants.rs, and the ban threshold inAddressBook::update()Follow-up Work
SummingMinerFees(above): score it, or document why the two call sites differ.VerifyCheckpointError::DuplicateTransaction(declared, scored 100, never constructed) andVerifyCheckpointError::BadMerkleRoot(declared, never constructed, and not in the scoring armsat all).
AI Disclosure
change, the audit, the assertions and the local test evidence were reviewed and verified by me.
PR Checklist
type(scope): description