https://github.com/hackenproof-public/somnia
Description:
Data network is used to receive blocks (DataChainBlockReceived) previously requested throught the protocol network (DataChainBlockRequest). The request is being throttled at max_data_chain_block_requests_per_period (500 request / second by default) at the protocol level, but there is no protection at the data network level. This fall into the scope of this audit under Peer-to-peer network flaws.
This design allows an attacker (any clients, not even need to be an official node or in-committee, just to be able to connect to the data network port) to flood the system by directly sending an arbitrary number of DataChainBlockReceived message to any peers which:
DataNetwork::TryReceiveDataFromPeer(), PeerDataChainActorShard::ProcessInput() and DataChain::BlockView::Validate()) forcing unnecessary cryptographic operations. Note that Validate() is executed twice during this flow, first during ProcessInput() and then at the end when calling ReceivedBlock(), assuming the attacker is sending a valid fake block (as shown in my PoC).received_blocks could be filled quickly by sending unique message (changing a single byte of the data member and recalculating the block hash), but it should not reach total memory exhaustion as this struct get reset at the change of the epoch (5 min - 3000 blocks), but could definitally impose some memory pressure on the node.Affected Code
std::uint64_t DataNetwork::TryReceiveDataFromPeer(Hash ip_hash, const Address&,
ByteSpanConst data) {
smash::SmashDecoder reader{data};
std::uint64_t bytes_consumed_after_last_message = 0;
// Try and read a serialised block.
std::vector<std::uint8_t> serialised_block;
while (reader.TryRead(serialised_block)) {
node_actor_manager.peer_data_chain_actor.ForceNonBlockingSend(
DataChainBlockReceived{std::move(serialised_block)});
bytes_consumed_after_last_message = reader.BytesRead();
}
kDataIngressBytes.Increment(bytes_consumed_after_last_message);
return bytes_consumed_after_last_message;
}
Impact
received_blocks can consume a lot of memory in a 5 minute window, but probably not reach node crash.Assets:
somnia\somnia\node\data_network.cc
Impact Rate: 3/5
Likelihood Rate: 3/5
Severity: Medium
Remediation:
Reproduce:
First, I added some traces into Somnia to get more visibility on the behavior.
void DataChain::ReceivedBlock(BlockView input_block) {
RELEASE_ASSERT(input_block.data_chain_id == data_chain_id);
DEBUG_ASSERT(input_block.Validate());
if (received_blocks.count(input_block.block_hash)) {
// We already have this block.
return;
}
// Save the block into the storage database.
storage_database.SetSmashData(
StorageKey::KeyFromHash(StorageKeyType::DATA_CHAIN_BLOCK, input_block.block_hash),
input_block);
// Store the block header in memory.
BlockHeader block_header = BlockHeader::FromBlock(input_block);
received_blocks.emplace(input_block.block_hash, block_header);
+ size_t received_blocks_RAM_size_kB = received_blocks.size() * (sizeof(HashableHash) + sizeof(BlockHeader) + 24) / 1024;
+spdlog::info("EL*****():DataChain::ReceivedBlock received_blocks size: {}, Bytes taken: {}", received_blocks.size(), received_blocks_RAM_size_kB);
...
void PeerDataChainActorShard::ProcessInput(PeerDataChainActorInput&& input) {
Match(
std::move(input),
[&](https://dashboard.hackenproof.com/redirect?url=DataChain::DataChainHeartbeat&& message) {
data_chain_store.ReceiveDataChainHeartbeat(message);
},
[&](https://dashboard.hackenproof.com/redirect?url=DataChain::DataChainBlockRequest&& message) {
data_chain_store.ReceivedDataChainBlockRequest(
message, node_actor_manager.data_network.GetAsyncDataNetwork());
},
[&](https://dashboard.hackenproof.com/redirect?url=DataChainBlockReceived&& message) {
+ spdlog::info("EL*****():PeerDataChainActorShard::ProcessInput() --SS-- ");
// Deserialise the block.
DataChain::BlockView block;
if (!smash::DeserialiseFromBuffer(message.serialised_block, block)) {
+ spdlog::info("EL*****():PeerDataChainActorShard::ProcessInput() --SS-- INVALID Deserialise ");
// This was an invalid block. Drop it.
return;
}
// Validate the block.
if (!block.Validate()) {
+ spdlog::info("EL*****():PeerDataChainActorShard::ProcessInput() --SS-- INVALID Validate ");
return;
}
+ spdlog::info("EL*****():PeerDataChainActorShard::ProcessInput() --SS-- GOOD BLOCK! ");
// Store the block ourselves.
data_chain_store.ReceivedBlock(block);
},
...
Then I used the following program in order to connect to the peer (node 0) and send it DataChainBlockReceived message once the localchain is running.
simple_data_client.cc and copy it inside somnia\node folder.cc_binary(
name = "simple_data_client",
srcs = ["simple_data_client.cc"],
deps = [
":node",
"//somnia/test:in_process_deployment",
],
)
NETWORK_PRESET=mainnet-small ./ci/run-local-deployment.shbazel run //somnia/node:simple_data_clientResults:
Looking at the screenshot uploaded, we can see the message getting processed properly generating [warning] Received conflicting block hashes on DataChain confirming all the waste in resources mention in the report including the block being added to the received_blocks map. Here one sample from the screenshot.
[N0] [2025-09-08 13:52:22.876] [peer_dc_1 ] [info] EL*****():PeerDataChainActorShard::ProcessInput() --SS--
9551 [N0] [2025-09-08 13:52:22.876] [peer_dc_1 ] [info] EL*****():PeerDataChainActorShard::ProcessInput() --SS-- GOOD BLOCK!
9552 [N0] [2025-09-08 13:52:22.876] [peer_dc_1 ] [info] EL*****():DataChain::ReceivedBlock received_blocks size: 70, Bytes taken: 14 9553 [N0] [2025-09-08 13:52:22.877] [peer_dc_1 ] [warning] Received conflicting block hashes on DataChain(owner: 0x654492b9ae9fbebe8931f1c3bf7ec1baff3bb138, epoch: 0) data chain, height 1. 0x4994179851b8 4ab8e487742d7c271a9b37a1128639f511e82f66703efdc46866 vs 0xea590412abb8a58267f0f0bd91a06388cfdb0a1642d370e68366ac0628f8bc97.