Skip to content

Commit df7fc12

Browse files
author
Vasilchenko Igor
committed
Add optional anonymous transaction broadcast over Tor (native P2P)
When node.tor.enabled is true, transactions submitted to this node's own broadcast API are validated locally as before but, instead of being advertised to the node's directly-connected sync peers (which exposes the origin node's IP), are relayed via the native TRON P2P protocol through the local Tor SOCKS5 proxy to standard, unmodified nodes learned via the (UDP) discovery layer that are NOT currently used as sync peers. Those nodes re-gossip the transaction as usual, hiding the origin. Block sync and normal P2P traffic are unaffected. The feature is off by default. Reaching a Tor peer and completing the P2P handshake costs several seconds, which is paid per connection, not per transaction. To broadcast quickly even at a high transaction rate, the service keeps a pool of persistent Tor tunnels to verified, at-head relay nodes: each tunnel is handshaked once, kept alive (answering keep-alive pings) and reused for every broadcast. The read timeout of an established tunnel is set well above the peer keep-alive interval so an idle but healthy tunnel is not torn down between pings. Transactions are buffered and flushed as a single TransactionsMessage batch (capped) over broadcastCount tunnels in parallel, so a burst is delivered in about one round-trip; broadcastTransaction enqueues and returns immediately. Expired transactions are dropped from the buffer. Each tunnel advertises a fresh random public IP (never our real one, and a fresh random node id per hello, so tunnels can't be clustered or linked to our clear-net node) and echoes the peer's head block so the peer marks the connection sync-complete before it will fetch a transaction. Relay candidates are discovered nodes minus current sync peers; only nodes at (or near) our head are kept. Resilience: no transaction is lost if Tor or a relay is down or degraded — it stays buffered and is delivered once a tunnel can be (re)built. A flush never blocks the pool on a silently-dead tunnel (writes are bounded; a stalled tunnel is dropped and rebuilt). Pool maintenance refills only the missing tunnels (plus a small margin) and backs off exponentially when Tor is congested, so it does not flood a struggling Tor with circuit requests. If no tunnel can be built for several rounds and node.tor.controlPort is set, the node signals NEWNYM over the Tor control port to rebuild circuits with fresh exit IPs. libp2p is used as a library only; it is not modified. - NodeConfig/Args/CommonParameter: parse and hold node.tor.* config (incl. optional controlPort/controlPassword for NEWNYM recovery) - TronNetService: expose discovery-table nodes (getTableNodes) - TorBroadcastService: persistent Tor tunnel pool + batched native-P2P relay, with bounded writes, adaptive refill/backoff, NEWNYM recovery, idle-safe read timeout, expired-tx eviction, and public-only advertised addresses - Wallet.broadcastTransaction: route through Tor when enabled - config.conf: documented node.tor block - Tests: delivery over a persistent tunnel via a SOCKS5 forwarder; Tor-outage and relay-degradation recovery; NEWNYM on a stuck pool; and routing (Tor path when enabled, normal P2P broadcast when disabled)
1 parent 381d369 commit df7fc12

9 files changed

Lines changed: 1616 additions & 1 deletion

File tree

common/src/main/java/org/tron/common/parameter/CommonParameter.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,37 @@ public class CommonParameter {
141141
@Getter
142142
@Setter
143143
public int minParticipationRate;
144+
// -- Tor anonymous transaction broadcast --
145+
// When enabled, locally-originated transactions are sent through the Tor SOCKS5 proxy
146+
// to the HTTP broadcast API of standard (unmodified) TRON nodes instead of being advertised
147+
// to P2P peers, hiding the originating node's IP.
148+
@Getter
149+
@Setter
150+
public boolean torBroadcastEnable = false;
151+
@Getter
152+
@Setter
153+
public String torSocksHost = "127.0.0.1";
154+
@Getter
155+
@Setter
156+
public int torSocksPort = 9050;
157+
@Getter
158+
@Setter
159+
public int torConnectTimeout = 30000;
160+
@Getter
161+
@Setter
162+
public int torReadTimeout = 30000;
163+
@Getter
164+
@Setter
165+
public int torBroadcastCount = 2;
166+
@Getter
167+
@Setter
168+
public boolean torCircuitIsolation = true;
169+
@Getter
170+
@Setter
171+
public int torControlPort = 0;
172+
@Getter
173+
@Setter
174+
public String torControlPassword = "";
144175
@Getter
145176
public P2pConfig p2pConfig;
146177
@Getter

common/src/main/java/org/tron/core/config/args/NodeConfig.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,73 @@ public class NodeConfig {
117117
private List<String> fastForward = new ArrayList<>();
118118
private List<String> disabledApi = new ArrayList<>();
119119

120+
// node.tor.* — anonymous outbound transaction broadcast over the Tor SOCKS5 proxy.
121+
// Read manually in fromConfig() so an absent [node.tor] block keeps prior behaviour
122+
// and no reference.conf defaults are required.
123+
@Getter(lombok.AccessLevel.NONE)
124+
@Setter(lombok.AccessLevel.NONE)
125+
private boolean torBroadcastEnable = false;
126+
@Getter(lombok.AccessLevel.NONE)
127+
@Setter(lombok.AccessLevel.NONE)
128+
private String torSocksHost = "127.0.0.1";
129+
@Getter(lombok.AccessLevel.NONE)
130+
@Setter(lombok.AccessLevel.NONE)
131+
private int torSocksPort = 9050;
132+
@Getter(lombok.AccessLevel.NONE)
133+
@Setter(lombok.AccessLevel.NONE)
134+
private int torConnectTimeout = 30000;
135+
@Getter(lombok.AccessLevel.NONE)
136+
@Setter(lombok.AccessLevel.NONE)
137+
private int torReadTimeout = 30000;
138+
@Getter(lombok.AccessLevel.NONE)
139+
@Setter(lombok.AccessLevel.NONE)
140+
private int torBroadcastCount = 2;
141+
@Getter(lombok.AccessLevel.NONE)
142+
@Setter(lombok.AccessLevel.NONE)
143+
private boolean torCircuitIsolation = true;
144+
@Getter(lombok.AccessLevel.NONE)
145+
@Setter(lombok.AccessLevel.NONE)
146+
private int torControlPort = 0;
147+
@Getter(lombok.AccessLevel.NONE)
148+
@Setter(lombok.AccessLevel.NONE)
149+
private String torControlPassword = "";
150+
151+
public boolean isTorBroadcastEnable() {
152+
return torBroadcastEnable;
153+
}
154+
155+
public String getTorSocksHost() {
156+
return torSocksHost;
157+
}
158+
159+
public int getTorSocksPort() {
160+
return torSocksPort;
161+
}
162+
163+
public int getTorConnectTimeout() {
164+
return torConnectTimeout;
165+
}
166+
167+
public int getTorReadTimeout() {
168+
return torReadTimeout;
169+
}
170+
171+
public int getTorBroadcastCount() {
172+
return torBroadcastCount;
173+
}
174+
175+
public boolean isTorCircuitIsolation() {
176+
return torCircuitIsolation;
177+
}
178+
179+
public int getTorControlPort() {
180+
return torControlPort;
181+
}
182+
183+
public String getTorControlPassword() {
184+
return torControlPassword;
185+
}
186+
120187
// ---- Sub-object fields ----
121188
private P2pConfig p2p = new P2pConfig();
122189
private HttpConfig http = new HttpConfig();
@@ -394,6 +461,17 @@ public static NodeConfig fromConfig(Config config) {
394461
+ "Please use [node.allowShieldedTransactionApi] instead.");
395462
}
396463

464+
// node.tor.* — read manually so an absent block is a no-op (feature stays off)
465+
nc.torBroadcastEnable = getBool(section, "tor.enabled", false);
466+
nc.torSocksHost = getString(section, "tor.socksHost", "127.0.0.1");
467+
nc.torSocksPort = getInt(section, "tor.socksPort", 9050);
468+
nc.torConnectTimeout = getInt(section, "tor.connectTimeout", 30000);
469+
nc.torReadTimeout = getInt(section, "tor.readTimeout", 30000);
470+
nc.torBroadcastCount = getInt(section, "tor.broadcastCount", 2);
471+
nc.torCircuitIsolation = getBool(section, "tor.circuitIsolation", true);
472+
nc.torControlPort = getInt(section, "tor.controlPort", 0);
473+
nc.torControlPassword = getString(section, "tor.controlPassword", "");
474+
397475
// node.shutdown.* — PascalCase keys (BlockTime, BlockHeight), cannot auto-bind
398476
nc.shutdownBlockTime = config.hasPath("node.shutdown.BlockTime")
399477
? config.getString("node.shutdown.BlockTime") : "";

framework/src/main/java/org/tron/core/Wallet.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@
189189
import org.tron.core.net.TronNetDelegate;
190190
import org.tron.core.net.TronNetService;
191191
import org.tron.core.net.message.adv.TransactionMessage;
192+
import org.tron.core.net.service.tor.TorBroadcastService;
192193
import org.tron.core.store.AccountIdIndexStore;
193194
import org.tron.core.store.AccountStore;
194195
import org.tron.core.store.AccountTraceStore;
@@ -277,6 +278,8 @@ public class Wallet {
277278
@Autowired
278279
private TronNetService tronNetService;
279280
@Autowired
281+
private TorBroadcastService torBroadcastService;
282+
@Autowired
280283
private TronNetDelegate tronNetDelegate;
281284
@Autowired
282285
private Manager dbManager;
@@ -556,9 +559,24 @@ public GrpcAPI.Return broadcastTransaction(Transaction signedTransaction) {
556559
if (trx.getInstance().getRawData().getContractCount() == 0) {
557560
throw new ContractValidateException(ActuatorConstant.CONTRACT_NOT_EXIST);
558561
}
559-
TransactionMessage message = new TransactionMessage(trx.getInstance().toByteArray());
560562
trx.checkExpiration(chainBaseManager.getNextBlockSlotTime());
561563
dbManager.pushTransaction(trx);
564+
565+
if (CommonParameter.getInstance().isTorBroadcastEnable()) {
566+
// Anonymous path: the transaction is validated and kept locally, but instead of being
567+
// advertised to P2P peers (which would expose this node's IP as the origin) it is relayed
568+
// through the Tor SOCKS5 proxy to standard TRON nodes, which broadcast it to the network.
569+
int relayed = torBroadcastService.broadcastTransaction(signedTransaction);
570+
if (relayed == 0) {
571+
return builder.setResult(false).setCode(response_code.NOT_ENOUGH_EFFECTIVE_CONNECTION)
572+
.setMessage(ByteString.copyFromUtf8("Tor broadcast failed.")).build();
573+
}
574+
logger.info("Broadcast transaction {} via Tor to {} relay nodes successfully.",
575+
txID, relayed);
576+
return builder.setResult(true).setCode(response_code.SUCCESS).build();
577+
}
578+
579+
TransactionMessage message = new TransactionMessage(trx.getInstance().toByteArray());
562580
int num = tronNetService.fastBroadcastTransaction(message);
563581
if (num == 0 && minEffectiveConnection != 0) {
564582
return builder.setResult(false).setCode(response_code.NOT_ENOUGH_EFFECTIVE_CONNECTION)

framework/src/main/java/org/tron/core/config/args/Args.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,17 @@ private static void applyNodeConfig(NodeConfig nc) {
640640
PARAMETER.unsolidifiedBlockCheck = nc.isUnsolidifiedBlockCheck();
641641
PARAMETER.maxUnsolidifiedBlocks = nc.getMaxUnsolidifiedBlocks();
642642

643+
// ---- Tor anonymous transaction broadcast ----
644+
PARAMETER.torBroadcastEnable = nc.isTorBroadcastEnable();
645+
PARAMETER.torSocksHost = nc.getTorSocksHost();
646+
PARAMETER.torSocksPort = nc.getTorSocksPort();
647+
PARAMETER.torConnectTimeout = nc.getTorConnectTimeout();
648+
PARAMETER.torReadTimeout = nc.getTorReadTimeout();
649+
PARAMETER.torBroadcastCount = nc.getTorBroadcastCount();
650+
PARAMETER.torCircuitIsolation = nc.isTorCircuitIsolation();
651+
PARAMETER.torControlPort = nc.getTorControlPort();
652+
PARAMETER.torControlPassword = nc.getTorControlPassword();
653+
643654
// disabledApi list — lowercase normalization
644655
PARAMETER.disabledApiList = nc.getDisabledApi().isEmpty()
645656
? Collections.emptyList()

framework/src/main/java/org/tron/core/net/TronNetService.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.tron.core.net.service.sync.SyncService;
3232
import org.tron.p2p.P2pConfig;
3333
import org.tron.p2p.P2pService;
34+
import org.tron.p2p.discover.Node;
3435
import org.tron.p2p.utils.NetUtil;
3536

3637
@Slf4j(topic = "net")
@@ -138,6 +139,15 @@ public int fastBroadcastTransaction(TransactionMessage msg) {
138139
return advService.fastBroadcastTransaction(msg);
139140
}
140141

142+
/**
143+
* Nodes discovered via the (UDP) discovery layer. Only a subset of these is actually connected
144+
* as sync peers; the rest are known-but-unconnected and are used as anonymous Tor broadcast
145+
* targets (see TorBroadcastService).
146+
*/
147+
public List<Node> getTableNodes() {
148+
return p2pService.getTableNodes();
149+
}
150+
141151
public static boolean hasIpv4Stack(Set<String> ipSet) {
142152
for (String ip : ipSet) {
143153
InetAddress inetAddress;

0 commit comments

Comments
 (0)