Skip to content

Fix POS round state logic - #210

Merged
sanada08 merged 3 commits into
devfrom
fix/pos-round-state
Aug 6, 2026
Merged

Fix POS round state logic#210
sanada08 merged 3 commits into
devfrom
fix/pos-round-state

Conversation

@victor-tucci

Copy link
Copy Markdown
Member

The POS state machine previously detected staleness by comparing
heights only. An equal-height fork switch (pop block N, adopt the competing
block N) leaves the chain height unchanged, so the state machine kept using
wait_for_next_block.top_hash, which by then referred to a block deleted
from both the main and the alt DB (switch_to_alternative_blockchain() is
called with keep_disconnected_chain=false on the POS weight/checkpoint
reorg paths). The next entropy lookup then failed with:
"Failed to find block "
"Failed to get quorum entropy for POS, next block parent "
and the node sat out POS participation until the next height. Comparing the
top hash as well restarts the POS stages against the new tip immediately.

The POS state machine previously detected staleness by comparing
heights only. An equal-height fork switch (pop block N, adopt the competing
block N) leaves the chain height unchanged, so the state machine kept using
`wait_for_next_block.top_hash`, which by then referred to a block deleted
from both the main and the alt DB (switch_to_alternative_blockchain() is
called with keep_disconnected_chain=false on the POS weight/checkpoint
reorg paths). The next entropy lookup then failed with:
     "Failed to find block <hash>"
     "Failed to get quorum entropy for POS, next block parent <hash>"
and the node sat out POS participation until the next height. Comparing the
top hash as well restarts the POS stages against the new tip immediately.
@victor-tucci
victor-tucci requested a review from sanada08 August 4, 2026 10:25
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved proof-of-stake recovery when blockchain reorganizations replace the current chain tip.
    • Prevented proof-of-stake preparation from continuing when required quorum information is unavailable.
  • Network Updates

    • Updated several mainnet seed node addresses to improve network connectivity.
  • Chores

    • Incremented the project version to 7.0.3.

Walkthrough

The POS loop now detects same-height tip replacements, handles unavailable quorum entropy, and updates test fault-injection height handling. Mainnet seed hostnames changed for three entries. The CMake project version changed from 7.0.2 to 7.0.3.

Changes

POS chain handling

Layer / File(s) Summary
Chain-tip restart detection
src/cryptonote_core/pos.cpp
POS waiting and preparation paths now detect changes to the cached tip hash in addition to height changes.
Quorum entropy and test height handling
src/cryptonote_core/pos.cpp
POS preparation clears round data when quorum entropy is unavailable. Test batching derives height from the waiting context.

Release and network configuration

Layer / File(s) Summary
Version and seed configuration
CMakeLists.txt, src/p2p/net_node.inl
The project version changes to 7.0.3. Mainnet seeds 1, 3, and 5 use rpcnode.stream hostnames.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant POS as POS round
  participant ChainContext as chain context
  participant QuorumEntropy as quorum entropy
  POS->>ChainContext: compare height and cached top-block hash
  ChainContext-->>POS: return chain_top_changed
  POS->>QuorumEntropy: prepare quorum entropy
  QuorumEntropy-->>POS: return entropy state
  POS->>POS: restart round when the chain tip changed
Loading

Suggested reviewers: sanada08

Poem

