Skip to content

rejected-deploy buffer: re-inclusion window between recovery proposal and finalization #490

Description

@spreston8

Summary

The KeyValueRejectedDeployBuffer is only evicted on finalization (in multi_parent_casper_impl.rs::finalizer, ~L1599 — buffer_guard.remove_by_sig(sig)). Between when a recovered deploy is re-proposed and when its containing block finalizes, the deploy's sig is re-included in every subsequent block. Wasteful but sound; surfaces as block-space bloat and redundant deploy execution during the recovery window.

Surfaced while writing the recovery-cycle E2E regression test in PR #488 (Gap 1 from the meta review's test-coverage requests).

Reproduction

In the new recovery_cycle_rejected_deploy_is_buffered_and_re_proposed test (PR #488 follow-up commit), after the recovery block is created, the buffer still contains the recovered sig. Each subsequent propose pulls the same sig and includes it in the new block. The pattern continues until LFB advances past either:

  • the original rejection block (so the sig leaves `rejected_in_scope`), or
  • the recovery block (so the finalizer evicts the sig from the buffer).

Why it's not unsafe

PR #488's validator-side defense (`fix(validate): gate repeat_deploy recovery exemption on Finalized status` — commit `55cbbb06`) catches re-inclusion attempts once the resolver reports `Finalized`. The merge engine doesn't conflict on multi-block clean inclusion of the same sig. So no double-execution, no fork-divergence — just block-space bloat during the recovery-to-finalization window.

Why it's worth fixing

For workloads that produce frequent rejections (bridge-style contracts, registry-contention patterns), every recovery wastes `window_size` blocks of body-space on redundant entries. Multiplied across recoveries, this is meaningful overhead. Also confusing for clients querying deploy state: a single sig appearing in many blocks' `body.deploys` is hard to interpret.

Design options

Three plausible approaches:

Option 1 — Status quo

Buffer holds until finalization. Wasteful but sound. (Current behavior.)

Option 2 — Evict on pull

`block_creator` removes from buffer at `prepare_user_deploys` time. Risk: if the recovery block gets orphaned (fork-race loser, validation failure, etc.), the sig is lost permanently. Would need orphan-detection logic to re-populate the buffer; not trivial since orphan handling lives in separate code paths.

Option 3 — Skip pull when clean inclusion exists in current view

In `prepare_user_deploys`, when a sig is in both `deploys_in_scope` and `rejected_in_scope`, check whether a clean canonical inclusion exists in the proposer's parents' main-chain ancestry. If yes, recovery already happened — skip. If no (the only clean inclusion is on a sibling fork), re-propose.

Implementation: needs a "is sig in clean canonical inclusion of an ancestor of X?" check, where X is the proposer's parents' main-chain. Could repurpose `deploy_finalization_status::resolve` (currently anchored on LFB) by generalizing it to take an arbitrary anchor block.

My lean: Option 3. Sound and orphan-resilient. The infrastructure (`resolve_batch` plumbing) exists from PR #488; just needs a parameterized anchor.

Tests

The `recovery_cycle_rejected_deploy_is_buffered_and_re_proposed` test (PR #488 follow-up) reproduces the symptom. A future test for the fix would assert:

  • After recovery block is in scope, next propose does NOT re-include the recovered sig
  • If recovery block is orphaned (constructed via DAG manipulation), next propose DOES re-include the sig (so recovery isn't lost)

Prerequisites

PR #488 (`fix/merge-stale-diff-rejection-expansion`) — introduces the recovery mechanism this issue concerns and the `deploy_finalization_status` infrastructure that Option 3's fix depends on.

Co-Authored-By: Claude noreply@anthropic.com

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions