From afc874a47c0b5778fc59270d0c31e4985e390851 Mon Sep 17 00:00:00 2001 From: wb Date: Mon, 20 Apr 2026 16:44:22 +0800 Subject: [PATCH 1/2] fix(net): prevent hello message replay Hello messages were accepted without checking timestamp freshness, allowing a previously signed witness message to be replayed indefinitely. Use a fixed five-minute freshness threshold and track the latest verified timestamp per witness so stale or repeated messages are rejected before trust is granted. --- .../core/net/service/relay/RelayService.java | 22 ++++ .../core/net/services/RelayServiceTest.java | 103 ++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java b/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java index d4e010ff21d..a7ff3de7cf6 100644 --- a/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java +++ b/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java @@ -1,5 +1,7 @@ package org.tron.core.net.service.relay; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.google.protobuf.ByteString; import java.net.InetSocketAddress; import java.util.Arrays; @@ -42,6 +44,7 @@ public class RelayService { private static final int MAX_PEER_COUNT_PER_ADDRESS = 5; + private static final long HELLO_MESSAGE_TIMESTAMP_THRESHOLD = TimeUnit.MINUTES.toMillis(5); @Autowired private ChainBaseManager chainBaseManager; @@ -74,6 +77,10 @@ public class RelayService { private int maxFastForwardNum = Args.getInstance().getMaxFastForwardNum(); + private final Cache helloReplayCache = CacheBuilder.newBuilder() + .maximumSize(100) + .build(); + public void init() { manager = ctx.getBean(Manager.class); witnessScheduleStore = ctx.getBean(WitnessScheduleStore.class); @@ -156,6 +163,20 @@ public boolean checkHelloMessage(HelloMessage message, Channel channel) { return false; } + long now = System.currentTimeMillis(); + if (now - msg.getTimestamp() > HELLO_MESSAGE_TIMESTAMP_THRESHOLD) { + logger.warn("HelloMessage from {}, timestamp age is {} ms", + channel.getInetAddress(), now - msg.getTimestamp()); + return false; + } + + Long lastTimestamp = helloReplayCache.getIfPresent(msg.getAddress()); + if (lastTimestamp != null && msg.getTimestamp() <= lastTimestamp) { + logger.warn("HelloMessage from {}, timestamp {} is not greater than last {}", + channel.getInetAddress(), msg.getTimestamp(), lastTimestamp); + return false; + } + boolean flag; try { Sha256Hash hash = Sha256Hash.of(CommonParameter @@ -172,6 +193,7 @@ public boolean checkHelloMessage(HelloMessage message, Channel channel) { flag = Arrays.equals(sigAddress, witnessPermissionAddress); } if (flag) { + helloReplayCache.put(msg.getAddress(), msg.getTimestamp()); TronNetService.getP2pConfig().getTrustNodes().add(channel.getInetAddress()); DesensitizedConverter.addSensitive(channel.getInetAddress().toString().substring(1), ByteArray.toHexString(msg.getAddress().toByteArray())); diff --git a/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java b/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java index 7c28757bd5c..0011448ed70 100644 --- a/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java +++ b/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java @@ -4,6 +4,7 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; +import com.google.common.cache.Cache; import com.google.protobuf.ByteString; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -13,6 +14,7 @@ import java.util.Comparator; import java.util.List; import java.util.Set; +import java.util.concurrent.TimeUnit; import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.util.encoders.Hex; @@ -73,6 +75,11 @@ public static void init() { @After public void clearPeers() { closePeer(); + Cache cache = + (Cache) ReflectUtils.getFieldObject(service, "helloReplayCache"); + if (cache != null) { + cache.invalidateAll(); + } } @Test @@ -250,6 +257,102 @@ private void testCheckHelloMessage() { } } + private HelloMessage buildSignedHello(long timestamp) throws Exception { + String key = "0154435f065a57fec6af1e12eaa2fa600030639448d7809f4c65bdcf8baed7e5"; + ByteString address = getFromHexString("418A8D690BF36806C36A7DAE3AF796643C1AA9CC01"); + Node node = new Node(NetUtil.getNodeId(), "127.0.0.1", null, 10001); + HelloMessage msg = new HelloMessage(node, timestamp, + ChainBaseManager.getChainBaseManager()); + SignInterface engine = SignUtils.fromPrivate(ByteArray.fromHexString(key), + Args.getInstance().isECKeyCryptoEngine()); + ByteString sig = ByteString.copyFrom(engine.Base64toBytes(engine + .signHash(Sha256Hash.of(CommonParameter.getInstance() + .isECKeyCryptoEngine(), ByteArray.fromLong(timestamp)).getBytes()))); + msg.setHelloMessage(msg.getHelloMessage().toBuilder() + .setAddress(address) + .setSignature(sig) + .build()); + return msg; + } + + private Channel buildChannel() { + InetSocketAddress addr = new InetSocketAddress("127.0.0.1", 10001); + Channel c = mock(Channel.class); + Mockito.when(c.getInetSocketAddress()).thenReturn(addr); + Mockito.when(c.getInetAddress()).thenReturn(addr.getAddress()); + return c; + } + + private void setupRelayServiceDeps() throws Exception { + Field f1 = service.getClass().getDeclaredField("witnessScheduleStore"); + f1.setAccessible(true); + f1.set(service, chainBaseManager.getWitnessScheduleStore()); + Field f2 = service.getClass().getDeclaredField("manager"); + f2.setAccessible(true); + f2.set(service, dbManager); + ReflectUtils.setFieldValue(tronNetService, "p2pConfig", new P2pConfig()); + } + + @Test + public void testCheckHelloMessage_staleTimestamp() throws Exception { + initWitness(); + setupRelayServiceDeps(); + Args.getInstance().fastForward = true; + long threshold = TimeUnit.MINUTES.toMillis(5); + long staleTimestamp = System.currentTimeMillis() - threshold - 1000; + HelloMessage msg = buildSignedHello(staleTimestamp); + boolean result = service.checkHelloMessage(msg, buildChannel()); + Assert.assertFalse(result); + } + + @Test + public void testCheckHelloMessage_freshTimestamp() throws Exception { + initWitness(); + setupRelayServiceDeps(); + Args.getInstance().fastForward = true; + long freshTimestamp = System.currentTimeMillis(); + HelloMessage msg = buildSignedHello(freshTimestamp); + boolean result = service.checkHelloMessage(msg, buildChannel()); + Assert.assertTrue(result); + } + + @Test + public void testCheckHelloMessage_replayRejected() throws Exception { + initWitness(); + setupRelayServiceDeps(); + Args.getInstance().fastForward = true; + long t = System.currentTimeMillis(); + HelloMessage msg1 = buildSignedHello(t); + Assert.assertTrue(service.checkHelloMessage(msg1, buildChannel())); + HelloMessage msg2 = buildSignedHello(t); + Assert.assertFalse(service.checkHelloMessage(msg2, buildChannel())); + } + + @Test + public void testCheckHelloMessage_strictlyLargerTimestampPasses() throws Exception { + initWitness(); + setupRelayServiceDeps(); + Args.getInstance().fastForward = true; + long t = System.currentTimeMillis(); + Assert.assertTrue(service.checkHelloMessage(buildSignedHello(t), buildChannel())); + Assert.assertTrue(service.checkHelloMessage(buildSignedHello(t + 1), buildChannel())); + } + + @Test + public void testCheckHelloMessage_badSigDoesNotPoisonCache() throws Exception { + initWitness(); + setupRelayServiceDeps(); + Args.getInstance().fastForward = true; + long t = System.currentTimeMillis(); + HelloMessage badMsg = buildSignedHello(t); + badMsg.setHelloMessage(badMsg.getHelloMessage().toBuilder() + .setSignature(ByteString.copyFrom(new byte[65])) + .build()); + Assert.assertFalse(service.checkHelloMessage(badMsg, buildChannel())); + HelloMessage goodMsg = buildSignedHello(t); + Assert.assertTrue(service.checkHelloMessage(goodMsg, buildChannel())); + } + @Test public void testNullWitnessAddress() { try { From c3847e9d0f6890c818c8f6529927a21f0da090a0 Mon Sep 17 00:00:00 2001 From: wb Date: Fri, 18 Sep 2026 11:57:55 +0800 Subject: [PATCH 2/2] fix(net): validate both sides of hello timestamp window --- .../core/net/service/relay/RelayService.java | 8 ++++-- .../core/net/services/RelayServiceTest.java | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java b/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java index a7ff3de7cf6..939d5fa4302 100644 --- a/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java +++ b/framework/src/main/java/org/tron/core/net/service/relay/RelayService.java @@ -164,9 +164,11 @@ public boolean checkHelloMessage(HelloMessage message, Channel channel) { } long now = System.currentTimeMillis(); - if (now - msg.getTimestamp() > HELLO_MESSAGE_TIMESTAMP_THRESHOLD) { - logger.warn("HelloMessage from {}, timestamp age is {} ms", - channel.getInetAddress(), now - msg.getTimestamp()); + long timestamp = msg.getTimestamp(); + if (timestamp < now - HELLO_MESSAGE_TIMESTAMP_THRESHOLD + || timestamp > now + HELLO_MESSAGE_TIMESTAMP_THRESHOLD) { + logger.warn("HelloMessage from {}, timestamp {} is outside the window around {}", + channel.getInetAddress(), timestamp, now); return false; } diff --git a/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java b/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java index 0011448ed70..0ee4fd3a4d5 100644 --- a/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java +++ b/framework/src/test/java/org/tron/core/net/services/RelayServiceTest.java @@ -305,6 +305,34 @@ public void testCheckHelloMessage_staleTimestamp() throws Exception { Assert.assertFalse(result); } + @Test + public void testCheckHelloMessage_futureTimestampDoesNotPoisonCache() throws Exception { + assertTimestampRejectedWithoutPoisoningCache( + System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)); + } + + @Test + public void testCheckHelloMessage_maxTimestampDoesNotPoisonCache() throws Exception { + assertTimestampRejectedWithoutPoisoningCache(Long.MAX_VALUE); + } + + @Test + public void testCheckHelloMessage_minTimestampDoesNotPoisonCache() throws Exception { + assertTimestampRejectedWithoutPoisoningCache(Long.MIN_VALUE); + } + + private void assertTimestampRejectedWithoutPoisoningCache(long timestamp) throws Exception { + initWitness(); + setupRelayServiceDeps(); + Args.getInstance().fastForward = true; + Channel channel = buildChannel(); + Assert.assertFalse(service.checkHelloMessage(buildSignedHello(timestamp), channel)); + Assert.assertFalse(TronNetService.getP2pConfig().getTrustNodes() + .contains(channel.getInetAddress())); + Assert.assertTrue(service.checkHelloMessage( + buildSignedHello(System.currentTimeMillis()), channel)); + } + @Test public void testCheckHelloMessage_freshTimestamp() throws Exception { initWitness();