Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ private void processBlock(PeerConnection peer, BlockCapsule block) throws P2pExc

try {
tronNetDelegate.processBlock(block, false);
peer.setBlockRcvTime(System.currentTimeMillis());
witnessProductBlockService.validWitnessProductTwoBlock(block);

Item item = new Item(blockId, InventoryType.BLOCK);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.tron.common.utils.Sha256Hash;
import org.tron.core.capsule.BlockCapsule.BlockId;
import org.tron.core.config.args.Args;
import org.tron.core.net.TronNetDelegate;
import org.tron.core.net.message.TronMessage;
Expand Down Expand Up @@ -40,7 +41,10 @@ public void processMessage(PeerConnection peer, TronMessage msg) {
peer.getAdvInvReceive().put(item, System.currentTimeMillis());
advService.addInv(item);
if (type.equals(InventoryType.BLOCK) && peer.getAdvInvSpread().getIfPresent(item) == null) {
peer.setLastInteractiveTime(System.currentTimeMillis());
long headNum = tronNetDelegate.getHeadBlockId().getNum();
if (new BlockId(id).getNum() > headNum) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[QUESTION] Small wording suggestion. The PR description says this prevents "attackers from forging activity via stale block hashes" — the stale-replay part is correct and a real improvement, but I don't think arbitrary forgery is prevented:

BlockId(Sha256Hash) reinterprets the first 8 bytes of the hash as a big-endian long (BlockCapsule.java:347-352) without verifying the hash matches any real block. An adversary can craft a 32-byte hash whose high bytes encode headNum + 1 and still pass the check.

The check still has clear value (this is the only path that refreshes lastInteractiveTime on INV — P2pEventHandlerImpl.updateLastInteractiveTime doesn't include INVENTORY in its switch, so without this, tightening any historical block hash refreshes the inactivity clock). Just suggest narrowing the description to "prevent stale block hashes from refreshing lastInteractiveTime" rather than the broader "forging activity" framing — keeps expectations accurate.

peer.setLastInteractiveTime(System.currentTimeMillis());
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ public class PeerConnection {
@Setter
private volatile long lastInteractiveTime;

@Setter
@Getter
private volatile long blockRcvTime;

@Getter
@Setter
private volatile TronState tronState = TronState.INIT;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import static org.tron.common.math.Maths.ceil;
import static org.tron.common.math.Maths.max;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -44,7 +46,7 @@ public class ResilienceService {

@Autowired
private ChainBaseManager chainBaseManager;

public void init() {
if (Args.getInstance().isOpenFullTcpDisconnect) {
executor.scheduleWithFixedDelay(() -> {
Expand Down Expand Up @@ -86,6 +88,7 @@ private void disconnectRandom() {
.collect(Collectors.toList());

if (peers.size() >= minBroadcastPeerSize) {
peers = getRandomDisconnectionPeers(peers);
long now = System.currentTimeMillis();
Map<Object, Integer> weights = new HashMap<>();
peers.forEach(peer -> {
Expand Down Expand Up @@ -121,6 +124,14 @@ private void disconnectRandom() {
}


private List<PeerConnection> getRandomDisconnectionPeers(List<PeerConnection> peers) {
Map<PeerConnection, Long> snapshot = new IdentityHashMap<>(peers.size());
peers.forEach(p -> snapshot.put(p, p.getBlockRcvTime()));
List<PeerConnection> sorted = new ArrayList<>(peers);
sorted.sort(Comparator.comparingLong(snapshot::get));
return sorted.subList(0, sorted.size() / 2);
}

private void disconnectLan() {
if (!isLanNode()) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ private void processSyncBlock(BlockCapsule block, PeerConnection peerConnection)
try {
tronNetDelegate.validSignature(block);
tronNetDelegate.processBlock(block, true);
peerConnection.setBlockRcvTime(System.currentTimeMillis());
pbftDataSyncHandler.processPBFTCommitData(block);
} catch (P2pException p2pException) {
logger.error("Process sync block {} failed, type: {}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,12 @@ public void testProcessMessage() {
}

@Test
public void testProcessBlock() {
public void testProcessBlock() throws Exception {
TronNetDelegate tronNetDelegate = Mockito.mock(TronNetDelegate.class);

Field field = handler.getClass().getDeclaredField("tronNetDelegate");
field.setAccessible(true);
Object origin = field.get(handler);
try {
Field field = handler.getClass().getDeclaredField("tronNetDelegate");
field.setAccessible(true);
field.set(handler, tronNetDelegate);

BlockCapsule blockCapsule0 = new BlockCapsule(1,
Expand All @@ -164,8 +164,74 @@ public void testProcessBlock() {
.getDeclaredMethod("processBlock", PeerConnection.class, BlockCapsule.class);
method.setAccessible(true);
method.invoke(handler, peer, blockCapsule0);
} catch (Exception e) {
Assert.fail();
} finally {
field.set(handler, origin);
}
}

@Test
public void testBlockRcvTimeSetAfterProcessBlockSuccess() throws Exception {
TronNetDelegate tronNetDelegate = Mockito.mock(TronNetDelegate.class);
Field field = handler.getClass().getDeclaredField("tronNetDelegate");
field.setAccessible(true);
Object origin = field.get(handler);
try {
field.set(handler, tronNetDelegate);

BlockCapsule blockCapsule = new BlockCapsule(1,
Sha256Hash.wrap(ByteString.copyFrom(ByteArray.fromHexString(
"9938a342238077182498b464ac0292229938a342238077182498b464ac029222"))),
1234, ByteString.copyFrom("1234567".getBytes()));

Mockito.doReturn(true).when(tronNetDelegate).validBlock(any(BlockCapsule.class));
Mockito.doReturn(true).when(tronNetDelegate).containBlock(any(BlockId.class));
Mockito.doReturn(blockCapsule.getBlockId()).when(tronNetDelegate).getHeadBlockId();
Mockito.doNothing().when(tronNetDelegate).processBlock(any(BlockCapsule.class), anyBoolean());
Mockito.doReturn(new ArrayList<PeerConnection>()).when(tronNetDelegate).getActivePeer();

peer.setBlockRcvTime(0L);
Method method = handler.getClass()
.getDeclaredMethod("processBlock", PeerConnection.class, BlockCapsule.class);
method.setAccessible(true);

long before = System.currentTimeMillis();
method.invoke(handler, peer, blockCapsule);
long after = System.currentTimeMillis();

Assert.assertTrue("blockRcvTime should be set after successful processBlock",
peer.getBlockRcvTime() >= before && peer.getBlockRcvTime() <= after);
} finally {
field.set(handler, origin);
}
}

@Test
public void testBlockRcvTimeNotSetWhenValidationFails() throws Exception {
TronNetDelegate tronNetDelegate = Mockito.mock(TronNetDelegate.class);
Field field = handler.getClass().getDeclaredField("tronNetDelegate");
field.setAccessible(true);
Object origin = field.get(handler);
try {
field.set(handler, tronNetDelegate);

BlockCapsule blockCapsule = new BlockCapsule(1,
Sha256Hash.wrap(ByteString.copyFrom(ByteArray.fromHexString(
"9938a342238077182498b464ac0292229938a342238077182498b464ac029222"))),
1234, ByteString.copyFrom("1234567".getBytes()));

// validBlock returns false → processBlock short-circuits before setBlockRcvTime
Mockito.doReturn(false).when(tronNetDelegate).validBlock(any(BlockCapsule.class));

peer.setBlockRcvTime(0L);
Method method = handler.getClass()
.getDeclaredMethod("processBlock", PeerConnection.class, BlockCapsule.class);
method.setAccessible(true);
method.invoke(handler, peer, blockCapsule);

Assert.assertEquals("blockRcvTime must stay 0 when block fails validation",
0L, peer.getBlockRcvTime());
} finally {
field.set(handler, origin);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
package org.tron.core.net.messagehandler;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;

import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import org.tron.common.TestConstants;
import org.tron.common.utils.Sha256Hash;
import org.tron.core.capsule.BlockCapsule.BlockId;
import org.tron.core.config.args.Args;
import org.tron.core.net.TronNetDelegate;
import org.tron.core.net.message.adv.InventoryMessage;
import org.tron.core.net.peer.PeerConnection;
import org.tron.core.net.service.adv.AdvService;
import org.tron.p2p.connection.Channel;
import org.tron.protos.Protocol.Inventory.InventoryType;

Expand Down Expand Up @@ -51,6 +57,85 @@ public void testProcessMessage() throws Exception {
handler.processMessage(peer, msg);
}

@Test
public void testLastInteractiveTimeNotUpdatedForSolidifiedBlock() throws Exception {
InventoryMsgHandler handler = new InventoryMsgHandler();
Args.setParam(new String[]{}, TestConstants.TEST_CONF);

TronNetDelegate tronNetDelegate = mock(TronNetDelegate.class);
AdvService advService = mock(AdvService.class);
Mockito.when(advService.addInv(any())).thenReturn(true);
// block num 100 is at head boundary — should NOT update
Mockito.when(tronNetDelegate.getHeadBlockId())
.thenReturn(new BlockId(Sha256Hash.ZERO_HASH, 100L));

Field delegateField = handler.getClass().getDeclaredField("tronNetDelegate");
delegateField.setAccessible(true);
delegateField.set(handler, tronNetDelegate);
Field advField = handler.getClass().getDeclaredField("advService");
advField.setAccessible(true);
advField.set(handler, advService);

PeerConnection peer = new PeerConnection();
peer.setChannel(getChannel("1.0.0.4", 1001));
peer.setNeedSyncFromPeer(false);
peer.setNeedSyncFromUs(false);
peer.setLastInteractiveTime(0L);

// Block hash encodes num=100 (at solidified boundary — should NOT update)
Sha256Hash blockHash = new BlockId(Sha256Hash.ZERO_HASH, 100L);
InventoryMessage msg = new InventoryMessage(Arrays.asList(blockHash), InventoryType.BLOCK);
handler.processMessage(peer, msg);

Assert.assertEquals("lastInteractiveTime should NOT be updated for solidified block",
0L, peer.getLastInteractiveTime());
}

@Test
public void testLastInteractiveTimeUpdatedForBothPeersWithSameAboveSolidifiedBlock()
throws Exception {
InventoryMsgHandler handler = new InventoryMsgHandler();
Args.setParam(new String[]{}, TestConstants.TEST_CONF);

TronNetDelegate tronNetDelegate = mock(TronNetDelegate.class);
AdvService advService = mock(AdvService.class);
// First call returns true (peer1), second call returns false (peer2 — already in cache)
Mockito.when(advService.addInv(any())).thenReturn(true).thenReturn(false);
Mockito.when(tronNetDelegate.getHeadBlockId())
.thenReturn(new BlockId(Sha256Hash.ZERO_HASH, 99L));

Field delegateField = handler.getClass().getDeclaredField("tronNetDelegate");
delegateField.setAccessible(true);
delegateField.set(handler, tronNetDelegate);
Field advField = handler.getClass().getDeclaredField("advService");
advField.setAccessible(true);
advField.set(handler, advService);

PeerConnection peer1 = new PeerConnection();
peer1.setChannel(getChannel("1.0.0.5", 1002));
peer1.setNeedSyncFromPeer(false);
peer1.setNeedSyncFromUs(false);
peer1.setLastInteractiveTime(0L);

PeerConnection peer2 = new PeerConnection();
peer2.setChannel(getChannel("1.0.0.6", 1003));
peer2.setNeedSyncFromPeer(false);
peer2.setNeedSyncFromUs(false);
peer2.setLastInteractiveTime(0L);

// block num 100 > solidified 99 — both peers should update
Sha256Hash blockHash = new BlockId(Sha256Hash.ZERO_HASH, 100L);
InventoryMessage msg = new InventoryMessage(Arrays.asList(blockHash), InventoryType.BLOCK);

handler.processMessage(peer1, msg);
handler.processMessage(peer2, msg);

Assert.assertTrue("peer1 lastInteractiveTime should be updated",
peer1.getLastInteractiveTime() > 0L);
Assert.assertTrue("peer2 lastInteractiveTime should be updated even when addInv returns false",
peer2.getLastInteractiveTime() > 0L);
}

private Channel getChannel(String host, int port) throws Exception {
Channel channel = new Channel();
InetSocketAddress inetSocketAddress = new InetSocketAddress(host, port);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Resource;
import org.junit.After;
Expand Down Expand Up @@ -97,6 +98,57 @@ public void testDisconnectRandom() {
Assert.assertEquals(maxConnection - 1, PeerManager.getPeers().size());
}

@Test
public void testDisconnectRandomPreservesRecentBlockRcvTimePeer() {
int maxConnection = 30;
Assert.assertEquals(0, PeerManager.getPeers().size());

ApplicationContext ctx = (ApplicationContext) ReflectUtils.getFieldObject(p2pEventHandler,
"ctx");

// Create maxConnection + 1 peers (triggers disconnectRandom)
for (int i = 0; i < maxConnection + 1; i++) {
InetSocketAddress inetSocketAddress = new InetSocketAddress("202.0.0." + i, 10001);
Channel c1 = spy(Channel.class);
ReflectUtils.setFieldValue(c1, "inetSocketAddress", inetSocketAddress);
ReflectUtils.setFieldValue(c1, "inetAddress", inetSocketAddress.getAddress());
ReflectUtils.setFieldValue(c1, "ctx", spy(ChannelHandlerContext.class));
Mockito.doNothing().when(c1).send((byte[]) any());
PeerManager.add(ctx, c1);
}

// Set first minBroadcastPeerSize peers as broadcast-state
List<PeerConnection> peers = PeerManager.getPeers();
for (PeerConnection peer : peers.subList(0, ResilienceService.minBroadcastPeerSize)) {
peer.setNeedSyncFromPeer(false);
peer.setNeedSyncFromUs(false);
peer.setLastInteractiveTime(System.currentTimeMillis() - 1000);
}
for (PeerConnection peer : peers.subList(ResilienceService.minBroadcastPeerSize,
maxConnection + 1)) {
peer.setNeedSyncFromPeer(false);
peer.setNeedSyncFromUs(true);
}

// Give the LAST broadcast peer a very recent blockRcvTime — it must NOT be disconnected
PeerConnection bestPeer = peers.stream()
.filter(p -> !p.isNeedSyncFromUs() && !p.isNeedSyncFromPeer())
.reduce((a, b) -> b) // last broadcast peer
.orElseThrow(() -> new AssertionError("no broadcast peer"));
bestPeer.setBlockRcvTime(System.currentTimeMillis());

InetSocketAddress bestPeerAddress = bestPeer.getChannel().getInetSocketAddress();

// With minBroadcastPeerSize=3 broadcast peers, getRandomDisconnectionPeers returns
// the 1 peer with oldest blockRcvTime (0). bestPeer has most recent time → exempt.
ReflectUtils.invokeMethod(service, "disconnectRandom");

boolean bestPeerStillConnected = PeerManager.getPeers().stream()
.anyMatch(p -> p.getChannel().getInetSocketAddress().equals(bestPeerAddress));
Assert.assertTrue("Peer with most recent blockRcvTime should not be disconnected",
bestPeerStillConnected);
}

@Test
public void testDisconnectLan() {
int minConnection = 8;
Expand Down
Loading
Loading