Summary
There are several instances of shared mutable state in the P2P / net modules of java-tron that are accessed concurrently by multiple threads, including net workers, synchronization threads, block-fetch thread pools, and scheduled tasks, but are not protected by synchronization, volatile, or thread-safe containers.
Such unsynchronized concurrent reads and writes constitute data races under the Java Memory Model and may result in stale reads, lost updates, inconsistent collection state, and race windows in compound operations such as check-then-set. Under high connection pressure or intensive multi-threaded scheduling, these issues may affect the determinism and stability of node behavior.
We recommend adding appropriate synchronization, visibility, and atomicity guarantees to these shared states.
Root Cause
The following eight instances of shared state lack adequate concurrency protection:
SyncService.syncBlockInProcess uses a non-thread-safe HashSet and is accessed concurrently by multiple threads without synchronization. HashSet does not guarantee internal consistency under concurrent structural modifications.
SyncService.syncNext performs a check-then-set operation on peer.syncChainRequested (checking whether it is unset before assigning a value). The operation is not atomic, leaving a race window when multiple threads enter the code concurrently. This may result in duplicate requests or inconsistent state.
- The shared field
fetchBlockInfo is not declared volatile and is concurrently read and written by three thread pools. Without a visibility guarantee, a write performed by one thread is not guaranteed to become visible to other threads in a timely manner.
cheatWitnessInfoMap uses a non-thread-safe HashMap. Concurrent reads/writes or structural modifications are not safely supported and may result in lost updates or inconsistent observations. If iteration occurs concurrently with modification, it may also trigger ConcurrentModificationException.
PeerManager.check() modifies the peers collection and related counters without synchronization. Reads, writes, and iteration may therefore occur concurrently and interfere with each other.
BlockChainMetricManager.getDupWitness() has a write-read ordering race involving dupWitnessBlockNum: the block-production thread first calls counterInc (making the counter key visible) and then performs put. Meanwhile, the metrics thread checks the counter key and calls dupWitnessBlockNum.get(witness). If the read occurs between these two operations, it may return null.
MessageCount uses ordinary fields for szCount[], index, and totalCount. add() performs non-atomic read-modify-write operations, while update() updates the rolling window without synchronization. These methods can be invoked concurrently by multiple sender threads, potentially resulting in lost increments and inconsistent counters.
BackupManager.status is an ordinary field updated by the Backup keep-alive scheduled task and UDP event-handling thread, while being read by the consensus block-production thread, Relay scheduled task, and Metrics thread. The field is neither declared volatile nor protected by consistent synchronization. Under the Java Memory Model, reader threads may continue observing a stale role.
The underlying cause is consistent across all cases: shared mutable state is accessed concurrently without adequate synchronization, visibility, or atomicity guarantees.
Impact
- Concurrent access to shared state may result in stale reads, lost updates, inconsistent collection state, or race conditions in compound operations, affecting the determinism and stability of node behavior.
- The issues are generally intermittent and dependent on thread scheduling, making them more likely to occur under high connection pressure or intensive multi-threaded workloads.
- The impact is limited to the runtime stability of an individual node. These issues do not alter protocol semantics and do not affect consensus correctness or asset security.
- A stale read of BackupManager.status may cause a witness node to incorrectly attempt or skip a scheduled production slot and may temporarily make Relay and monitoring behavior inconsistent.
Suggested Fix
Apply appropriate concurrency protection to each shared state based on its access pattern:
syncBlockInProcess: Replace it with a thread-safe collection, such as Collections.synchronizedSet or ConcurrentHashMap.newKeySet(), or consistently synchronize access to the set.
syncChainRequested in SyncService.syncNext: Replace the check-then-set operation with an atomic operation, such as performing both operations inside a synchronized block or using an atomic reference with CAS, eliminating the race window.
fetchBlockInfo: Declare the field as volatile (or use an atomic reference) to guarantee cross-thread visibility. If compound updates are involved, synchronization should be added as well.
cheatWitnessInfoMap: Replace HashMap with ConcurrentHashMap.
PeerManager.check(): Synchronize reads and writes to peers and the related counters, or use thread-safe containers to ensure that modifications and iteration do not occur concurrently.
getDupWitness / dupWitnessBlockNum: Ensure that the companion map becomes visible before the counter key by changing the write order to dupWitnessBlockNum.put(...) followed by counterInc(...). On the read side, use dupWitnessBlockNum.getOrDefault(witness, 0L) to eliminate the potential NullPointerException caused by automatic unboxing, providing an additional safeguard.
MessageCount: Use a thread-safe implementation and synchronize add, add(int), getCount, and update consistently to ensure cross-thread visibility and prevent lost updates.
BackupManager.status: At minimum, declare the field volatile so that consensus, Relay, Metrics, and other reader threads observe the latest role.
General Principle
For each shared state, identify the threads that access it and the corresponding access patterns, then choose the minimal and correct concurrency mechanism:
- Visibility → volatile
- Compound atomicity → synchronization / locks or CAS
- Concurrent collections → thread-safe implementations
The goal is to avoid unprotected shared mutable state while keeping synchronization overhead and implementation complexity to a minimum.
Summary
There are several instances of shared mutable state in the P2P / net modules of java-tron that are accessed concurrently by multiple threads, including net workers, synchronization threads, block-fetch thread pools, and scheduled tasks, but are not protected by synchronization, volatile, or thread-safe containers.
Such unsynchronized concurrent reads and writes constitute data races under the Java Memory Model and may result in stale reads, lost updates, inconsistent collection state, and race windows in compound operations such as check-then-set. Under high connection pressure or intensive multi-threaded scheduling, these issues may affect the determinism and stability of node behavior.
We recommend adding appropriate synchronization, visibility, and atomicity guarantees to these shared states.
Root Cause
The following eight instances of shared state lack adequate concurrency protection:
SyncService.syncBlockInProcessuses a non-thread-safe HashSet and is accessed concurrently by multiple threads without synchronization. HashSet does not guarantee internal consistency under concurrent structural modifications.SyncService.syncNextperforms a check-then-set operation onpeer.syncChainRequested(checking whether it is unset before assigning a value). The operation is not atomic, leaving a race window when multiple threads enter the code concurrently. This may result in duplicate requests or inconsistent state.fetchBlockInfois not declared volatile and is concurrently read and written by three thread pools. Without a visibility guarantee, a write performed by one thread is not guaranteed to become visible to other threads in a timely manner.cheatWitnessInfoMapuses a non-thread-safe HashMap. Concurrent reads/writes or structural modifications are not safely supported and may result in lost updates or inconsistent observations. If iteration occurs concurrently with modification, it may also trigger ConcurrentModificationException.PeerManager.check()modifies the peers collection and related counters without synchronization. Reads, writes, and iteration may therefore occur concurrently and interfere with each other.BlockChainMetricManager.getDupWitness()has a write-read ordering race involvingdupWitnessBlockNum: the block-production thread first calls counterInc (making the counter key visible) and then performs put. Meanwhile, the metrics thread checks the counter key and callsdupWitnessBlockNum.get(witness). If the read occurs between these two operations, it may return null.MessageCountuses ordinary fields forszCount[],index, andtotalCount.add()performs non-atomic read-modify-write operations, whileupdate()updates the rolling window without synchronization. These methods can be invoked concurrently by multiple sender threads, potentially resulting in lost increments and inconsistent counters.BackupManager.statusis an ordinary field updated by the Backup keep-alive scheduled task and UDP event-handling thread, while being read by the consensus block-production thread, Relay scheduled task, and Metrics thread. The field is neither declared volatile nor protected by consistent synchronization. Under the Java Memory Model, reader threads may continue observing a stale role.The underlying cause is consistent across all cases: shared mutable state is accessed concurrently without adequate synchronization, visibility, or atomicity guarantees.
Impact
Suggested Fix
Apply appropriate concurrency protection to each shared state based on its access pattern:
syncBlockInProcess: Replace it with a thread-safe collection, such asCollections.synchronizedSetorConcurrentHashMap.newKeySet(), or consistently synchronize access to the set.syncChainRequestedinSyncService.syncNext: Replace the check-then-set operation with an atomic operation, such as performing both operations inside a synchronized block or using an atomic reference with CAS, eliminating the race window.fetchBlockInfo: Declare the field as volatile (or use an atomic reference) to guarantee cross-thread visibility. If compound updates are involved, synchronization should be added as well.cheatWitnessInfoMap: Replace HashMap with ConcurrentHashMap.PeerManager.check(): Synchronize reads and writes to peers and the related counters, or use thread-safe containers to ensure that modifications and iteration do not occur concurrently.getDupWitness / dupWitnessBlockNum: Ensure that the companion map becomes visible before the counter key by changing the write order todupWitnessBlockNum.put(...)followed bycounterInc(...). On the read side, usedupWitnessBlockNum.getOrDefault(witness, 0L)to eliminate the potential NullPointerException caused by automatic unboxing, providing an additional safeguard.MessageCount: Use a thread-safe implementation and synchronizeadd,add(int),getCount, andupdateconsistently to ensure cross-thread visibility and prevent lost updates.BackupManager.status: At minimum, declare the field volatile so that consensus, Relay, Metrics, and other reader threads observe the latest role.General Principle
For each shared state, identify the threads that access it and the corresponding access patterns, then choose the minimal and correct concurrency mechanism:
The goal is to avoid unprotected shared mutable state while keeping synchronization overhead and implementation complexity to a minimum.