Fix POS round state logic - #210
Conversation
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.
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe 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. ChangesPOS chain handling
Release and network configuration
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
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
CMakeLists.txtsrc/cryptonote_core/pos.cppsrc/p2p/net_node.inl
| // 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); | ||
| } |
There was a problem hiding this comment.
🎯 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.hRepository: 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.
| if (context.wait_for_next_block.height != chain_height) | ||
| return true; |
There was a problem hiding this comment.
📐 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
| 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"); |
There was a problem hiding this comment.
🩺 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.
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 deletedfrom 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.