Skip to content

fix: avoid livelock after mining delay - #7489

Open
brice-stacks wants to merge 4 commits into
stacks-network:mainfrom
brice-stacks:feat/replication-void-fix
Open

fix: avoid livelock after mining delay#7489
brice-stacks wants to merge 4 commits into
stacks-network:mainfrom
brice-stacks:feat/replication-void-fix

Conversation

@brice-stacks

Copy link
Copy Markdown
Contributor

If, somehow, we arrive in a scenario where the miner's proposals are not reaching signers (or at least not 70% of them) for block_proposal_max_age_secs, then finally, the proposals arrive at the signers, they would previously silently drop this proposal, neither approving or rejecting it. The miner, continuing to wait for approval or rejection would be permanently stuck in the propose_block loop, only ever reproposing the same block. This commit changes the signer behavior so that instead of silently ignoring the block, the reject it with a new reason, ProposalTooOld, which, when it receives >= 30% of these rejections, will trigger the miner to exit that loop and mine a new block.

Checklist

  • Test coverage for new or modified code paths
  • For new Clarity features or consensus changes, add property tests (see docs/property-testing.md)
  • Changelog fragment(s) or "no changelog" label added (see changelog.d/README.md)
  • Required documentation changes (e.g., rpc/openapi.yaml for RPC endpoints, event-dispatcher.md for new events)
  • New clarity functions have corresponding PR in clarity-benchmarking repo

If, somehow, we arrive in a scenario where the miner's proposals are not
reaching signers (or at least not 70% of them) for
`block_proposal_max_age_secs`, then finally, the proposals arrive at the
signers, they would previously silently drop this proposal, neither
approving or rejecting it. The miner, continuing to wait for approval or
rejection would be permanently stuck in the `propose_block` loop, only
ever reproposing the same block. This commit changes the signer behavior
so that instead of silently ignoring the block, the reject it with a new
reason, `ProposalTooOld`, which, when it receives >= 30% of these
rejections, will trigger the miner to exit that loop and mine a new
block.
@coveralls

coveralls commented Aug 5, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 32497649447

Coverage decreased (-0.001%) to 86.606%

Details

  • Coverage decreased (-0.001%) from the base build.
  • Patch coverage: 1 uncovered change across 1 file (28 of 29 lines covered, 96.55%).
  • 169 coverage regressions across 33 files.

Uncovered Changes

File Changed Covered %
libsigner/src/v0/messages.rs 6 5 83.33%
Total (2 files) 29 28 96.55%

Coverage Regressions

169 previously-covered lines in 33 files lost coverage.

Top 10 Files by Coverage Loss Lines Losing Coverage Coverage
clarity/src/vm/functions/bitcoin_madhouse.rs 38 82.57%
stackslib/src/net/p2p.rs 23 75.06%
stackslib/src/net/inv/epoch2x.rs 10 79.44%
stackslib/src/burnchains/burnchain.rs 8 71.39%
stackslib/src/net/api/get_tenure_tip_meta.rs 8 68.6%
stackslib/src/net/stackerdb/sync.rs 8 75.69%
stacks-node/src/neon_node.rs 7 83.42%
stacks-signer/src/chainstate/v2.rs 6 88.38%
stacks-signer/src/v0/signer.rs 6 88.62%
stacks-common/src/deps_common/bitcoin/network/encodable.rs 4 88.71%

Coverage Stats

Coverage Status
Relevant Lines: 233634
Covered Lines: 202342
Line Coverage: 86.61%
Coverage Strength: 19348278.6 hits per line

💛 - Coveralls

Comment thread stacks-signer/src/v0/signer.rs
Comment thread stacks-signer/src/v0/signer.rs

@federico-stacks federico-stacks left a comment

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.

Implementation looks fine. There is a conflict that need to be addressed.

I also noticed 3 things that are pre-existing behaviour, so flagging for possible follow-ups/investigation:

  • BlockInfo::valid is set at pre-commit, one phase before a signature is owed. mark_pre_committed() sets valid = Some(true), and determine_response keys on valid alone. It therefore can't distinguish "I intend to sign" (PreCommitted) from "I signed" (LocallyAccepted), and returns a full acceptance for both. Net effect: a re-proposal makes a pre-committed signer release its signature even though the 70% pre-commit threshold was never reached. Maybe not a big issue considering it is only reachable when pre-commits aren't circulating to 70% while the miner is already re-proposing. A check on state alongside valid should close it.

  • should_reevaluate_block has a predicate name but broadcasts.. It sounds a bit measleading. A better naming (respond_to_known_proposal?!), or even producing a return type (Responded / Reevaluate) or even producing return type + splitting the re-evaluation behavior and the broadcasting could improve readability and maintainability.

  • determine_response drops failed_txid when re-sending a rejection. It substitutes RejectReason::RejectedInPriorRound, and BlockRejection::new passes None for failed_txid. The miner's exclusion logic requires both a failed_txid and RejectCode::ValidationFailed(BadTransaction|ProblematicTransaction), so a re-sent rejection contributes reject weight but no txid (probably relevant in case the miner didn't receive the previous rejection).

@brice-stacks
brice-stacks requested review from francesco-stacks and a balanced review from Copilot August 19, 2026 19:47

Copilot AI left a comment

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.

Pull request overview

Prevents miner livelock by rejecting stale block proposals while preserving prior signer decisions.

Changes:

  • Adds the ProposalTooOld rejection reason and serialization.
  • Rejects expired proposals and adds regression coverage.
  • Updates signer changelog documentation.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
stacks-signer/src/v0/signer.rs Implements stale-proposal rejection.
stacks-signer/changelog.d/replication-void-fix.changed Documents signer behavior change.
stacks-node/src/tests/signer/v0/proposal_replication_void.rs Tests short and prolonged proposal voids.
stacks-node/src/tests/signer/v0/mod.rs Registers and updates signer tests.
libsigner/src/v0/messages.rs Defines and serializes the rejection reason.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +1387 to +1389
let rejection =
self.create_block_rejection(RejectReason::ProposalTooOld, &block_proposal.block);
self.send_block_response(&block_proposal.block, rejection.into());
@@ -0,0 +1 @@
Instead of silently ignoring old block proposals, reject them with the new `ProposalTooOld` reason. This allows the miner to break out of its `propose_block` loop and mine a new block instead of being stuck in a live lock until the next Bitcoin block arrives. Proposals for blocks that we have already decided on are unaffected: the signer resends its prior decision rather than flipping it.
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.

4 participants