A rabbit checks the chain-tip gleam,
Hash and height now guide the dream.
Quorum winds may fade from sight,
So rounds pause and wait upright.
New seeds hop through streams of light.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: correcting POS round state logic for chain reorganizations.
Description check ✅ Passed The description directly explains the POS stale-state bug, its equal-height fork cause, and the implemented fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pos-round-state

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cryptonote_core/pos.cpp`:
- Around line 1054-1055: Update the conditional branches in the affected PoS
logic, including the check on context.wait_for_next_block.height and the
corresponding branch near the second location, to wrap each controlled return
statement in braces. Preserve the existing conditions and return behavior.
- Around line 1047-1057: The POS wait flow currently reads the chain tip
multiple times, allowing height and hash-dependent state to come from different
snapshots. Update wait_for_next_block() and chain_top_changed() to obtain one
locked chain-tip snapshot via the existing chain API, then reuse its height and
hash for the changed check, prev_hash, timing setup, and POS round context
updates.

In `@src/p2p/net_node.inl`:
- Around line 596-600: Verify that seed1.rpcnode.stream, seed3.rpcnode.stream,
and seed5.rpcnode.stream resolve to the intended MAINNET P2P services on port
19090, and confirm these endpoints use independent DNS/provider failure domains.
Apply any required corrections to the default seed entries and the fallback seed
list so bootstrap redundancy is preserved.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8ca5b06d-8fd8-40a0-8eba-ee824435fb1f

📥 Commits

Reviewing files that changed from the base of the PR and between 511aa33 and 6ba65cd.

📒 Files selected for processing (3)
  • CMakeLists.txt
  • src/cryptonote_core/pos.cpp
  • src/p2p/net_node.inl

Comment on lines +1047 to +1057
// Returns true if the blockchain's top block differs from the tip cached in
// the round context — either because a new block arrived (height changed) or
// because a same-height reorg replaced the top block (hash changed at the
// same height).
static bool chain_top_changed(round_context const &context, cryptonote::Blockchain const &blockchain)
{
uint64_t const chain_height = blockchain.get_current_blockchain_height(true /*lock*/);
if (context.wait_for_next_block.height != chain_height)
return true;
return context.wait_for_next_block.top_hash != blockchain.get_block_id_by_height(chain_height - 1);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Inspect the lock lifetime and all chain-tip reads.
rg -n -C 12 \
  'get_current_blockchain_height|get_block_id_by_height|get_tail_id|chain_top_changed|wait_for_next_block' \
  src/cryptonote_core --glob '*.cpp' --glob '*.h'

Repository: Beldex-Coin/beldex

Length of output: 50375


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Inspect Blockchain tip read implementations and lock names.
fd -a 'blockchain\.(cpp|h)$' src | sed 's#^\./##' | while read -r f; do
  echo "===== $f ====="
  rg -n -C 8 'get_current_blockchain_height|get_tail_id|get_block_id_by_height' "$f" || true
done

# Focused slice around Blockchain implementation declarations/definitions.
rg -n -C 5 'Blockchain::(get_current_blockchain_height|get_tail_id(?:\b|\(.*\))|get_block_id_by_height)' src/cryptonote_core/blockchain.cpp src/cryptonote_core --glob '*.cpp' --glob '*.h'

Repository: Beldex-Coin/beldex

Length of output: 33330


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Focused slice around Blockchain implementation declarations/definitions.
rg -n -C 5 'Blockchain::(get_current_blockchain_height|get_tail_id(?:\b|\(.*\))|get_block_id_by_height)' src/cryptonote_core/blockchain.cpp src/cryptonote_core/blockchain.h

Repository: Beldex-Coin/beldex

Length of output: 3608


Use one locked chain-tip snapshot for POS state updates.

wait_for_next_block() reads the chain height, then chain_top_changed() reads it a second time. get_current_blockchain_height(true) releases the lock, and get_block_id_by_height() also acquires its own lock, so the comparison and the state update can observe different chain tips. Use one atomic snapshot, such as get_tail_id(chain_height_out), and reuse that height/hash for prev_hash, timing setup, and the POS context.

🧰 Tools
🪛 Clang (14.0.6)

[warning] 1051-1051: use a trailing return type for this function

(modernize-use-trailing-return-type)


[warning] 1054-1054: statement should be inside braces

(readability-braces-around-statements)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cryptonote_core/pos.cpp` around lines 1047 - 1057, The POS wait flow
currently reads the chain tip multiple times, allowing height and hash-dependent
state to come from different snapshots. Update wait_for_next_block() and
chain_top_changed() to obtain one locked chain-tip snapshot via the existing
chain API, then reuse its height and hash for the changed check, prev_hash,
timing setup, and POS round context updates.

Comment on lines +1054 to +1055
if (context.wait_for_next_block.height != chain_height)
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add braces around the new conditional branches.

Clang reports readability-braces-around-statements at both locations. Add braces before later edits change the controlled statement unintentionally.

Proposed fix
-  if (context.wait_for_next_block.height != chain_height)
-    return true;
+  if (context.wait_for_next_block.height != chain_height)
+  {
+    return true;
+  }

-    if (chain_top_changed(context, blockchain))
-      return goto_wait_for_next_block_and_clear_round_data(context);
+    if (chain_top_changed(context, blockchain))
+    {
+      return goto_wait_for_next_block_and_clear_round_data(context);
+    }

Also applies to: 1138-1139

🧰 Tools
🪛 Clang (14.0.6)

[warning] 1054-1054: statement should be inside braces

(readability-braces-around-statements)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cryptonote_core/pos.cpp` around lines 1054 - 1055, Update the conditional
branches in the affected PoS logic, including the check on
context.wait_for_next_block.height and the corresponding branch near the second
location, to wrap each controlled return statement in braces. Preserve the
existing conditions and return behavior.

Source: Linters/SAST tools

Comment thread src/p2p/net_node.inl
Comment on lines +596 to +600
full_addrs.insert("seed1.rpcnode.stream:19090");
full_addrs.insert("seed2.rpcnode.stream:19090");
full_addrs.insert("seed3.beldex.io:19090");
full_addrs.insert("seed3.rpcnode.stream:19090");
full_addrs.insert("seed4.rpcnode.stream:19090");
full_addrs.insert("seed5.beldex.io:19090");
full_addrs.insert("seed5.rpcnode.stream:19090");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial

Verify bootstrap redundancy before release.

All five MAINNET seed entries now use rpcnode.stream. A DNS or provider outage for this domain can remove every default bootstrap endpoint. The fallback path also reuses the same seed list at Line 1463-1466. Confirm that seed1.rpcnode.stream, seed3.rpcnode.stream, and seed5.rpcnode.stream resolve to the intended MAINNET P2P services on port 19090. Confirm that the seed infrastructure has independent failure domains.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/p2p/net_node.inl` around lines 596 - 600, Verify that
seed1.rpcnode.stream, seed3.rpcnode.stream, and seed5.rpcnode.stream resolve to
the intended MAINNET P2P services on port 19090, and confirm these endpoints use
independent DNS/provider failure domains. Apply any required corrections to the
default seed entries and the fallback seed list so bootstrap redundancy is
preserved.

@sanada08
sanada08 merged commit bab8324 into dev Aug 6, 2026
2 checks passed
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