CVE-2026-34069
Description
nimiq/core-rs-albatross is a Rust implementation of the Nimiq Proof-of-Stake protocol based on the Albatross consensus algorithm. In versions 1.2.2 and below, an unauthenticated p2p peer can cause the RequestMacroChain message handler task to panic. Sending a RequestMacroChain message where the first locator hash on the victim’s main chain is a micro block hash (not a macro block hash) causes said panic. The RequestMacroChain::handle handler selects the locator based only on "is on main chain", then calls get_macro_blocks() and panics via .unwrap() when the selected hash is not a macro block (BlockchainError::BlockIsNotMacro). This issue has been fixed in version 1.3.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
nimiq-consensuscrates.io | <= 1.2.2 | — |
Affected products
1Patches
1ae6c1e92342eFix panic in RequestMacroChain with micro block locator
2 files changed · +224 −4
consensus/src/messages/handlers.rs+11 −4 modified@@ -57,9 +57,16 @@ impl<N: Network> Handle<N, BlockchainProxy> for RequestMacroChain { if let Ok(chain_info) = chain_info && chain_info.on_main_chain { - // We found a block, ignore remaining block locator hashes. - start_block_hash = Some(locator.clone()); - break; + // Validate that the locator is a macro block before using it. + // This prevents panics when get_macro_blocks() is called with a micro block hash. + if let Ok(block) = blockchain.get_block(locator, false) + && block.is_macro() + { + // We found a macro block on the main chain, use it. + start_block_hash = Some(locator.clone()); + break; + } + // Skip micro blocks and continue searching for a macro block. } } let Some(start_block_hash) = start_block_hash else { @@ -75,7 +82,7 @@ impl<N: Network> Handle<N, BlockchainProxy> for RequestMacroChain { Direction::Forward, true, ) - .unwrap(); // We made sure that start_block_hash is on our chain. + .unwrap(); // Safe: We validated that start_block_hash is a macro block on our chain. let epochs: Vec<_> = election_blocks.iter().map(|block| block.hash()).collect(); // Add latest checkpoint block if all of the following conditions are met:
consensus/tests/request_macro_chain.rs+213 −0 added@@ -0,0 +1,213 @@ +use std::sync::Arc; + +use nimiq_blockchain::{BlockProducer, Blockchain, BlockchainConfig}; +use nimiq_blockchain_interface::AbstractBlockchain; +use nimiq_blockchain_proxy::BlockchainProxy; +use nimiq_consensus::messages::{MacroChainError, RequestMacroChain}; +use nimiq_database::mdbx::MdbxDatabase; +use nimiq_network_interface::request::Handle; +use nimiq_network_mock::{MockNetwork, MockPeerId}; +use nimiq_primitives::{networks::NetworkId, policy::Policy}; +use nimiq_test_log::test; +use nimiq_test_utils::blockchain::{produce_macro_blocks, signing_key, voting_key}; +use nimiq_utils::time::OffsetTime; +use parking_lot::RwLock; + +/// Helper to create a test blockchain with some blocks +fn create_test_blockchain(num_macro_blocks: usize) -> Arc<RwLock<Blockchain>> { + let blockchain = Arc::new(RwLock::new( + Blockchain::new( + MdbxDatabase::new_volatile(Default::default()).unwrap(), + BlockchainConfig::default(), + NetworkId::UnitAlbatross, + Arc::new(OffsetTime::new()), + ) + .unwrap(), + )); + + let producer = BlockProducer::new(signing_key(), voting_key()); + produce_macro_blocks(&producer, &blockchain, num_macro_blocks); + + blockchain +} + +#[test(tokio::test)] +async fn test_request_macro_chain_rejects_micro_blocks() { + // Create a blockchain with at least one batch (so we have micro blocks) + let blockchain = create_test_blockchain(2); + let blockchain_proxy = BlockchainProxy::from(&blockchain); + + // Find a micro block hash on the main chain + let micro_block_hash = { + let blockchain = blockchain.read(); + let current_block = blockchain.block_number(); + + let mut found_hash = None; + for block_num in 1..=current_block { + if !Policy::is_macro_block_at(block_num) { + if let Ok(block) = blockchain.get_block_at(block_num, false, None) { + if block.is_micro() { + found_hash = Some(block.hash()); + break; + } + } + } + } + found_hash.expect("Should have at least one micro block") + }; + + // Create a request with only the micro block hash as locator + let request = RequestMacroChain { + locators: vec![micro_block_hash], + max_epochs: 10, + }; + + // Handle the request + let peer_id = MockPeerId(1); + let result = <RequestMacroChain as Handle<MockNetwork, BlockchainProxy>>::handle( + &request, + peer_id, + &blockchain_proxy, + ); + + // Should return UnknownLocators error instead of panicking + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + MacroChainError::UnknownLocators + )); +} + +#[test(tokio::test)] +async fn test_request_macro_chain_accepts_macro_blocks() { + // Create a blockchain with multiple macro blocks + let blockchain = create_test_blockchain(3); + let blockchain_proxy = BlockchainProxy::from(&blockchain); + + // Get the first macro block hash from the chain (not the last one) + let macro_block_hash = { + let blockchain = blockchain.read(); + + // Find the first macro block (genesis or first checkpoint) + let mut found_hash = None; + for block_num in 1..=blockchain.block_number() { + if Policy::is_macro_block_at(block_num) { + if let Ok(block) = blockchain.get_block_at(block_num, false, None) { + if block.is_macro() { + found_hash = Some(block.hash()); + break; // Use the first one, not the last + } + } + } + } + found_hash.expect("Should have at least one macro block") + }; + + // Create a request with the macro block hash as locator + let request = RequestMacroChain { + locators: vec![macro_block_hash], + max_epochs: 10, + }; + + // Handle the request + let peer_id = MockPeerId(1); + let result = <RequestMacroChain as Handle<MockNetwork, BlockchainProxy>>::handle( + &request, + peer_id, + &blockchain_proxy, + ); + + // Should succeed + assert!(result.is_ok()); + let response = result.unwrap(); + + // Should return macro blocks after the locator (or be empty if locator is the last block) + // Since we used the first macro block, there should be more blocks after it + assert!(!response.epochs.is_empty() || response.checkpoint.is_some()); +} + +#[test(tokio::test)] +async fn test_request_macro_chain_skips_micro_blocks() { + // Create a blockchain with multiple batches + let blockchain = create_test_blockchain(3); + let blockchain_proxy = BlockchainProxy::from(&blockchain); + + // Find both a micro block and a macro block (not the last macro block) + let (micro_block_hash, macro_block_hash) = { + let blockchain = blockchain.read(); + let current_block = blockchain.block_number(); + + let mut micro_hash = None; + let mut macro_hash = None; + + // Find first micro and first macro block + for block_num in 1..=current_block { + if let Ok(block) = blockchain.get_block_at(block_num, false, None) { + if block.is_micro() && micro_hash.is_none() { + micro_hash = Some(block.hash()); + } else if block.is_macro() && macro_hash.is_none() { + macro_hash = Some(block.hash()); + } + + if micro_hash.is_some() && macro_hash.is_some() { + break; + } + } + } + + ( + micro_hash.expect("Should have at least one micro block"), + macro_hash.expect("Should have at least one macro block"), + ) + }; + + // Create a request with micro block first, then macro block + // The handler should skip the micro block and use the macro block + let request = RequestMacroChain { + locators: vec![micro_block_hash, macro_block_hash], + max_epochs: 10, + }; + + // Handle the request + let peer_id = MockPeerId(1); + let result = <RequestMacroChain as Handle<MockNetwork, BlockchainProxy>>::handle( + &request, + peer_id, + &blockchain_proxy, + ); + + // Should succeed by using the macro block + assert!(result.is_ok()); + let response = result.unwrap(); + + // Should return macro blocks starting from the macro block locator (or checkpoint) + assert!(!response.epochs.is_empty() || response.checkpoint.is_some()); +} + +#[test(tokio::test)] +async fn test_request_macro_chain_too_many_locators() { + let blockchain = create_test_blockchain(2); + let blockchain_proxy = BlockchainProxy::from(&blockchain); + + // Create a request with too many locators (> MAX_LOCATORS = 100) + let locators = vec![Default::default(); 101]; + let request = RequestMacroChain { + locators, + max_epochs: 10, + }; + + // Handle the request + let peer_id = MockPeerId(1); + let result = <RequestMacroChain as Handle<MockNetwork, BlockchainProxy>>::handle( + &request, + peer_id, + &blockchain_proxy, + ); + + // Should return TooManyLocators error + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + MacroChainError::TooManyLocators + )); +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/nimiq/core-rs-albatross/commit/ae6c1e92342e72f80fd12accbe66ee80dd6802acnvdPatchWEB
- github.com/nimiq/core-rs-albatross/pull/3660nvdIssue TrackingPatchWEB
- github.com/nimiq/core-rs-albatross/security/advisories/GHSA-48m6-486p-9j8pnvdPatchVendor AdvisoryWEB
- github.com/advisories/GHSA-48m6-486p-9j8pghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-34069ghsaADVISORY
- github.com/nimiq/core-rs-albatross/releases/tag/v1.3.0nvdRelease NotesWEB
News mentions
0No linked articles in our index yet.