Skip to content

security: p2p epoch proposal merkle check accepts self-authenticated fake distributions #2121

@createkr

Description

@createkr

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

  1. Set up a 3+ node P2P cluster with shared HMAC secret.
  2. Wait until a controlled node becomes epoch leader (round-robin by epoch % num_nodes).
  3. As leader, craft an EPOCH_PROPOSE message with distribution: {"attacker_wallet": 1.5} and compute merkle_root = sha256(json.dumps(sorted(distribution))).
  4. Broadcast to peers.
  5. Each peer computes the same merkle hash from the same fake data → votes "accept".
  6. 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 accept

The 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

No overlap with any prior submission.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions