Skip to content

fix(p2p)!: fix BLOCK_TXS response under proposer equivocation#23786

Merged
spalladino merged 1 commit into
merge-train/spartanfrom
fc/fix-block-txs-equivocation
Jun 2, 2026
Merged

fix(p2p)!: fix BLOCK_TXS response under proposer equivocation#23786
spalladino merged 1 commit into
merge-train/spartanfrom
fc/fix-block-txs-equivocation

Conversation

@fcarreiro

@fcarreiro fcarreiro commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes A-1070: a malicious proposer who sends two different proposals with the same archive root but different tx sets could make two honest nodes fail the BLOCK_TXS exchange and penalize each other.

In the BLOCK_TXS protocol the requester asks for txs by their index within a block (proposal), identified only by its archive root. If an equivocating proposer gives node A and node B two proposals that share an archive root but differ in their tx list, then:

  • Node A (requester) asks node B for txs at indices [i, j, …] of "the block with this archive root".
  • Node B (responder) resolves those indices against its version of the proposal and returns txs that, from A's perspective, are not part of the block.
  • A's validateRequestedBlockTxsConsistency rejects the response and penalizes B — an honest node punished for honest behavior.

Fix

The request now carries a commitment to the full set of block tx hashes (blockTxHashesCommitment, a SHA-256 over the serialized tx hashes) alongside the archive root. The responder only serves txs by index (and advertises availability via the bitvector) when its own block's tx-hash commitment matches the request's. Otherwise it treats the request as "I don't have that block" — returning an empty bitvector and only servicing any explicitly-requested tx hashes — so neither side is penalized for an equivocation it didn't cause.

This closes the gap that the archive root alone could not: identical archive roots no longer imply identical tx sets.

Why not use proposal hash?

That would work when the BLOCK_TXS request is from a proposal, but it cannot be used when it's done from a block (e.g., in the prover node).

Changes

  • BlockTxsRequest gains a blockTxHashesCommitment field and a computeBlockTxHashesCommitment helper; serialization and fromTxsSourceAndMissingTxs updated accordingly.
  • reqRespBlockTxsHandler verifies the commitment before serving txs by index; on mismatch it falls back to the "block not available" path instead of returning indexed txs.
  • reqRespBlockTxsHandler no longer returns NOT_FOUND. It always services what it can — explicitly-requested tx hashes, plus index-based txs when the commitment matches — and signals that it does not have the block via an empty bitvector. Requesters detect this through BlockTxsResponse.peerHasBlock(). Duplicate requested hashes (by index and by explicit hash) are de-duplicated before the pool lookup.
  • BatchTxRequester: a peer that lacks the block (empty bitvector) is demoted to dumb without penalty. handleFailResponseFromPeer now penalizes NOT_FOUND along with FAILURE/UNKNOWN (since NOT_FOUND is no longer a legitimate response), while still demoting-without-penalty on INTERNAL_ERROR (consistency-validation failure). extractHashesPeerHasFromResponse is simplified to map bitvector indices directly to block tx hashes.
  • This builds on the preceding BLOCK_TXS validation revamp commit (consistency checks on the requester side, response no longer echoes the archive root).
  • Tests adapted across block_txs, block_txs_handler, batch_tx_requester, libp2p_service, and the integration suite: the "no block" case now expects an empty, block-less SUCCESS response instead of NOT_FOUND; the obsolete "demote on NOT_FOUND without penalizing" test was removed (legitimate demotion is now the empty-bitvector path). Added a handler test covering the equivocation case (different proposal under the same archive root → responder refuses to serve by index) and one asserting no duplicate txs are served when the same tx is requested by index and by repeated explicit hash.

Closes https://linear.app/aztec-labs/issue/A-1070/malicious-proposer-can-make-honest-nodes-to-fail-tx-validation .

@fcarreiro fcarreiro changed the base branch from merge-train/spartan to fc/fix-block-txs-validation June 1, 2026 21:55
@fcarreiro fcarreiro changed the title fix(p2p): fix BLOCK_TXS response under proposer equivocation fix(p2p)!: fix BLOCK_TXS response under proposer equivocation Jun 1, 2026
Comment on lines +73 to +74
const responseTxs = (await txPool.getTxsByHash(requestedTxsHashes)).filter(tx => !!tx);
const response = new BlockTxsResponse(new TxArray(...responseTxs), responseBitVector);
const response = new BlockTxsResponse(new TxArray(...responseTxs), availableIndicesBitVector);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we check if requestedTxHashes is non-empty before this, and if it is, return NOT_FOUND?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For simplicity I'm choosing to always try to service and return a response. NOT_FOUND is not a valid response anymore. The requester can now derive if the peer has the block from the bit vector (or better, using peerHasBlock(), and then can look at the array to see what the peer was able to return.

The only concern I see is that the "nothing found" path would be slightly bigger on the network, but still sth like 2 bytes (instead of possibly 1).

Base automatically changed from fc/fix-block-txs-validation to merge-train/spartan June 2, 2026 01:56
@spalladino spalladino force-pushed the fc/fix-block-txs-equivocation branch from 12c4d5c to c8f703b Compare June 2, 2026 01:58
@spalladino spalladino enabled auto-merge (squash) June 2, 2026 02:00
@fcarreiro fcarreiro force-pushed the fc/fix-block-txs-equivocation branch from c8f703b to 2be45eb Compare June 2, 2026 10:47
@fcarreiro fcarreiro force-pushed the fc/fix-block-txs-equivocation branch from 2be45eb to 75c2843 Compare June 2, 2026 10:57
@spalladino spalladino merged commit 922e0d9 into merge-train/spartan Jun 2, 2026
14 checks passed
@spalladino spalladino deleted the fc/fix-block-txs-equivocation branch June 2, 2026 11:37
danielntmd pushed a commit to danielntmd/aztec-packages that referenced this pull request Jun 4, 2026
BEGIN_COMMIT_OVERRIDE
chore: deploy next-net and reuse contracts (AztecProtocol#23761)
chore: turn on autoscaling (AztecProtocol#23706)
chore: rename staging-public to staging (AztecProtocol#23767)
chore(p2p): use sync hash for tx validation hashing (AztecProtocol#23768)
test(e2e): wait warmup slots in slashing tests (AztecProtocol#23719)
feat(api)!: make getTxReceipt the single tx-lookup API (AztecProtocol#23660)
fix: cap cloned n_tps fees within sponsored FPC balance (AztecProtocol#23770)
fix: protect HA validator Postgres from cluster scale-down (AztecProtocol#23772)
refactor: remove non-pipelining sequencer code path (AztecProtocol#23665)
feat(archiver): add getL2ToL1MembershipWitness node RPC (AztecProtocol#23646)
fix(p2p)!: revamp BLOCK_TXS validations (AztecProtocol#23778)
chore: name the bots (AztecProtocol#23795)
fix(e2e): ensure BBSync init (AztecProtocol#23793)
fix(p2p)!: fix BLOCK_TXS response under proposer equivocation (AztecProtocol#23786)
fix: reconnect L1 port-forward after epoch-boundary sleep in n_tps_prove
(AztecProtocol#23800)
chore: add empty vscode settings for yarn-project (AztecProtocol#23808)
fix(sequencer): only warn about missing proposed checkpoint once overdue
(AztecProtocol#23807)
fix: refresh n_tps fee quotes during sustained benchmark (AztecProtocol#23797)
fix(sequencer): enforce build-frame deadlines and align
attestation/publish windows (AztecProtocol#23776)
END_COMMIT_OVERRIDE
spalladino added a commit that referenced this pull request Jun 4, 2026
## Motivation

`v5-next` was cut from `next` at cbc99df (Jun 1), so PRs merged to
`merge-train/spartan` after the cut never flowed into it. This backports
all of them (authored by @spalladino and @fcarreiro) to keep v5-next
current with the spartan train.

## Approach

Each PR is cherry-picked from its squashed merge commit on
`merge-train/spartan`, in merge order, preserving the original commit
message and PR number — one commit per backported PR. All 11 applied
cleanly with no conflicts; patches are identical to the originals
(verified via `git patch-id`), and `bootstrap.sh build yarn-project`
passes on the result. Labeled `ci-no-squash` to preserve the per-PR
commits.

## Backported PRs

- #23768 — chore(p2p): use sync hash for tx validation hashing
- #23719 — test(e2e): wait warmup slots in slashing tests
- #23660 — feat(api)!: make getTxReceipt the single tx-lookup API
- #23665 — refactor: remove non-pipelining sequencer code path
- #23646 — feat(archiver): add getL2ToL1MembershipWitness node RPC
- #23778 — fix(p2p)!: revamp BLOCK_TXS validations
- #23786 — fix(p2p)!: fix BLOCK_TXS response under proposer equivocation
- #23808 — chore: add empty vscode settings for yarn-project
- #23807 — fix(sequencer): only warn about missing proposed checkpoint
once overdue
- #23776 — fix(sequencer): enforce build-frame deadlines and align
attestation/publish windows
- #23818 — chore(p2p): BlockTxsRequest comment

Note #23660, #23778, and #23786 are breaking changes (node RPC +
tx-effect db format, and p2p wire format respectively), as they were on
`next`.
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.

2 participants