-
-
Notifications
You must be signed in to change notification settings - Fork 197
security: p2p epoch proposal merkle check accepts self-authenticated fake distributions #2121
Description
P2P Epoch Proposal: Merkle Self-Validation Allows Reward Theft
Summary
GossipLayer._handle_epoch_propose() in node/rustchain_p2p_gossip.py validates the merkle root of an epoch settlement proposal by computing it from the proposal's own distribution field and comparing it to the proposal's own merkle_root. This is tautological — it only proves the proposer didn't make a typo in their own hash. It never verifies that distribution recipients are actually attested miners in miner_attest_recent.
A malicious epoch leader can craft a proposal paying only themselves (or any arbitrary wallet), compute the correct merkle root for that fake distribution, and all receiving nodes will vote "accept" because the merkle self-check passes.
Severity
Critical — consensus bypass. A single compromised leader node can steal all epoch rewards without detection by other validators.
Affected File
node/rustchain_p2p_gossip.py — _handle_epoch_propose() (lines 514-562)
Reproduction
- Set up a 3+ node P2P cluster with shared HMAC secret.
- Wait until a controlled node becomes epoch leader (round-robin by
epoch % num_nodes). - As leader, craft an
EPOCH_PROPOSEmessage withdistribution: {"attacker_wallet": 1.5}and computemerkle_root = sha256(json.dumps(sorted(distribution))). - Broadcast to peers.
- Each peer computes the same merkle hash from the same fake data → votes "accept".
- Quorum reached → epoch commits with all rewards sent to
attacker_wallet.
Root Cause
# Lines 528-534 (before fix)
distribution = proposal.get("distribution", {})
remote_merkle = proposal.get("merkle_root", "")
sorted_dist = sorted(distribution.items())
merkle_data = json.dumps(sorted_dist, sort_keys=True)
local_merkle = hashlib.sha256(merkle_data.encode()).hexdigest()
if remote_merkle != local_merkle:
# reject...
# → Merkle verified - vote to acceptThe local_merkle is computed from distribution in the same proposal as remote_merkle. This proves nothing about the legitimacy of the distribution — only that the proposer's hash function works correctly.
Impact
- Reward theft: Compromised leader diverts all epoch rewards (1.5 RTC/epoch) to arbitrary wallets.
- No detection: Other nodes vote "accept" because the merkle check passes.
- Quorum bypass: Since all honest nodes accept the proposal, quorum is reached normally.
- No rollback: Once committed via
EPOCH_COMMIT, the epoch is finalized in the G-Set CRDT.
Fix
After the merkle internal-consistency check, cross-reference each distribution recipient against miner_attest_recent to ensure only legitimately attested miners receive rewards. Reject the proposal if any recipient is not found in the local attested miner set.
The fix adds ~20 lines: a DB query to fetch attested miners, a loop to validate recipients, and a helper method _reject_epoch_vote() to deduplicate rejection logic.
Distinction from Prior Issues
- Security: hardcoded default P2P HMAC secret enables forged gossip messages on default deployments #2046/security: require explicit P2P HMAC secret for gossip #2047 (P2P default secret): Auth configuration issue. This is content validation of authenticated proposals.
- Security: BFT PRE-PREPARE messages bypass signature and freshness checks #2061/security: verify BFT PRE-PREPARE messages before accept #2062 (BFT PRE-PREPARE auth bypass): Unauthenticated endpoint. This finding is about logically invalid but authenticated proposals.
- BFT cross-epoch replay (2026-04-05): TTL/digest validation on PREPARE/COMMIT messages. This is about epoch proposal content validation in the P2P gossip layer.
No overlap with any prior submission.