From 47e0568a107eb0d09f6804219118a06ae0983ce8 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 18 Aug 2026 17:17:55 +0800 Subject: [PATCH 01/16] test(rpc): probe grpc interceptor ordering and thread affinity --- .../filter/GrpcInterceptorProbeTest.java | 266 ++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 framework/src/test/java/org/tron/core/services/filter/GrpcInterceptorProbeTest.java diff --git a/framework/src/test/java/org/tron/core/services/filter/GrpcInterceptorProbeTest.java b/framework/src/test/java/org/tron/core/services/filter/GrpcInterceptorProbeTest.java new file mode 100644 index 00000000000..d88d6576a03 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/filter/GrpcInterceptorProbeTest.java @@ -0,0 +1,266 @@ +package org.tron.core.services.filter; + +import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.stub.StreamObserver; +import java.net.ServerSocket; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.api.DatabaseGrpc; +import org.tron.api.DatabaseGrpc.DatabaseImplBase; +import org.tron.api.GrpcAPI.EmptyMessage; +import org.tron.protos.Protocol.Block; + +/** + * Pins down the two gRPC properties {@link CursorServerInterceptor} relies on. + * + *

First, which of two registered interceptors ends up innermost, i.e. closest to the handler. + * This determines whether a cursor interceptor must be registered before or after the interceptors + * added by the base service class. + * + *

Second, whether {@code Listener.onHalfClose()} runs on the same thread as the handler. The + * cursor is a {@link ThreadLocal}, so a cursor set anywhere else has no effect on the read path and + * fails silently, serving HEAD data from a cursor port. + */ +public class GrpcInterceptorProbeTest { + + /** Phase and thread of every observed step, in occurrence order. */ + private static final List TRACE = new CopyOnWriteArrayList<>(); + + private static String handlerThread; + + private Server server; + private ManagedChannel channel; + private ExecutorService executor; + + /** Records its position in the call flow without altering behaviour. */ + private static class ProbeInterceptor implements ServerInterceptor { + + private final String tag; + + ProbeInterceptor(String tag) { + this.tag = tag; + } + + @Override + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + record(tag, "interceptCall"); + return new SimpleForwardingServerCallListener(next.startCall(call, headers)) { + @Override + public void onHalfClose() { + record(tag, "onHalfClose-IN"); + super.onHalfClose(); + record(tag, "onHalfClose-OUT"); + } + }; + } + } + + private static void record(String tag, String phase) { + String line = String.format("%-14s %-16s thread=%s", + "[" + tag + "]", phase, Thread.currentThread().getName()); + TRACE.add(line); + System.out.println(line); + } + + /** Minimal synchronous unary service, matching the shape of the cursor-port services. */ + private static class ProbeDatabaseApi extends DatabaseImplBase { + @Override + public void getNowBlock(EmptyMessage request, StreamObserver observer) { + handlerThread = Thread.currentThread().getName(); + record("HANDLER", "execute"); + observer.onNext(Block.getDefaultInstance()); + observer.onCompleted(); + } + } + + @Before + public void setUp() throws Exception { + TRACE.clear(); + handlerThread = null; + int port = freePort(); + + // A fixed thread pool mirrors the production server configuration. + executor = Executors.newFixedThreadPool(2, r -> { + Thread t = new Thread(r); + t.setName("probe-rpc-executor-" + t.getId()); + return t; + }); + + server = ServerBuilder.forPort(port) + .executor(executor) + .addService(new ProbeDatabaseApi()) + .intercept(new ProbeInterceptor("A")) + .intercept(new ProbeInterceptor("B")) + .build() + .start(); + + channel = ManagedChannelBuilder.forAddress("127.0.0.1", port) + .usePlaintext().directExecutor().build(); + } + + @After + public void tearDown() throws Exception { + if (channel != null) { + channel.shutdownNow(); + } + if (server != null) { + server.shutdownNow(); + } + if (executor != null) { + executor.shutdown(); + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } + } + + /** + * Registration order versus nesting depth. The interceptor whose {@code onHalfClose} runs later + * is the innermost one; the printed conclusion states where a cursor interceptor belongs. + */ + @Test + public void testInterceptorOrdering() { + DatabaseGrpc.newBlockingStub(channel).getNowBlock(EmptyMessage.getDefaultInstance()); + + int aIn = indexOf("[A]", "onHalfClose-IN"); + int bIn = indexOf("[B]", "onHalfClose-IN"); + Assert.assertTrue("no onHalfClose captured for A", aIn >= 0); + Assert.assertTrue("no onHalfClose captured for B", bIn >= 0); + + boolean lastRegisteredIsInnermost = bIn > aIn; + System.out.println("\n===== ordering ====="); + System.out.println("registered: intercept(A) then intercept(B)"); + System.out.println(lastRegisteredIsInnermost + ? "B (registered last) is innermost -> register the cursor interceptor LAST" + : "A (registered first) is innermost -> register the cursor interceptor FIRST"); + System.out.println("====================\n"); + + int handlerIdx = indexOf("[HANDLER]", "execute"); + Assert.assertTrue("handler did not run inside both interceptors", + handlerIdx > aIn && handlerIdx > bIn); + } + + /** + * Thread affinity between {@code onHalfClose} and the handler. They must coincide for a + * ThreadLocal cursor set in the interceptor to be visible to the read path. + */ + @Test + public void testThreadAffinity() { + DatabaseGrpc.newBlockingStub(channel).getNowBlock(EmptyMessage.getDefaultInstance()); + + String halfCloseThread = threadOf("[B]", "onHalfClose-IN"); + Assert.assertNotNull("no thread captured for onHalfClose", halfCloseThread); + Assert.assertNotNull("no thread captured for the handler", handlerThread); + + System.out.println("\n===== thread affinity ====="); + System.out.println("onHalfClose thread = " + halfCloseThread); + System.out.println("handler thread = " + handlerThread); + System.out.println(halfCloseThread.equals(handlerThread) + ? "same thread -> a cursor set in onHalfClose reaches the handler" + : "different threads -> a ThreadLocal cursor cannot reach the handler"); + System.out.println("===========================\n"); + + Assert.assertEquals( + "onHalfClose and the handler must share a thread for the ThreadLocal cursor to apply", + handlerThread, halfCloseThread); + } + + /** + * End-to-end check that a ThreadLocal written in {@code onHalfClose} is observable by the + * handler, which is exactly how the cursor reaches the read path. + */ + @Test + public void testThreadLocalPropagation() throws Exception { + final ThreadLocal probe = new ThreadLocal<>(); + final String[] seenByHandler = new String[1]; + + int port = freePort(); + Server s = ServerBuilder.forPort(port) + .executor(executor) + .addService(new DatabaseImplBase() { + @Override + public void getNowBlock(EmptyMessage req, StreamObserver obs) { + seenByHandler[0] = probe.get(); + obs.onNext(Block.getDefaultInstance()); + obs.onCompleted(); + } + }) + .intercept(new ServerInterceptor() { + @Override + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + return new SimpleForwardingServerCallListener(next.startCall(call, headers)) { + @Override + public void onHalfClose() { + try { + probe.set("SET_BY_INTERCEPTOR"); + super.onHalfClose(); + } finally { + probe.remove(); + } + } + }; + } + }) + .build() + .start(); + + ManagedChannel ch = ManagedChannelBuilder.forAddress("127.0.0.1", port) + .usePlaintext().directExecutor().build(); + try { + DatabaseGrpc.newBlockingStub(ch).getNowBlock(EmptyMessage.getDefaultInstance()); + System.out.println("\n===== ThreadLocal propagation ====="); + System.out.println("value seen by handler = " + seenByHandler[0]); + System.out.println("===================================\n"); + + Assert.assertEquals( + "a ThreadLocal set in onHalfClose must be visible to the handler", + "SET_BY_INTERCEPTOR", seenByHandler[0]); + } finally { + ch.shutdownNow(); + s.shutdownNow(); + s.awaitTermination(5, TimeUnit.SECONDS); + } + } + + private static int freePort() throws Exception { + try (ServerSocket socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } + } + + private int indexOf(String tag, String phase) { + for (int i = 0; i < TRACE.size(); i++) { + String line = TRACE.get(i); + if (line.contains(tag) && line.contains(phase)) { + return i; + } + } + return -1; + } + + private String threadOf(String tag, String phase) { + int i = indexOf(tag, phase); + if (i < 0) { + return null; + } + String line = TRACE.get(i); + return line.substring(line.indexOf("thread=") + "thread=".length()).trim(); + } +} From 44a40435a061c0abf0462f903f668524adbf7bbf Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 18 Aug 2026 17:17:55 +0800 Subject: [PATCH 02/16] feat(rpc): add cursor server interceptors for solidity and pbft --- .../filter/CursorServerInterceptor.java | 62 +++++++++++++++++++ .../filter/PbftCursorInterceptor.java | 15 +++++ .../filter/SolidityCursorInterceptor.java | 15 +++++ 3 files changed, 92 insertions(+) create mode 100644 framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java create mode 100644 framework/src/main/java/org/tron/core/services/filter/PbftCursorInterceptor.java create mode 100644 framework/src/main/java/org/tron/core/services/filter/SolidityCursorInterceptor.java diff --git a/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java b/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java new file mode 100644 index 00000000000..81b54ad2b57 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java @@ -0,0 +1,62 @@ +package org.tron.core.services.filter; + +import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.tron.core.db.Manager; +import org.tron.core.db2.core.Chainbase; + +/** + * Switches the read cursor of the current thread for the duration of a gRPC call, and restores it + * afterwards. Every call served by a server carrying this interceptor therefore reads from the + * snapshot the subclass selects. + * + *

The service implementations behind it make no assumption about the cursor and never touch it; + * which snapshot a read resolves to is decided solely by the calling thread's cursor. A single + * service instance can thus serve HEAD, SOLIDITY and PBFT semantics on different servers. + * + *

Two invariants this class depends on

+ * + *

The bracket must wrap {@code Listener.onHalfClose()}, not the body of + * {@code interceptCall}. {@code interceptCall} runs when the call arrives and may execute on an + * IO thread, while the handler that reads the database runs in {@code onHalfClose()} on an executor + * thread. The cursor is a {@link ThreadLocal}: setting it on the IO thread has no effect on the + * executor thread, and nothing throws — the port silently serves HEAD data instead. {@code + * GrpcInterceptorProbeTest} asserts this thread affinity; re-run it before changing this class. + * + *

All handlers must be synchronous unary. The cursor is reset as soon as + * {@code onHalfClose} returns, which requires handlers to complete {@code onNext/onCompleted} + * inline. A handler that defers its database reads to another thread or an asynchronous callback + * would read after the reset and observe HEAD data. + * + *

The {@code finally} block is mandatory: gRPC serves calls from a fixed thread pool, so a + * cursor left behind would leak into the next call handled by the same thread. + */ +public abstract class CursorServerInterceptor implements ServerInterceptor { + + @Autowired + protected Manager dbManager; + + /** Snapshot every call on this server reads from; set by the subclass. */ + protected Chainbase.Cursor cursor; + + @Override + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + return new SimpleForwardingServerCallListener(next.startCall(call, headers)) { + @Override + public void onHalfClose() { + try { + // For PBFT the offset is computed inside Manager#setCursor at call time. + dbManager.setCursor(cursor); + super.onHalfClose(); + } finally { + dbManager.resetCursor(); + } + } + }; + } +} diff --git a/framework/src/main/java/org/tron/core/services/filter/PbftCursorInterceptor.java b/framework/src/main/java/org/tron/core/services/filter/PbftCursorInterceptor.java new file mode 100644 index 00000000000..3ea55501d35 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/filter/PbftCursorInterceptor.java @@ -0,0 +1,15 @@ +package org.tron.core.services.filter; + +import org.springframework.stereotype.Component; +import org.tron.core.db2.core.Chainbase; + +/** + * Makes every call on the PBFT gRPC server read the PBFT-confirmed state view. + */ +@Component +public class PbftCursorInterceptor extends CursorServerInterceptor { + + public PbftCursorInterceptor() { + this.cursor = Chainbase.Cursor.PBFT; + } +} diff --git a/framework/src/main/java/org/tron/core/services/filter/SolidityCursorInterceptor.java b/framework/src/main/java/org/tron/core/services/filter/SolidityCursorInterceptor.java new file mode 100644 index 00000000000..5cf95ca0db4 --- /dev/null +++ b/framework/src/main/java/org/tron/core/services/filter/SolidityCursorInterceptor.java @@ -0,0 +1,15 @@ +package org.tron.core.services.filter; + +import org.springframework.stereotype.Component; +import org.tron.core.db2.core.Chainbase; + +/** + * Makes every call on the Solidity gRPC server read the solidified state view. + */ +@Component +public class SolidityCursorInterceptor extends CursorServerInterceptor { + + public SolidityCursorInterceptor() { + this.cursor = Chainbase.Cursor.SOLIDITY; + } +} From 96842cae6937f54798192d6f3f14b011d0868a55 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 18 Aug 2026 17:17:55 +0800 Subject: [PATCH 03/16] refactor(rpc): serve solidity grpc via shared service instances --- .../RpcApiServiceOnSolidity.java | 471 +----------------- 1 file changed, 12 insertions(+), 459 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/RpcApiServiceOnSolidity.java b/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/RpcApiServiceOnSolidity.java index 315d70df8d6..a65146a4e39 100755 --- a/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/RpcApiServiceOnSolidity.java +++ b/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/RpcApiServiceOnSolidity.java @@ -1,70 +1,21 @@ package org.tron.core.services.interfaceOnSolidity; -import com.google.protobuf.ByteString; import io.grpc.netty.NettyServerBuilder; -import io.grpc.stub.StreamObserver; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.tron.api.DatabaseGrpc.DatabaseImplBase; -import org.tron.api.GrpcAPI; -import org.tron.api.GrpcAPI.AssetIssueList; -import org.tron.api.GrpcAPI.BlockExtention; -import org.tron.api.GrpcAPI.BlockReference; -import org.tron.api.GrpcAPI.BytesMessage; -import org.tron.api.GrpcAPI.CanDelegatedMaxSizeRequestMessage; -import org.tron.api.GrpcAPI.CanDelegatedMaxSizeResponseMessage; -import org.tron.api.GrpcAPI.CanWithdrawUnfreezeAmountRequestMessage; -import org.tron.api.GrpcAPI.CanWithdrawUnfreezeAmountResponseMessage; -import org.tron.api.GrpcAPI.DelegatedResourceList; -import org.tron.api.GrpcAPI.DelegatedResourceMessage; -import org.tron.api.GrpcAPI.EmptyMessage; -import org.tron.api.GrpcAPI.ExchangeList; -import org.tron.api.GrpcAPI.GetAvailableUnfreezeCountRequestMessage; -import org.tron.api.GrpcAPI.GetAvailableUnfreezeCountResponseMessage; -import org.tron.api.GrpcAPI.NoteParameters; -import org.tron.api.GrpcAPI.NumberMessage; -import org.tron.api.GrpcAPI.PaginatedMessage; -import org.tron.api.GrpcAPI.PricesResponseMessage; -import org.tron.api.GrpcAPI.Return; -import org.tron.api.GrpcAPI.Return.response_code; -import org.tron.api.GrpcAPI.SpendResult; -import org.tron.api.GrpcAPI.TransactionExtention; -import org.tron.api.GrpcAPI.TransactionInfoList; -import org.tron.api.GrpcAPI.WitnessList; -import org.tron.api.WalletSolidityGrpc.WalletSolidityImplBase; import org.tron.common.application.RpcService; -import org.tron.common.parameter.CommonParameter; -import org.tron.common.utils.Sha256Hash; -import org.tron.core.capsule.BlockCapsule; import org.tron.core.config.args.Args; import org.tron.core.services.RpcApiService; -import org.tron.protos.Protocol.Account; -import org.tron.protos.Protocol.Block; -import org.tron.protos.Protocol.DelegatedResourceAccountIndex; -import org.tron.protos.Protocol.DynamicProperties; -import org.tron.protos.Protocol.Exchange; -import org.tron.protos.Protocol.MarketOrder; -import org.tron.protos.Protocol.MarketOrderList; -import org.tron.protos.Protocol.MarketOrderPair; -import org.tron.protos.Protocol.MarketOrderPairList; -import org.tron.protos.Protocol.MarketPriceList; -import org.tron.protos.Protocol.Transaction; -import org.tron.protos.Protocol.TransactionInfo; -import org.tron.protos.contract.AssetIssueContractOuterClass.AssetIssueContract; -import org.tron.protos.contract.ShieldContract.IncrementalMerkleVoucherInfo; -import org.tron.protos.contract.ShieldContract.OutputPointInfo; -import org.tron.protos.contract.SmartContractOuterClass.TriggerSmartContract; - +import org.tron.core.services.filter.SolidityCursorInterceptor; @Slf4j(topic = "API") public class RpcApiServiceOnSolidity extends RpcService { - @Autowired - private WalletOnSolidity walletOnSolidity; + private RpcApiService rpcApiService; @Autowired - private RpcApiService rpcApiService; + private SolidityCursorInterceptor solidityCursorInterceptor; public RpcApiServiceOnSolidity() { port = Args.getInstance().getRpcOnSolidityPort(); @@ -74,415 +25,17 @@ public RpcApiServiceOnSolidity() { @Override protected void addService(NettyServerBuilder serverBuilder) { - serverBuilder.addService(new DatabaseApi()); - serverBuilder.addService(new WalletSolidityApi()); + serverBuilder.addService(rpcApiService.getDatabaseApi()); + serverBuilder.addService(rpcApiService.getWalletSolidityApi()); } - private TransactionExtention transaction2Extention(Transaction transaction) { - if (transaction == null) { - return null; - } - TransactionExtention.Builder trxExtBuilder = TransactionExtention.newBuilder(); - Return.Builder retBuilder = Return.newBuilder(); - trxExtBuilder.setTransaction(transaction); - trxExtBuilder.setTxid(Sha256Hash.of(CommonParameter.getInstance().isECKeyCryptoEngine(), - transaction.getRawData().toByteArray()).getByteString()); - retBuilder.setResult(true).setCode(response_code.SUCCESS); - trxExtBuilder.setResult(retBuilder); - return trxExtBuilder.build(); - } - - private BlockExtention block2Extention(Block block) { - if (block == null) { - return null; - } - BlockExtention.Builder builder = BlockExtention.newBuilder(); - BlockCapsule blockCapsule = new BlockCapsule(block); - builder.setBlockHeader(block.getBlockHeader()); - builder.setBlockid(ByteString.copyFrom(blockCapsule.getBlockId().getBytes())); - for (int i = 0; i < block.getTransactionsCount(); i++) { - Transaction transaction = block.getTransactions(i); - builder.addTransactions(transaction2Extention(transaction)); - } - return builder.build(); - } - - /** - * DatabaseApi. - */ - private class DatabaseApi extends DatabaseImplBase { - - @Override - public void getBlockReference(EmptyMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getDatabaseApi().getBlockReference(request, responseObserver)); - } - - @Override - public void getNowBlock(EmptyMessage request, StreamObserver responseObserver) { - walletOnSolidity - .futureGet(() -> rpcApiService.getDatabaseApi().getNowBlock(request, responseObserver)); - } - - @Override - public void getBlockByNum(NumberMessage request, StreamObserver responseObserver) { - walletOnSolidity - .futureGet(() -> rpcApiService.getDatabaseApi().getBlockByNum(request, responseObserver)); - } - - @Override - public void getDynamicProperties(EmptyMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getDatabaseApi().getDynamicProperties(request, responseObserver)); - } + @Override + protected void addInterceptor(NettyServerBuilder serverBuilder) { + // Registered first so it is innermost, wrapping the handler alone (in gRPC 1.83.0 the + // first-registered interceptor is closest to the handler, pinned by GrpcInterceptorProbeTest). + // It scopes the SOLIDITY cursor to the data read. + serverBuilder.intercept(solidityCursorInterceptor); + super.addInterceptor(serverBuilder); } - /** - * WalletSolidityApi. - */ - private class WalletSolidityApi extends WalletSolidityImplBase { - - @Override - public void getAccount(Account request, StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getAccount(request, responseObserver)); - } - - @Override - public void getAccountById(Account request, StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getAccountById(request, responseObserver)); - } - - @Override - public void listWitnesses(EmptyMessage request, StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().listWitnesses(request, responseObserver)); - } - - public void getPaginatedNowWitnessList(PaginatedMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getPaginatedNowWitnessList(request, responseObserver)); - } - - @Override - public void getAssetIssueById(BytesMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getAssetIssueById(request, responseObserver)); - } - - @Override - public void getAssetIssueByName(BytesMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .getAssetIssueByName(request, responseObserver)); - } - - @Override - public void getAssetIssueList(EmptyMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getAssetIssueList(request, responseObserver)); - } - - @Override - public void getAssetIssueListByName(BytesMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .getAssetIssueListByName(request, responseObserver)); - } - - @Override - public void getPaginatedAssetIssueList(PaginatedMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .getPaginatedAssetIssueList(request, responseObserver)); - } - - @Override - public void getExchangeById(BytesMessage request, StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getExchangeById(request, responseObserver)); - } - - @Override - public void getNowBlock(EmptyMessage request, StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getNowBlock(request, responseObserver)); - } - - @Override - public void getNowBlock2(EmptyMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getNowBlock2(request, responseObserver)); - - } - - @Override - public void getBlockByNum(NumberMessage request, StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getBlockByNum(request, responseObserver)); - } - - @Override - public void getBlockByNum2(NumberMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getBlockByNum2(request, responseObserver)); - } - - @Override - public void getDelegatedResource(DelegatedResourceMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .getDelegatedResource(request, responseObserver)); - } - - @Override - public void getDelegatedResourceV2(DelegatedResourceMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .getDelegatedResourceV2(request, responseObserver)); - } - - @Override - public void getDelegatedResourceAccountIndex(BytesMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .getDelegatedResourceAccountIndex(request, responseObserver)); - } - - @Override - public void getDelegatedResourceAccountIndexV2(BytesMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .getDelegatedResourceAccountIndexV2(request, responseObserver)); - } - - @Override - public void getCanDelegatedMaxSize(CanDelegatedMaxSizeRequestMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .getCanDelegatedMaxSize(request, responseObserver)); - } - - @Override - public void getAvailableUnfreezeCount(GetAvailableUnfreezeCountRequestMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .getAvailableUnfreezeCount(request, responseObserver)); - } - - @Override - public void getCanWithdrawUnfreezeAmount(CanWithdrawUnfreezeAmountRequestMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .getCanWithdrawUnfreezeAmount(request, responseObserver)); - } - - @Override - public void getTransactionCountByBlockNum(NumberMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .getTransactionCountByBlockNum(request, responseObserver)); - } - - @Override - public void getTransactionById(BytesMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getTransactionById(request, responseObserver)); - - } - - @Override - public void getTransactionInfoById(BytesMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .getTransactionInfoById(request, responseObserver)); - - } - - @Override - public void listExchanges(EmptyMessage request, StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().listExchanges(request, responseObserver)); - } - - @Override - public void triggerConstantContract(TriggerSmartContract request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .triggerConstantContract(request, responseObserver)); - } - - @Override - public void estimateEnergy(TriggerSmartContract request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .estimateEnergy(request, responseObserver)); - } - - @Override - public void getRewardInfo(BytesMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getRewardInfo(request, responseObserver)); - } - - @Override - public void getBrokerageInfo(BytesMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getBrokerageInfo(request, responseObserver)); - } - - @Override - public void getMerkleTreeVoucherInfo(OutputPointInfo request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .getMerkleTreeVoucherInfo(request, responseObserver)); - } - - @Override - public void scanNoteByIvk(GrpcAPI.IvkDecryptParameters request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().scanNoteByIvk(request, responseObserver)); - } - - @Override - public void scanAndMarkNoteByIvk(GrpcAPI.IvkDecryptAndMarkParameters request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .scanAndMarkNoteByIvk(request, responseObserver)); - } - - @Override - public void scanNoteByOvk(GrpcAPI.OvkDecryptParameters request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().scanNoteByOvk(request, responseObserver)); - } - - @Override - public void isSpend(NoteParameters request, StreamObserver responseObserver) { - walletOnSolidity - .futureGet(() -> rpcApiService.getWalletSolidityApi().isSpend(request, responseObserver)); - } - - @Override - public void getTransactionInfoByBlockNum(NumberMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet(() -> rpcApiService.getWalletSolidityApi() - .getTransactionInfoByBlockNum(request, responseObserver)); - } - - @Override - public void scanShieldedTRC20NotesByIvk(GrpcAPI.IvkDecryptTRC20Parameters request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .scanShieldedTRC20NotesByIvk(request, responseObserver) - ); - } - - @Override - public void scanShieldedTRC20NotesByOvk(GrpcAPI.OvkDecryptTRC20Parameters request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .scanShieldedTRC20NotesByOvk(request, responseObserver) - ); - } - - @Override - public void isShieldedTRC20ContractNoteSpent(GrpcAPI.NfTRC20Parameters request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .isShieldedTRC20ContractNoteSpent(request, responseObserver) - ); - } - - @Override - public void getMarketOrderByAccount(BytesMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getMarketOrderByAccount(request, responseObserver) - ); - } - - @Override - public void getMarketOrderById(BytesMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getMarketOrderById(request, responseObserver) - ); - } - - @Override - public void getMarketPriceByPair(MarketOrderPair request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getMarketPriceByPair(request, responseObserver) - ); - } - - @Override - public void getMarketOrderListByPair(org.tron.protos.Protocol.MarketOrderPair request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getMarketOrderListByPair(request, responseObserver) - ); - } - - @Override - public void getMarketPairList(EmptyMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getMarketPairList(request, responseObserver) - ); - } - - @Override - public void getBurnTrx(EmptyMessage request, StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getBurnTrx(request, responseObserver) - ); - } - - @Override - public void getBlock(GrpcAPI.BlockReq request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getBlock(request, responseObserver)); - } - - @Override - public void getBandwidthPrices(EmptyMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getBandwidthPrices(request, responseObserver)); - } - - @Override - public void getEnergyPrices(EmptyMessage request, - StreamObserver responseObserver) { - walletOnSolidity.futureGet( - () -> rpcApiService.getWalletSolidityApi().getEnergyPrices(request, responseObserver)); - } - - } } From d7300ae4c0de630a1dbe994451c20ceef6a56b77 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 18 Aug 2026 17:17:55 +0800 Subject: [PATCH 04/16] refactor(rpc): serve pbft grpc via shared service instances --- .../interfaceOnPBFT/RpcApiServiceOnPBFT.java | 478 +----------------- 1 file changed, 12 insertions(+), 466 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/RpcApiServiceOnPBFT.java b/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/RpcApiServiceOnPBFT.java index 54e7b69f7fc..98c82a2118a 100755 --- a/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/RpcApiServiceOnPBFT.java +++ b/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/RpcApiServiceOnPBFT.java @@ -1,67 +1,21 @@ package org.tron.core.services.interfaceOnPBFT; import io.grpc.netty.NettyServerBuilder; -import io.grpc.stub.StreamObserver; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.tron.api.DatabaseGrpc.DatabaseImplBase; -import org.tron.api.GrpcAPI; -import org.tron.api.GrpcAPI.AssetIssueList; -import org.tron.api.GrpcAPI.BlockExtention; -import org.tron.api.GrpcAPI.BlockReference; -import org.tron.api.GrpcAPI.BytesMessage; -import org.tron.api.GrpcAPI.CanDelegatedMaxSizeRequestMessage; -import org.tron.api.GrpcAPI.CanDelegatedMaxSizeResponseMessage; -import org.tron.api.GrpcAPI.CanWithdrawUnfreezeAmountRequestMessage; -import org.tron.api.GrpcAPI.CanWithdrawUnfreezeAmountResponseMessage; -import org.tron.api.GrpcAPI.DecryptNotesTRC20; -import org.tron.api.GrpcAPI.DelegatedResourceList; -import org.tron.api.GrpcAPI.DelegatedResourceMessage; -import org.tron.api.GrpcAPI.EmptyMessage; -import org.tron.api.GrpcAPI.ExchangeList; -import org.tron.api.GrpcAPI.GetAvailableUnfreezeCountRequestMessage; -import org.tron.api.GrpcAPI.GetAvailableUnfreezeCountResponseMessage; -import org.tron.api.GrpcAPI.IvkDecryptTRC20Parameters; -import org.tron.api.GrpcAPI.NfTRC20Parameters; -import org.tron.api.GrpcAPI.NoteParameters; -import org.tron.api.GrpcAPI.NullifierResult; -import org.tron.api.GrpcAPI.NumberMessage; -import org.tron.api.GrpcAPI.OvkDecryptTRC20Parameters; -import org.tron.api.GrpcAPI.PaginatedMessage; -import org.tron.api.GrpcAPI.PricesResponseMessage; -import org.tron.api.GrpcAPI.SpendResult; -import org.tron.api.GrpcAPI.TransactionExtention; -import org.tron.api.GrpcAPI.WitnessList; -import org.tron.api.WalletSolidityGrpc.WalletSolidityImplBase; import org.tron.common.application.RpcService; import org.tron.core.config.args.Args; import org.tron.core.services.RpcApiService; -import org.tron.protos.Protocol.Account; -import org.tron.protos.Protocol.Block; -import org.tron.protos.Protocol.DelegatedResourceAccountIndex; -import org.tron.protos.Protocol.DynamicProperties; -import org.tron.protos.Protocol.Exchange; -import org.tron.protos.Protocol.MarketOrder; -import org.tron.protos.Protocol.MarketOrderList; -import org.tron.protos.Protocol.MarketOrderPair; -import org.tron.protos.Protocol.MarketOrderPairList; -import org.tron.protos.Protocol.MarketPriceList; -import org.tron.protos.Protocol.Transaction; -import org.tron.protos.Protocol.TransactionInfo; -import org.tron.protos.contract.AssetIssueContractOuterClass.AssetIssueContract; -import org.tron.protos.contract.ShieldContract.IncrementalMerkleVoucherInfo; -import org.tron.protos.contract.ShieldContract.OutputPointInfo; -import org.tron.protos.contract.SmartContractOuterClass.TriggerSmartContract; - +import org.tron.core.services.filter.PbftCursorInterceptor; @Slf4j(topic = "API") public class RpcApiServiceOnPBFT extends RpcService { @Autowired - private WalletOnPBFT walletOnPBFT; + private RpcApiService rpcApiService; @Autowired - private RpcApiService rpcApiService; + private PbftCursorInterceptor pbftCursorInterceptor; public RpcApiServiceOnPBFT() { port = Args.getInstance().getRpcOnPBFTPort(); @@ -71,425 +25,17 @@ public RpcApiServiceOnPBFT() { @Override protected void addService(NettyServerBuilder serverBuilder) { - serverBuilder.addService(new DatabaseApi()); - serverBuilder.addService(new WalletPBFTApi()); + serverBuilder.addService(rpcApiService.getDatabaseApi()); + serverBuilder.addService(rpcApiService.getWalletSolidityApi()); } - /** - * DatabaseApi. - */ - private class DatabaseApi extends DatabaseImplBase { - - @Override - public void getBlockReference(EmptyMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getDatabaseApi().getBlockReference(request, responseObserver) - ); - } - - @Override - public void getNowBlock(EmptyMessage request, StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getDatabaseApi().getNowBlock(request, responseObserver)); - } - - @Override - public void getBlockByNum(NumberMessage request, StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getDatabaseApi().getBlockByNum(request, responseObserver) - ); - } - - @Override - public void getDynamicProperties(EmptyMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getDatabaseApi().getDynamicProperties(request, responseObserver) - ); - } + @Override + protected void addInterceptor(NettyServerBuilder serverBuilder) { + // Registered first so it is innermost, wrapping the handler alone (in gRPC 1.83.0 the + // first-registered interceptor is closest to the handler, pinned by GrpcInterceptorProbeTest). + // It scopes the PBFT cursor to the data read. + serverBuilder.intercept(pbftCursorInterceptor); + super.addInterceptor(serverBuilder); } - /** - * WalletPBFTApi. - */ - private class WalletPBFTApi extends WalletSolidityImplBase { - - @Override - public void getAccount(Account request, StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getAccount(request, responseObserver) - ); - } - - @Override - public void getAccountById(Account request, StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getAccountById(request, responseObserver) - ); - } - - @Override - public void listWitnesses(EmptyMessage request, StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().listWitnesses(request, responseObserver) - ); - } - - @Override - public void getAssetIssueById(BytesMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getAssetIssueById(request, responseObserver) - ); - } - - @Override - public void getAssetIssueByName(BytesMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getAssetIssueByName(request, responseObserver) - ); - } - - @Override - public void getAssetIssueList(EmptyMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getAssetIssueList(request, responseObserver) - ); - } - - @Override - public void getAssetIssueListByName(BytesMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getAssetIssueListByName(request, responseObserver) - ); - } - - @Override - public void getPaginatedAssetIssueList(PaginatedMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getPaginatedAssetIssueList(request, responseObserver) - ); - } - - @Override - public void getExchangeById(BytesMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getExchangeById( - request, responseObserver - ) - ); - } - - @Override - public void getNowBlock(EmptyMessage request, StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getNowBlock(request, responseObserver) - ); - } - - @Override - public void getNowBlock2(EmptyMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getNowBlock2(request, responseObserver) - ); - - } - - @Override - public void getBlockByNum(NumberMessage request, StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getBlockByNum(request, responseObserver) - ); - } - - @Override - public void getBlockByNum2(NumberMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getBlockByNum2(request, responseObserver) - ); - } - - @Override - public void getDelegatedResource(DelegatedResourceMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getDelegatedResource(request, responseObserver) - ); - } - - @Override - public void getDelegatedResourceV2(DelegatedResourceMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getDelegatedResourceV2(request, responseObserver) - ); - } - - @Override - public void getDelegatedResourceAccountIndex(BytesMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getDelegatedResourceAccountIndex(request, responseObserver) - ); - } - - @Override - public void getDelegatedResourceAccountIndexV2(BytesMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getDelegatedResourceAccountIndexV2(request, responseObserver) - ); - } - - @Override - public void getCanDelegatedMaxSize(CanDelegatedMaxSizeRequestMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getCanDelegatedMaxSize(request, responseObserver) - ); - } - - @Override - public void getAvailableUnfreezeCount(GetAvailableUnfreezeCountRequestMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getAvailableUnfreezeCount(request, responseObserver) - ); - } - - @Override - public void getCanWithdrawUnfreezeAmount(CanWithdrawUnfreezeAmountRequestMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getCanWithdrawUnfreezeAmount(request, responseObserver) - ); - } - - @Override - public void getTransactionCountByBlockNum(NumberMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getTransactionCountByBlockNum(request, responseObserver) - ); - } - - @Override - public void getTransactionById(BytesMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getTransactionById(request, responseObserver) - ); - - } - - @Override - public void getTransactionInfoById(BytesMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getTransactionInfoById(request, responseObserver) - ); - - } - - @Override - public void listExchanges(EmptyMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().listExchanges(request, responseObserver) - ); - } - - @Override - public void triggerConstantContract(TriggerSmartContract request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .triggerConstantContract(request, responseObserver) - ); - } - - @Override - public void estimateEnergy(TriggerSmartContract request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .estimateEnergy(request, responseObserver) - ); - } - - @Override - public void getRewardInfo(BytesMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getRewardInfo(request, responseObserver) - ); - } - - @Override - public void getBrokerageInfo(BytesMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getBrokerageInfo(request, responseObserver) - ); - } - - @Override - public void getMerkleTreeVoucherInfo(OutputPointInfo request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getMerkleTreeVoucherInfo(request, responseObserver) - ); - } - - @Override - public void scanNoteByIvk(GrpcAPI.IvkDecryptParameters request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().scanNoteByIvk(request, responseObserver) - ); - } - - @Override - public void scanAndMarkNoteByIvk(GrpcAPI.IvkDecryptAndMarkParameters request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().scanAndMarkNoteByIvk(request, responseObserver) - ); - } - - @Override - public void scanNoteByOvk(GrpcAPI.OvkDecryptParameters request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().scanNoteByOvk(request, responseObserver) - ); - } - - @Override - public void isSpend(NoteParameters request, StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().isSpend(request, responseObserver) - ); - } - - @Override - public void getMarketOrderByAccount(BytesMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getMarketOrderByAccount(request, responseObserver) - ); - } - - @Override - public void getMarketOrderById(BytesMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getMarketOrderById(request, responseObserver) - ); - } - - @Override - public void getMarketPriceByPair(MarketOrderPair request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getMarketPriceByPair(request, responseObserver) - ); - } - - @Override - public void getMarketOrderListByPair(MarketOrderPair request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getMarketOrderListByPair(request, responseObserver) - ); - } - - @Override - public void getMarketPairList(EmptyMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .getMarketPairList(request, responseObserver) - ); - } - - @Override - public void scanShieldedTRC20NotesByIvk(IvkDecryptTRC20Parameters request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .scanShieldedTRC20NotesByIvk(request, responseObserver) - ); - } - - @Override - public void scanShieldedTRC20NotesByOvk(OvkDecryptTRC20Parameters request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .scanShieldedTRC20NotesByOvk(request, responseObserver) - ); - } - - @Override - public void isShieldedTRC20ContractNoteSpent(NfTRC20Parameters request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi() - .isShieldedTRC20ContractNoteSpent(request, responseObserver) - ); - } - - @Override - public void getBurnTrx(EmptyMessage request, StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getBurnTrx(request, responseObserver) - ); - } - - @Override - public void getBlock(GrpcAPI.BlockReq request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getBlock(request, responseObserver)); - } - - @Override - public void getBandwidthPrices(EmptyMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getBandwidthPrices(request, responseObserver)); - } - - @Override - public void getEnergyPrices(EmptyMessage request, - StreamObserver responseObserver) { - walletOnPBFT.futureGet( - () -> rpcApiService.getWalletSolidityApi().getEnergyPrices(request, responseObserver)); - } - - } } From 99892dfed0ee8f8766509877a5daf9d6dfc4571c Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 18 Aug 2026 17:39:38 +0800 Subject: [PATCH 05/16] test(rpc): assert wallet-solidity grpc methods are a subset of wallet --- .../WalletSolidityApiMethodSubsetTest.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 framework/src/test/java/org/tron/core/services/WalletSolidityApiMethodSubsetTest.java diff --git a/framework/src/test/java/org/tron/core/services/WalletSolidityApiMethodSubsetTest.java b/framework/src/test/java/org/tron/core/services/WalletSolidityApiMethodSubsetTest.java new file mode 100644 index 00000000000..10a22f683ee --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/WalletSolidityApiMethodSubsetTest.java @@ -0,0 +1,47 @@ +package org.tron.core.services; + +import io.grpc.stub.StreamObserver; +import java.util.Arrays; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; +import org.junit.Assert; +import org.junit.Test; + +/** + * Guards the precondition for the future WalletApi / WalletSolidityApi dedup (clean-rpc.md §10). + * + *

{@code WalletSolidityApi} (serving {@code protocol.WalletSolidity}) and {@code WalletApi} + * (serving {@code protocol.Wallet}) implement the same read methods twice as byte-identical glue + * over the shared {@code Wallet} object. Deduping means replacing the WalletSolidity bodies with a + * one-line delegation, which is only sound while every WalletSolidity gRPC method also exists on + * WalletApi. This test pins that subset relationship so the invariant cannot silently break. + */ +public class WalletSolidityApiMethodSubsetTest { + + /** Signatures of the gRPC unary handlers a service impl overrides: {@code name(requestType)}. */ + private static Set grpcHandlerSignatures(Class impl) { + return Arrays.stream(impl.getDeclaredMethods()) + .filter(m -> m.getReturnType() == void.class) + .filter(m -> m.getParameterCount() == 2) + .filter(m -> m.getParameterTypes()[1] == StreamObserver.class) + .map(m -> m.getName() + "(" + m.getParameterTypes()[0].getName() + ")") + .collect(Collectors.toCollection(TreeSet::new)); + } + + @Test + public void testWalletSolidityApiMethodsAllExistOnWalletApi() throws ClassNotFoundException { + Class walletApi = Class.forName("org.tron.core.services.RpcApiService$WalletApi"); + Class walletSolidityApi = + Class.forName("org.tron.core.services.RpcApiService$WalletSolidityApi"); + + Set onWalletApi = grpcHandlerSignatures(walletApi); + Set solidityOnly = new TreeSet<>(grpcHandlerSignatures(walletSolidityApi)); + solidityOnly.removeAll(onWalletApi); + + Assert.assertTrue( + "these WalletSolidityApi methods have no WalletApi counterpart and cannot be dedup'd by " + + "delegating to a shared Wallet handler (see clean-rpc.md §10): " + solidityOnly, + solidityOnly.isEmpty()); + } +} From be46efc5c7335413cacdcd54f3a6d44877a7f1ce Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 18 Aug 2026 17:53:27 +0800 Subject: [PATCH 06/16] docs(rpc): note wallet-solidity api is the read-only subset of wallet --- .../java/org/tron/core/services/RpcApiService.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/RpcApiService.java b/framework/src/main/java/org/tron/core/services/RpcApiService.java index b9cb05a3b14..5f5dd5414a1 100755 --- a/framework/src/main/java/org/tron/core/services/RpcApiService.java +++ b/framework/src/main/java/org/tron/core/services/RpcApiService.java @@ -181,6 +181,8 @@ public class RpcApiService extends RpcService { private MetricsApiService metricsApiService; @Getter private DatabaseApi databaseApi = new DatabaseApi(); + // WalletApi is the full protocol.Wallet impl (HEAD); WalletSolidityApi is its read-only subset, + // reused by the Solidity/PBFT cursor ports and pinned by WalletSolidityApiMethodSubsetTest. private WalletApi walletApi = new WalletApi(); @Getter private WalletSolidityApi walletSolidityApi = new WalletSolidityApi(); @@ -362,7 +364,9 @@ public void getDynamicProperties(EmptyMessage request, } /** - * WalletSolidityApi. + * WalletSolidityApi is the full implementation of the {@code protocol.WalletSolidity} gRPC + * service. Every method here is read-only and also present on {@link WalletApi}: this is a + * read-only subset of {@code WalletApi}, guarded by {@code WalletSolidityApiMethodSubsetTest}. */ public class WalletSolidityApi extends WalletSolidityImplBase { @@ -953,7 +957,9 @@ private TransactionListExtention transactionList2Extention(TransactionList trans } /** - * WalletApi. + * WalletApi is the full implementation of the {@code protocol.Wallet} gRPC service, including + * write and build endpoints. {@link WalletSolidityApi} is the read-only subset of this surface, + * pinned by {@code WalletSolidityApiMethodSubsetTest}. */ public class WalletApi extends WalletImplBase { From d1de2dc463d5f5e969427dfdb4229b4887d5c4c1 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Fri, 21 Aug 2026 16:43:33 +0800 Subject: [PATCH 07/16] reduce duplicate code --- .../org/tron/core/services/RpcApiService.java | 197 +++--------------- 1 file changed, 24 insertions(+), 173 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/RpcApiService.java b/framework/src/main/java/org/tron/core/services/RpcApiService.java index 5f5dd5414a1..af796d5370e 100755 --- a/framework/src/main/java/org/tron/core/services/RpcApiService.java +++ b/framework/src/main/java/org/tron/core/services/RpcApiService.java @@ -396,34 +396,25 @@ public void getAccountById(Account request, StreamObserver responseObse @Override public void listWitnesses(EmptyMessage request, StreamObserver responseObserver) { - responseObserver.onNext(wallet.getWitnessList()); - responseObserver.onCompleted(); + walletApi.listWitnesses(request, responseObserver); } @Override public void getPaginatedNowWitnessList(PaginatedMessage request, StreamObserver responseObserver) { - try { - responseObserver.onNext( - wallet.getPaginatedNowWitnessList(request.getOffset(), request.getLimit())); - } catch (MaintenanceUnavailableException e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.getPaginatedNowWitnessList(request, responseObserver); } @Override public void getAssetIssueList(EmptyMessage request, StreamObserver responseObserver) { - responseObserver.onNext(wallet.getAssetIssueList()); - responseObserver.onCompleted(); + walletApi.getAssetIssueList(request, responseObserver); } @Override public void getPaginatedAssetIssueList(PaginatedMessage request, StreamObserver responseObserver) { - responseObserver.onNext(wallet.getAssetIssueList(request.getOffset(), request.getLimit())); - responseObserver.onCompleted(); + walletApi.getPaginatedAssetIssueList(request, responseObserver); } @Override @@ -446,33 +437,18 @@ public void getAssetIssueByName(BytesMessage request, @Override public void getAssetIssueListByName(BytesMessage request, StreamObserver responseObserver) { - ByteString assetName = request.getValue(); - - if (assetName != null) { - responseObserver.onNext(wallet.getAssetIssueListByName(assetName)); - } else { - responseObserver.onNext(null); - } - responseObserver.onCompleted(); + walletApi.getAssetIssueListByName(request, responseObserver); } @Override public void getAssetIssueById(BytesMessage request, StreamObserver responseObserver) { - ByteString assetId = request.getValue(); - - if (assetId != null) { - responseObserver.onNext(wallet.getAssetIssueById(assetId.toStringUtf8())); - } else { - responseObserver.onNext(null); - } - responseObserver.onCompleted(); + walletApi.getAssetIssueById(request, responseObserver); } @Override public void getNowBlock(EmptyMessage request, StreamObserver responseObserver) { - responseObserver.onNext(wallet.getNowBlock()); - responseObserver.onCompleted(); + walletApi.getNowBlock(request, responseObserver); } @Override @@ -511,46 +487,25 @@ public void getBlockByNum2(NumberMessage request, @Override public void getDelegatedResource(DelegatedResourceMessage request, StreamObserver responseObserver) { - responseObserver - .onNext(wallet.getDelegatedResource(request.getFromAddress(), request.getToAddress())); - responseObserver.onCompleted(); + walletApi.getDelegatedResource(request, responseObserver); } @Override public void getDelegatedResourceV2(DelegatedResourceMessage request, StreamObserver responseObserver) { - try { - responseObserver.onNext(wallet.getDelegatedResourceV2( - request.getFromAddress(), request.getToAddress()) - ); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.getDelegatedResourceV2(request, responseObserver); } @Override public void getDelegatedResourceAccountIndex(BytesMessage request, StreamObserver responseObserver) { - try { - responseObserver - .onNext(wallet.getDelegatedResourceAccountIndex(request.getValue())); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.getDelegatedResourceAccountIndex(request, responseObserver); } @Override public void getDelegatedResourceAccountIndexV2(BytesMessage request, StreamObserver responseObserver) { - try { - responseObserver - .onNext(wallet.getDelegatedResourceAccountIndexV2(request.getValue())); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.getDelegatedResourceAccountIndexV2(request, responseObserver); } @Override @@ -568,13 +523,7 @@ public void getCanDelegatedMaxSize(GrpcAPI.CanDelegatedMaxSizeRequestMessage req @Override public void getAvailableUnfreezeCount(GrpcAPI.GetAvailableUnfreezeCountRequestMessage request, StreamObserver responseObserver) { - try { - responseObserver.onNext(wallet.getAvailableUnfreezeCount( - request.getOwnerAddress())); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.getAvailableUnfreezeCount(request, responseObserver); } @Override @@ -594,21 +543,13 @@ public void getCanWithdrawUnfreezeAmount(CanWithdrawUnfreezeAmountRequestMessage @Override public void getExchangeById(BytesMessage request, StreamObserver responseObserver) { - ByteString exchangeId = request.getValue(); - - if (Objects.nonNull(exchangeId)) { - responseObserver.onNext(wallet.getExchangeById(exchangeId)); - } else { - responseObserver.onNext(null); - } - responseObserver.onCompleted(); + walletApi.getExchangeById(request, responseObserver); } @Override public void listExchanges(EmptyMessage request, StreamObserver responseObserver) { - responseObserver.onNext(wallet.getExchangeList()); - responseObserver.onCompleted(); + walletApi.listExchanges(request, responseObserver); } @Override @@ -634,15 +575,7 @@ public void getTransactionById(BytesMessage request, @Override public void getTransactionInfoById(BytesMessage request, StreamObserver responseObserver) { - ByteString id = request.getValue(); - if (null != id) { - TransactionInfo reply = wallet.getTransactionInfoById(id); - - responseObserver.onNext(reply); - } else { - responseObserver.onNext(null); - } - responseObserver.onCompleted(); + walletApi.getTransactionInfoById(request, responseObserver); } @Override @@ -792,71 +725,31 @@ public void isShieldedTRC20ContractNoteSpent(NfTRC20Parameters request, @Override public void getMarketOrderByAccount(BytesMessage request, StreamObserver responseObserver) { - try { - ByteString address = request.getValue(); - - MarketOrderList marketOrderList = wallet - .getMarketOrderByAccount(address); - responseObserver.onNext(marketOrderList); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.getMarketOrderByAccount(request, responseObserver); } @Override public void getMarketOrderById(BytesMessage request, StreamObserver responseObserver) { - try { - ByteString address = request.getValue(); - - MarketOrder marketOrder = wallet - .getMarketOrderById(address); - responseObserver.onNext(marketOrder); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.getMarketOrderById(request, responseObserver); } @Override public void getMarketPriceByPair(MarketOrderPair request, StreamObserver responseObserver) { - try { - MarketPriceList marketPriceList = wallet - .getMarketPriceByPair(request.getSellTokenId().toByteArray(), - request.getBuyTokenId().toByteArray()); - responseObserver.onNext(marketPriceList); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.getMarketPriceByPair(request, responseObserver); } @Override public void getMarketOrderListByPair(org.tron.protos.Protocol.MarketOrderPair request, StreamObserver responseObserver) { - try { - MarketOrderList orderPairList = wallet - .getMarketOrderListByPair(request.getSellTokenId().toByteArray(), - request.getBuyTokenId().toByteArray()); - responseObserver.onNext(orderPairList); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.getMarketOrderListByPair(request, responseObserver); } @Override public void getMarketPairList(EmptyMessage request, StreamObserver responseObserver) { - try { - MarketOrderPairList pairList = wallet.getMarketPairList(); - responseObserver.onNext(pairList); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.getMarketPairList(request, responseObserver); } @Override @@ -869,45 +762,13 @@ public void triggerConstantContract(TriggerSmartContract request, @Override public void estimateEnergy(TriggerSmartContract request, StreamObserver responseObserver) { - TransactionExtention.Builder trxExtBuilder = TransactionExtention.newBuilder(); - Return.Builder retBuilder = Return.newBuilder(); - EstimateEnergyMessage.Builder estimateBuilder - = EstimateEnergyMessage.newBuilder(); - - try { - TransactionCapsule trxCap = createTransactionCapsule(request, - ContractType.TriggerSmartContract); - wallet.estimateEnergy(request, trxCap, trxExtBuilder, retBuilder, estimateBuilder); - } catch (ContractValidateException | VMIllegalException e) { - retBuilder.setResult(false).setCode(response_code.CONTRACT_VALIDATE_ERROR) - .setMessage(ByteString.copyFromUtf8(Wallet - .CONTRACT_VALIDATE_ERROR + e.getMessage())); - logger.warn(CONTRACT_VALIDATE_EXCEPTION, e.getMessage()); - } catch (RuntimeException e) { - retBuilder.setResult(false).setCode(response_code.CONTRACT_EXE_ERROR) - .setMessage(ByteString.copyFromUtf8(e.getClass() + " : " + e.getMessage())); - logger.warn("When run estimate energy in VM, have Runtime Exception: " + e.getMessage()); - } catch (Exception e) { - retBuilder.setResult(false).setCode(response_code.OTHER_ERROR) - .setMessage(ByteString.copyFromUtf8(e.getClass() + " : " + e.getMessage())); - logger.warn(UNKNOWN_EXCEPTION_CAUGHT + e.getMessage(), e); - } finally { - estimateBuilder.setResult(retBuilder); - responseObserver.onNext(estimateBuilder.build()); - responseObserver.onCompleted(); - } + walletApi.estimateEnergy(request, responseObserver); } @Override public void getTransactionInfoByBlockNum(NumberMessage request, StreamObserver responseObserver) { - try { - responseObserver.onNext(wallet.getTransactionInfoByBlockNum(request.getNum())); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - - responseObserver.onCompleted(); + walletApi.getTransactionInfoByBlockNum(request, responseObserver); } @Override @@ -919,23 +780,13 @@ public void getBlock(GrpcAPI.BlockReq request, @Override public void getBandwidthPrices(EmptyMessage request, StreamObserver responseObserver) { - try { - responseObserver.onNext(wallet.getBandwidthPrices()); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.getBandwidthPrices(request, responseObserver); } @Override public void getEnergyPrices(EmptyMessage request, StreamObserver responseObserver) { - try { - responseObserver.onNext(wallet.getEnergyPrices()); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.getEnergyPrices(request, responseObserver); } } From 102aa19216540aab2440657a36f518bb36394bb6 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Fri, 21 Aug 2026 17:16:05 +0800 Subject: [PATCH 08/16] refactor(rpc): dedup remaining wallet-solidity read handlers Delegate 17 more WalletSolidityApi read methods to the WalletApi singleton so each handler body lives once. 41/47 methods now delegate; the other 6 already share outer *Common helpers. Behavior notes: - getBlockByNum/getBlockByNum2: WalletApi adopts the guarded version, so the FullNode HEAD port now returns null for negative num. - getMerkleTreeVoucherInfo, isSpend, scanNoteByIvk/ByOvk, scanAndMarkNoteByIvk and the 3 shielded-TRC20 methods: solidity error paths now match WalletApi (return after onError; the two scanShieldedTRC20Notes* also add error logging). Client-observable behavior unchanged. - getAssetIssueByName: solidity debug log label Solidity -> FullNode. Still TODO: build verification and tests for the HEAD getBlockByNum guard change. --- .../org/tron/core/services/RpcApiService.java | 195 +++--------------- 1 file changed, 32 insertions(+), 163 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/RpcApiService.java b/framework/src/main/java/org/tron/core/services/RpcApiService.java index af796d5370e..44e6b711028 100755 --- a/framework/src/main/java/org/tron/core/services/RpcApiService.java +++ b/framework/src/main/java/org/tron/core/services/RpcApiService.java @@ -372,26 +372,12 @@ public class WalletSolidityApi extends WalletSolidityImplBase { @Override public void getAccount(Account request, StreamObserver responseObserver) { - ByteString addressBs = request.getAddress(); - if (addressBs != null) { - Account reply = wallet.getAccount(request); - responseObserver.onNext(reply); - } else { - responseObserver.onNext(null); - } - responseObserver.onCompleted(); + walletApi.getAccount(request, responseObserver); } @Override public void getAccountById(Account request, StreamObserver responseObserver) { - ByteString id = request.getAccountId(); - if (id != null) { - Account reply = wallet.getAccountById(request); - responseObserver.onNext(reply); - } else { - responseObserver.onNext(null); - } - responseObserver.onCompleted(); + walletApi.getAccountById(request, responseObserver); } @Override @@ -420,18 +406,7 @@ public void getPaginatedAssetIssueList(PaginatedMessage request, @Override public void getAssetIssueByName(BytesMessage request, StreamObserver responseObserver) { - ByteString assetName = request.getValue(); - if (assetName != null) { - try { - responseObserver.onNext(wallet.getAssetIssueByName(assetName)); - } catch (NonUniqueObjectException e) { - responseObserver.onNext(null); - logger.debug("Solidity NonUniqueObjectException: {}", e.getMessage()); - } - } else { - responseObserver.onNext(null); - } - responseObserver.onCompleted(); + walletApi.getAssetIssueByName(request, responseObserver); } @Override @@ -454,33 +429,18 @@ public void getNowBlock(EmptyMessage request, StreamObserver responseObse @Override public void getNowBlock2(EmptyMessage request, StreamObserver responseObserver) { - responseObserver.onNext(block2Extention(wallet.getNowBlock())); - responseObserver.onCompleted(); + walletApi.getNowBlock2(request, responseObserver); } @Override public void getBlockByNum(NumberMessage request, StreamObserver responseObserver) { - long num = request.getNum(); - if (num >= 0) { - Block reply = wallet.getBlockByNum(num); - responseObserver.onNext(reply); - } else { - responseObserver.onNext(null); - } - responseObserver.onCompleted(); + walletApi.getBlockByNum(request, responseObserver); } @Override public void getBlockByNum2(NumberMessage request, StreamObserver responseObserver) { - long num = request.getNum(); - if (num >= 0) { - Block reply = wallet.getBlockByNum(num); - responseObserver.onNext(block2Extention(reply)); - } else { - responseObserver.onNext(null); - } - responseObserver.onCompleted(); + walletApi.getBlockByNum2(request, responseObserver); } @@ -511,13 +471,7 @@ public void getDelegatedResourceAccountIndexV2(BytesMessage request, @Override public void getCanDelegatedMaxSize(GrpcAPI.CanDelegatedMaxSizeRequestMessage request, StreamObserver responseObserver) { - try { - responseObserver.onNext(wallet.getCanDelegatedMaxSize( - request.getOwnerAddress(),request.getType())); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.getCanDelegatedMaxSize(request, responseObserver); } @Override @@ -529,15 +483,7 @@ public void getAvailableUnfreezeCount(GrpcAPI.GetAvailableUnfreezeCountRequestMe @Override public void getCanWithdrawUnfreezeAmount(CanWithdrawUnfreezeAmountRequestMessage request, StreamObserver responseObserver) { - try { - responseObserver - .onNext(wallet.getCanWithdrawUnfreezeAmount( - request.getOwnerAddress(), request.getTimestamp()) - ); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.getCanWithdrawUnfreezeAmount(request, responseObserver); } @Override @@ -561,15 +507,7 @@ public void getTransactionCountByBlockNum(NumberMessage request, @Override public void getTransactionById(BytesMessage request, StreamObserver responseObserver) { - ByteString id = request.getValue(); - if (null != id) { - Transaction reply = wallet.getTransactionById(id); - - responseObserver.onNext(reply); - } else { - responseObserver.onNext(null); - } - responseObserver.onCompleted(); + walletApi.getTransactionById(request, responseObserver); } @Override @@ -598,128 +536,48 @@ public void getBurnTrx(EmptyMessage request, StreamObserver respo @Override public void getMerkleTreeVoucherInfo(OutputPointInfo request, StreamObserver responseObserver) { - - try { - IncrementalMerkleVoucherInfo witnessInfo = wallet - .getMerkleTreeVoucherInfo(request); - responseObserver.onNext(witnessInfo); - } catch (Exception ex) { - responseObserver.onError(getRunTimeException(ex)); - } - responseObserver.onCompleted(); + walletApi.getMerkleTreeVoucherInfo(request, responseObserver); } @Override public void scanNoteByIvk(GrpcAPI.IvkDecryptParameters request, StreamObserver responseObserver) { - long startNum = request.getStartBlockIndex(); - long endNum = request.getEndBlockIndex(); - - try { - DecryptNotes decryptNotes = wallet - .scanNoteByIvk(startNum, endNum, request.getIvk().toByteArray()); - responseObserver.onNext(decryptNotes); - } catch (BadItemException | ZksnarkException e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.scanNoteByIvk(request, responseObserver); } @Override public void scanAndMarkNoteByIvk(GrpcAPI.IvkDecryptAndMarkParameters request, StreamObserver responseObserver) { - long startNum = request.getStartBlockIndex(); - long endNum = request.getEndBlockIndex(); - - try { - DecryptNotesMarked decryptNotes = wallet.scanAndMarkNoteByIvk(startNum, endNum, - request.getIvk().toByteArray(), - request.getAk().toByteArray(), - request.getNk().toByteArray()); - responseObserver.onNext(decryptNotes); - } catch (BadItemException | ZksnarkException | InvalidProtocolBufferException - | ItemNotFoundException e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.scanAndMarkNoteByIvk(request, responseObserver); } @Override public void scanNoteByOvk(GrpcAPI.OvkDecryptParameters request, StreamObserver responseObserver) { - long startNum = request.getStartBlockIndex(); - long endNum = request.getEndBlockIndex(); - try { - DecryptNotes decryptNotes = wallet - .scanNoteByOvk(startNum, endNum, request.getOvk().toByteArray()); - responseObserver.onNext(decryptNotes); - } catch (BadItemException | ZksnarkException e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.scanNoteByOvk(request, responseObserver); } @Override public void isSpend(NoteParameters request, StreamObserver responseObserver) { - try { - responseObserver.onNext(wallet.isSpend(request)); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.isSpend(request, responseObserver); } @Override public void scanShieldedTRC20NotesByIvk(IvkDecryptTRC20Parameters request, StreamObserver responseObserver) { - if (rejectIfEventsPresent(responseObserver, request.getEventsList())) { - return; - } - long startNum = request.getStartBlockIndex(); - long endNum = request.getEndBlockIndex(); - byte[] contractAddress = request.getShieldedTRC20ContractAddress().toByteArray(); - byte[] ivk = request.getIvk().toByteArray(); - byte[] ak = request.getAk().toByteArray(); - byte[] nk = request.getNk().toByteArray(); - - try { - responseObserver.onNext( - wallet.scanShieldedTRC20NotesByIvk(startNum, endNum, contractAddress, ivk, ak, nk)); - - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.scanShieldedTRC20NotesByIvk(request, responseObserver); } @Override public void scanShieldedTRC20NotesByOvk(OvkDecryptTRC20Parameters request, StreamObserver responseObserver) { - if (rejectIfEventsPresent(responseObserver, request.getEventsList())) { - return; - } - long startNum = request.getStartBlockIndex(); - long endNum = request.getEndBlockIndex(); - byte[] contractAddress = request.getShieldedTRC20ContractAddress().toByteArray(); - byte[] ovk = request.getOvk().toByteArray(); - try { - responseObserver - .onNext(wallet.scanShieldedTRC20NotesByOvk(startNum, endNum, ovk, contractAddress)); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.scanShieldedTRC20NotesByOvk(request, responseObserver); } @Override public void isShieldedTRC20ContractNoteSpent(NfTRC20Parameters request, StreamObserver responseObserver) { - try { - responseObserver.onNext(wallet.isShieldedTRC20ContractNoteSpent(request)); - } catch (Exception e) { - responseObserver.onError(getRunTimeException(e)); - } - responseObserver.onCompleted(); + walletApi.isShieldedTRC20ContractNoteSpent(request, responseObserver); } @Override @@ -1336,15 +1194,26 @@ public void getNowBlock2(EmptyMessage request, @Override public void getBlockByNum(NumberMessage request, StreamObserver responseObserver) { - responseObserver.onNext(wallet.getBlockByNum(request.getNum())); + long num = request.getNum(); + if (num >= 0) { + Block reply = wallet.getBlockByNum(num); + responseObserver.onNext(reply); + } else { + responseObserver.onNext(null); + } responseObserver.onCompleted(); } @Override public void getBlockByNum2(NumberMessage request, StreamObserver responseObserver) { - Block block = wallet.getBlockByNum(request.getNum()); - responseObserver.onNext(block2Extention(block)); + long num = request.getNum(); + if (num >= 0) { + Block reply = wallet.getBlockByNum(num); + responseObserver.onNext(block2Extention(reply)); + } else { + responseObserver.onNext(null); + } responseObserver.onCompleted(); } @@ -1451,7 +1320,7 @@ public void getAssetIssueByName(BytesMessage request, responseObserver.onNext(wallet.getAssetIssueByName(assetName)); } catch (NonUniqueObjectException e) { responseObserver.onNext(null); - logger.debug("FullNode NonUniqueObjectException: {}", e.getMessage()); + logger.debug("NonUniqueObjectException: {}", e.getMessage()); } } else { responseObserver.onNext(null); From 32ca3bf880b139489e60dcf92eb728b3cd53b358 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 25 Aug 2026 17:47:09 +0800 Subject: [PATCH 09/16] fix test --- .../filter/CursorServerInterceptor.java | 26 ++-- .../filter/GrpcInterceptorProbeTest.java | 117 +++++++++++++++++- 2 files changed, 132 insertions(+), 11 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java b/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java index 81b54ad2b57..3702559c65f 100644 --- a/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java +++ b/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java @@ -21,16 +21,24 @@ *

Two invariants this class depends on

* *

The bracket must wrap {@code Listener.onHalfClose()}, not the body of - * {@code interceptCall}. {@code interceptCall} runs when the call arrives and may execute on an - * IO thread, while the handler that reads the database runs in {@code onHalfClose()} on an executor - * thread. The cursor is a {@link ThreadLocal}: setting it on the IO thread has no effect on the - * executor thread, and nothing throws — the port silently serves HEAD data instead. {@code - * GrpcInterceptorProbeTest} asserts this thread affinity; re-run it before changing this class. + * {@code interceptCall}. gRPC delivers a call's listener callbacks as separate tasks through a + * per-call {@code SerializingExecutor} over the server's application executor. They are serialized + * with respect to each other, but they are not pinned to one thread, so + * {@code interceptCall} and {@code onHalfClose} routinely run on different threads of the same + * pool — {@code GrpcInterceptorProbeTest} shows exactly that. The cursor is a {@link ThreadLocal}, + * so a cursor set in {@code interceptCall} reaches the read path only by luck, and always reaches + * it when the pool holds a single thread, which is why the mistake does not reproduce on a small + * machine. The handler, by contrast, is invoked inline by gRPC's unary listener from + * {@code onHalfClose()}, so it always shares that thread. Setting the cursor anywhere else fails + * silently: nothing throws and the port serves HEAD data. * - *

All handlers must be synchronous unary. The cursor is reset as soon as - * {@code onHalfClose} returns, which requires handlers to complete {@code onNext/onCompleted} - * inline. A handler that defers its database reads to another thread or an asynchronous callback - * would read after the reset and observe HEAD data. + *

Every handler that reads the database must be synchronous. The cursor is reset as soon + * as {@code onHalfClose} returns, which requires such handlers to complete {@code onNext} / + * {@code onCompleted} inline. A handler that defers its database reads to another thread or an + * asynchronous callback would read after the reset and observe HEAD data. Every method of the + * services mounted on the cursor ports is a synchronous unary handler. The one streaming service + * these servers also carry, {@code ProtoReflectionService}, is bracketed too but reads no chain + * state, so the reset racing its responses is harmless. * *

The {@code finally} block is mandatory: gRPC serves calls from a fixed thread pool, so a * cursor left behind would leak into the next call handled by the same thread. diff --git a/framework/src/test/java/org/tron/core/services/filter/GrpcInterceptorProbeTest.java b/framework/src/test/java/org/tron/core/services/filter/GrpcInterceptorProbeTest.java index d88d6576a03..798c26dca86 100644 --- a/framework/src/test/java/org/tron/core/services/filter/GrpcInterceptorProbeTest.java +++ b/framework/src/test/java/org/tron/core/services/filter/GrpcInterceptorProbeTest.java @@ -1,5 +1,10 @@ package org.tron.core.services.filter; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; @@ -10,6 +15,7 @@ import io.grpc.ServerCallHandler; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; +import java.lang.reflect.Field; import java.net.ServerSocket; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -23,6 +29,8 @@ import org.tron.api.DatabaseGrpc; import org.tron.api.DatabaseGrpc.DatabaseImplBase; import org.tron.api.GrpcAPI.EmptyMessage; +import org.tron.core.db.Manager; +import org.tron.core.db2.core.Chainbase; import org.tron.protos.Protocol.Block; /** @@ -143,10 +151,9 @@ public void testInterceptorOrdering() { Assert.assertTrue("no onHalfClose captured for A", aIn >= 0); Assert.assertTrue("no onHalfClose captured for B", bIn >= 0); - boolean lastRegisteredIsInnermost = bIn > aIn; System.out.println("\n===== ordering ====="); System.out.println("registered: intercept(A) then intercept(B)"); - System.out.println(lastRegisteredIsInnermost + System.out.println(bIn > aIn ? "B (registered last) is innermost -> register the cursor interceptor LAST" : "A (registered first) is innermost -> register the cursor interceptor FIRST"); System.out.println("====================\n"); @@ -154,6 +161,15 @@ public void testInterceptorOrdering() { int handlerIdx = indexOf("[HANDLER]", "execute"); Assert.assertTrue("handler did not run inside both interceptors", handlerIdx > aIn && handlerIdx > bIn); + + // The innermost interceptor enters onHalfClose last. RpcApiServiceOnSolidity / + // RpcApiServiceOnPBFT register their cursor interceptor BEFORE super.addInterceptor(...) + // precisely because the first-registered one ends up innermost, wrapping the handler alone. + // Asserted, not merely printed: if a gRPC upgrade flips this, those two services silently + // start bracketing the other interceptors instead of the handler. + Assert.assertTrue( + "first-registered interceptor must be innermost; the cursor services depend on it", + aIn > bIn); } /** @@ -239,6 +255,103 @@ public void onHalfClose() { } } + /** + * The production interceptor itself, not a stand-in: the cursor must be set on the thread that + * runs the handler and must be restored before the call ends, even when the handler throws. + */ + @Test + public void testProductionInterceptorSetsCursorOnTheHandlerThread() throws Exception { + final List setOn = new CopyOnWriteArrayList<>(); + final List resetOn = new CopyOnWriteArrayList<>(); + final String[] handlerOn = new String[1]; + + Manager manager = mock(Manager.class); + doAnswer(inv -> setOn.add(Thread.currentThread().getName())) + .when(manager).setCursor(any(Chainbase.Cursor.class)); + doAnswer(inv -> resetOn.add(Thread.currentThread().getName())) + .when(manager).resetCursor(); + + SolidityCursorInterceptor interceptor = new SolidityCursorInterceptor(); + Field dbManager = CursorServerInterceptor.class.getDeclaredField("dbManager"); + dbManager.setAccessible(true); + dbManager.set(interceptor, manager); + + int port = freePort(); + Server s = ServerBuilder.forPort(port) + .executor(executor) + .addService(new DatabaseImplBase() { + @Override + public void getNowBlock(EmptyMessage req, StreamObserver obs) { + handlerOn[0] = Thread.currentThread().getName(); + obs.onNext(Block.getDefaultInstance()); + obs.onCompleted(); + } + }) + .intercept(interceptor) + .build() + .start(); + + ManagedChannel ch = ManagedChannelBuilder.forAddress("127.0.0.1", port) + .usePlaintext().directExecutor().build(); + try { + DatabaseGrpc.newBlockingStub(ch).getNowBlock(EmptyMessage.getDefaultInstance()); + + Assert.assertEquals("cursor must be set exactly once per call", 1, setOn.size()); + Assert.assertEquals("cursor must be restored exactly once per call", 1, resetOn.size()); + Assert.assertNotNull("handler did not run", handlerOn[0]); + // the ThreadLocal cursor only reaches the read path if it is set on the handler's thread + Assert.assertEquals("cursor was set on a thread other than the handler's", + handlerOn[0], setOn.get(0)); + Assert.assertEquals("cursor was restored on a thread other than the handler's", + handlerOn[0], resetOn.get(0)); + verify(manager).setCursor(Chainbase.Cursor.SOLIDITY); + } finally { + ch.shutdownNow(); + s.shutdownNow(); + s.awaitTermination(5, TimeUnit.SECONDS); + } + } + + /** A handler that throws must still leave the cursor restored, or it leaks into the next call. */ + @Test + public void testProductionInterceptorRestoresCursorWhenHandlerThrows() throws Exception { + Manager manager = mock(Manager.class); + SolidityCursorInterceptor interceptor = new SolidityCursorInterceptor(); + Field dbManager = CursorServerInterceptor.class.getDeclaredField("dbManager"); + dbManager.setAccessible(true); + dbManager.set(interceptor, manager); + + int port = freePort(); + Server s = ServerBuilder.forPort(port) + .executor(executor) + .addService(new DatabaseImplBase() { + @Override + public void getNowBlock(EmptyMessage req, StreamObserver obs) { + throw new IllegalStateException("boom"); + } + }) + .intercept(interceptor) + .build() + .start(); + + ManagedChannel ch = ManagedChannelBuilder.forAddress("127.0.0.1", port) + .usePlaintext().directExecutor().build(); + try { + try { + DatabaseGrpc.newBlockingStub(ch).getNowBlock(EmptyMessage.getDefaultInstance()); + Assert.fail("expected the handler failure to surface"); + } catch (RuntimeException expected) { + // the call fails; what matters is the cursor below + } + verify(manager).setCursor(Chainbase.Cursor.SOLIDITY); + verify(manager).resetCursor(); + } finally { + ch.shutdownNow(); + s.shutdownNow(); + s.awaitTermination(5, TimeUnit.SECONDS); + } + } + private static int freePort() throws Exception { try (ServerSocket socket = new ServerSocket(0)) { return socket.getLocalPort(); From 86fec322480a5b9137a4e45c0e18c2ad7f95b824 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 25 Aug 2026 17:56:01 +0800 Subject: [PATCH 10/16] fix comments --- .../filter/CursorServerInterceptor.java | 48 +++++++------------ 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java b/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java index 3702559c65f..bef059c1ed8 100644 --- a/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java +++ b/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java @@ -10,38 +10,24 @@ import org.tron.core.db2.core.Chainbase; /** - * Switches the read cursor of the current thread for the duration of a gRPC call, and restores it - * afterwards. Every call served by a server carrying this interceptor therefore reads from the - * snapshot the subclass selects. + * Switches the current thread's read cursor for the duration of a gRPC call and restores it + * afterwards, so every call served through this interceptor reads from the snapshot its subclass + * selects. The services behind it never touch the cursor, so one instance can serve HEAD, SOLIDITY + * and PBFT semantics on different servers. * - *

The service implementations behind it make no assumption about the cursor and never touch it; - * which snapshot a read resolves to is decided solely by the calling thread's cursor. A single - * service instance can thus serve HEAD, SOLIDITY and PBFT semantics on different servers. + *

Two invariants it relies on: + *

* - *

Two invariants this class depends on

- * - *

The bracket must wrap {@code Listener.onHalfClose()}, not the body of - * {@code interceptCall}. gRPC delivers a call's listener callbacks as separate tasks through a - * per-call {@code SerializingExecutor} over the server's application executor. They are serialized - * with respect to each other, but they are not pinned to one thread, so - * {@code interceptCall} and {@code onHalfClose} routinely run on different threads of the same - * pool — {@code GrpcInterceptorProbeTest} shows exactly that. The cursor is a {@link ThreadLocal}, - * so a cursor set in {@code interceptCall} reaches the read path only by luck, and always reaches - * it when the pool holds a single thread, which is why the mistake does not reproduce on a small - * machine. The handler, by contrast, is invoked inline by gRPC's unary listener from - * {@code onHalfClose()}, so it always shares that thread. Setting the cursor anywhere else fails - * silently: nothing throws and the port serves HEAD data. - * - *

Every handler that reads the database must be synchronous. The cursor is reset as soon - * as {@code onHalfClose} returns, which requires such handlers to complete {@code onNext} / - * {@code onCompleted} inline. A handler that defers its database reads to another thread or an - * asynchronous callback would read after the reset and observe HEAD data. Every method of the - * services mounted on the cursor ports is a synchronous unary handler. The one streaming service - * these servers also carry, {@code ProtoReflectionService}, is bracketed too but reads no chain - * state, so the reset racing its responses is harmless. - * - *

The {@code finally} block is mandatory: gRPC serves calls from a fixed thread pool, so a - * cursor left behind would leak into the next call handled by the same thread. + *

The {@code finally} reset is mandatory: gRPC reuses a fixed thread pool, so a leftover cursor + * would leak into the next call on the same thread. */ public abstract class CursorServerInterceptor implements ServerInterceptor { @@ -58,7 +44,7 @@ public ServerCall.Listener interceptCall( @Override public void onHalfClose() { try { - // For PBFT the offset is computed inside Manager#setCursor at call time. + // For SOLIDITY/PBFT the offset is computed inside Manager#setCursor at call time. dbManager.setCursor(cursor); super.onHalfClose(); } finally { From 224cfc61d5038bbbbb2b1cc5bdb17a40fa1c7648 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Wed, 26 Aug 2026 16:35:27 +0800 Subject: [PATCH 11/16] refactor(rpc): bind the cursor interceptor to the shared services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attach the cursor interceptor with ServerInterceptors.intercept on the two shared service definitions instead of serverBuilder.intercept on the port. The call order a read sees is unchanged either way — the cursor still runs innermost, immediately before the handler — but the reason it does becomes structural rather than an assumption about registration order, and the interceptor no longer reaches services it has no business bracketing. That matters on the PBFT port: switching the PBFT cursor reads the head and latest pbft block numbers, which a reflection call should not pay for. Also scope the cursor to the synchronous handler callback rather than to "the call" in the docs and javadoc: a call spans several listener callbacks and may span several threads, so call-lifetime scoping is not a safe model for a ThreadLocal. Note that the synchronous-handler property is an implementation invariant rather than a type-level guarantee, and correct the offset comment — only PBFT computes a head-to-pbft offset, SOLIDITY does not. Tests: CursorInterceptorScopeTest drives interceptCall on one thread and onHalfClose on another, so an implementation that scoped the cursor around interceptCall fails by construction instead of by scheduling luck. CursorInterceptorAttachmentTest pins both properties of the change above. --- .../filter/CursorServerInterceptor.java | 32 ++- .../interfaceOnPBFT/RpcApiServiceOnPBFT.java | 25 ++- .../RpcApiServiceOnSolidity.java | 23 ++- .../CursorInterceptorAttachmentTest.java | 195 ++++++++++++++++++ .../filter/CursorInterceptorScopeTest.java | 136 ++++++++++++ 5 files changed, 379 insertions(+), 32 deletions(-) create mode 100644 framework/src/test/java/org/tron/core/services/filter/CursorInterceptorAttachmentTest.java create mode 100644 framework/src/test/java/org/tron/core/services/filter/CursorInterceptorScopeTest.java diff --git a/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java b/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java index bef059c1ed8..6ea5d24f316 100644 --- a/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java +++ b/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java @@ -10,24 +10,35 @@ import org.tron.core.db2.core.Chainbase; /** - * Switches the current thread's read cursor for the duration of a gRPC call and restores it - * afterwards, so every call served through this interceptor reads from the snapshot its subclass - * selects. The services behind it never touch the cursor, so one instance can serve HEAD, SOLIDITY - * and PBFT semantics on different servers. + * Switches the current thread's read cursor for the duration of the synchronous handler callback + * and restores it afterwards, so a handler served through this interceptor reads from the snapshot + * its subclass selects. The services behind it never touch the cursor, so one instance can serve + * HEAD, SOLIDITY and PBFT semantics on different servers. + * + *

The scope is deliberately the handler callback, not the call: a call spans several listener + * callbacks and may span several threads, so call-lifetime scoping is not a safe model for a + * {@link ThreadLocal}. * *

Two invariants it relies on: *

* *

The {@code finally} reset is mandatory: gRPC reuses a fixed thread pool, so a leftover cursor * would leak into the next call on the same thread. + * + *

Attach this at the service level ({@code ServerInterceptors.intercept}) rather than the + * server level, so it stays inside the server-wide chain by construction instead of by registration + * order, and only brackets the services that actually read chain state. */ public abstract class CursorServerInterceptor implements ServerInterceptor { @@ -44,7 +55,8 @@ public ServerCall.Listener interceptCall( @Override public void onHalfClose() { try { - // For SOLIDITY/PBFT the offset is computed inside Manager#setCursor at call time. + // PBFT additionally needs a head-to-pbft offset, computed inside Manager#setCursor + // per call. dbManager.setCursor(cursor); super.onHalfClose(); } finally { diff --git a/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/RpcApiServiceOnPBFT.java b/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/RpcApiServiceOnPBFT.java index 98c82a2118a..70dcdcb3b23 100755 --- a/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/RpcApiServiceOnPBFT.java +++ b/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/RpcApiServiceOnPBFT.java @@ -1,5 +1,6 @@ package org.tron.core.services.interfaceOnPBFT; +import io.grpc.ServerInterceptors; import io.grpc.netty.NettyServerBuilder; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -23,19 +24,21 @@ public RpcApiServiceOnPBFT() { executorName = "rpc-pbft-executor"; } + /** + * Binds the PBFT cursor to the two shared services rather than to the server. A service-level + * interceptor lives inside the {@code ServerServiceDefinition}, so it always sits between the + * server-level chain and the handler regardless of registration order, and it reaches only these + * two services — the server-level chain (rate limiter, api access, lite-fullnode, prometheus) is + * left exactly as the base class builds it, and reflection is not bracketed. That last point + * matters here: switching to the PBFT cursor reads the head and latest-pbft block numbers, so it + * should not run for calls that never touch chain state. + */ @Override protected void addService(NettyServerBuilder serverBuilder) { - serverBuilder.addService(rpcApiService.getDatabaseApi()); - serverBuilder.addService(rpcApiService.getWalletSolidityApi()); - } - - @Override - protected void addInterceptor(NettyServerBuilder serverBuilder) { - // Registered first so it is innermost, wrapping the handler alone (in gRPC 1.83.0 the - // first-registered interceptor is closest to the handler, pinned by GrpcInterceptorProbeTest). - // It scopes the PBFT cursor to the data read. - serverBuilder.intercept(pbftCursorInterceptor); - super.addInterceptor(serverBuilder); + serverBuilder.addService( + ServerInterceptors.intercept(rpcApiService.getDatabaseApi(), pbftCursorInterceptor)); + serverBuilder.addService(ServerInterceptors.intercept( + rpcApiService.getWalletSolidityApi(), pbftCursorInterceptor)); } } diff --git a/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/RpcApiServiceOnSolidity.java b/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/RpcApiServiceOnSolidity.java index a65146a4e39..7f70f7ba01f 100755 --- a/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/RpcApiServiceOnSolidity.java +++ b/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/RpcApiServiceOnSolidity.java @@ -1,5 +1,6 @@ package org.tron.core.services.interfaceOnSolidity; +import io.grpc.ServerInterceptors; import io.grpc.netty.NettyServerBuilder; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -23,19 +24,19 @@ public RpcApiServiceOnSolidity() { executorName = "rpc-solidity-executor"; } + /** + * Binds the SOLIDITY cursor to the two shared services rather than to the server. A service-level + * interceptor lives inside the {@code ServerServiceDefinition}, so it always sits between the + * server-level chain and the handler regardless of registration order, and it reaches only these + * two services — the server-level chain (rate limiter, api access, lite-fullnode, prometheus) is + * left exactly as the base class builds it, and reflection is not bracketed. + */ @Override protected void addService(NettyServerBuilder serverBuilder) { - serverBuilder.addService(rpcApiService.getDatabaseApi()); - serverBuilder.addService(rpcApiService.getWalletSolidityApi()); - } - - @Override - protected void addInterceptor(NettyServerBuilder serverBuilder) { - // Registered first so it is innermost, wrapping the handler alone (in gRPC 1.83.0 the - // first-registered interceptor is closest to the handler, pinned by GrpcInterceptorProbeTest). - // It scopes the SOLIDITY cursor to the data read. - serverBuilder.intercept(solidityCursorInterceptor); - super.addInterceptor(serverBuilder); + serverBuilder.addService( + ServerInterceptors.intercept(rpcApiService.getDatabaseApi(), solidityCursorInterceptor)); + serverBuilder.addService(ServerInterceptors.intercept( + rpcApiService.getWalletSolidityApi(), solidityCursorInterceptor)); } } diff --git a/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorAttachmentTest.java b/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorAttachmentTest.java new file mode 100644 index 00000000000..d573a379a7c --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorAttachmentTest.java @@ -0,0 +1,195 @@ +package org.tron.core.services.filter; + +import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Metadata; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.ServerInterceptors; +import io.grpc.protobuf.services.ProtoReflectionService; +import io.grpc.reflection.v1alpha.ServerReflectionGrpc; +import io.grpc.reflection.v1alpha.ServerReflectionRequest; +import io.grpc.reflection.v1alpha.ServerReflectionResponse; +import io.grpc.stub.StreamObserver; +import java.net.ServerSocket; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.Assert; +import org.junit.Test; +import org.tron.api.DatabaseGrpc; +import org.tron.api.DatabaseGrpc.DatabaseImplBase; +import org.tron.api.GrpcAPI.EmptyMessage; +import org.tron.protos.Protocol.Block; + +/** + * Why the cursor interceptor is attached to the service definitions rather than to the server. + * + *

Both wirings put it innermost, so the call order a read sees is the same either way — the + * first test pins that, so the switch is provably behaviour-preserving. What differs is reach: a + * server-level interceptor brackets every service on the port, including the streaming reflection + * service, while a service-level one reaches only the services it is bound to. The second test + * pins that, which is the reason for the choice: switching the PBFT cursor reads chain state, and + * a call that never touches the database should not pay for it. + */ +public class CursorInterceptorAttachmentTest { + + private static final List TRACE = new CopyOnWriteArrayList<>(); + + /** Records when its listener runs, i.e. where it sits in the chain. */ + private static class OrderProbe implements ServerInterceptor { + + private final String tag; + + OrderProbe(String tag) { + this.tag = tag; + } + + @Override + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + return new SimpleForwardingServerCallListener(next.startCall(call, headers)) { + @Override + public void onHalfClose() { + TRACE.add(tag); + super.onHalfClose(); + } + }; + } + } + + /** Records at interceptCall, so it also registers for streaming calls. */ + private static class AttachProbe implements ServerInterceptor { + + @Override + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + TRACE.add("attached:" + call.getMethodDescriptor().getFullMethodName()); + return next.startCall(call, headers); + } + } + + private static class ProbeDatabaseApi extends DatabaseImplBase { + + @Override + public void getNowBlock(EmptyMessage request, StreamObserver observer) { + TRACE.add("handler"); + observer.onNext(Block.getDefaultInstance()); + observer.onCompleted(); + } + } + + @Test + public void testServiceLevelAttachmentKeepsTheSameCallOrder() throws Exception { + Assert.assertEquals("attaching the cursor per service must not reorder the chain", + callOrder(false), callOrder(true)); + // and it is innermost, immediately before the handler + List order = callOrder(true); + Assert.assertEquals("cursor must run last before the handler", + "cursor", order.get(order.size() - 2)); + Assert.assertEquals("handler", order.get(order.size() - 1)); + } + + @Test + public void testServiceLevelAttachmentLeavesReflectionAlone() throws Exception { + Assert.assertTrue("a server-level interceptor brackets reflection too", + reflectionIsBracketed(false)); + Assert.assertFalse("a service-level interceptor must not bracket reflection", + reflectionIsBracketed(true)); + } + + /** Drives one unary call and returns the observed chain order. */ + private List callOrder(boolean serviceLevel) throws Exception { + TRACE.clear(); + int port = freePort(); + ServerBuilder builder = ServerBuilder.forPort(port); + OrderProbe cursor = new OrderProbe("cursor"); + if (serviceLevel) { + builder.addService(ServerInterceptors.intercept(new ProbeDatabaseApi(), cursor)); + } else { + builder.addService(new ProbeDatabaseApi()).intercept(cursor); + } + // the server-level chain, registered exactly as RpcService#addInterceptor does + builder.intercept(new OrderProbe("rateLimiter")); + builder.intercept(new OrderProbe("apiAccess")); + builder.intercept(new OrderProbe("liteFnQuery")); + builder.intercept(new OrderProbe("prometheus")); + + Server server = builder.build().start(); + ManagedChannel channel = + ManagedChannelBuilder.forAddress("127.0.0.1", port).usePlaintext().build(); + try { + DatabaseGrpc.newBlockingStub(channel).getNowBlock(EmptyMessage.getDefaultInstance()); + } finally { + shutdown(channel, server); + } + return new ArrayList<>(TRACE); + } + + /** Drives one reflection call and reports whether the cursor interceptor saw it. */ + private boolean reflectionIsBracketed(boolean serviceLevel) throws Exception { + TRACE.clear(); + int port = freePort(); + ServerBuilder builder = ServerBuilder.forPort(port); + AttachProbe cursor = new AttachProbe(); + if (serviceLevel) { + builder.addService(ServerInterceptors.intercept(new ProbeDatabaseApi(), cursor)); + } else { + builder.addService(new ProbeDatabaseApi()).intercept(cursor); + } + builder.addService(ProtoReflectionService.newInstance()); + + Server server = builder.build().start(); + ManagedChannel channel = + ManagedChannelBuilder.forAddress("127.0.0.1", port).usePlaintext().build(); + CountDownLatch done = new CountDownLatch(1); + try { + StreamObserver request = + ServerReflectionGrpc.newStub(channel).serverReflectionInfo( + new StreamObserver() { + @Override + public void onNext(ServerReflectionResponse value) { + } + + @Override + public void onError(Throwable t) { + done.countDown(); + } + + @Override + public void onCompleted() { + done.countDown(); + } + }); + request.onNext(ServerReflectionRequest.newBuilder().setListServices("").build()); + request.onCompleted(); + done.await(5, TimeUnit.SECONDS); + } finally { + shutdown(channel, server); + } + for (String entry : TRACE) { + if (entry.startsWith("attached:") && entry.contains("ServerReflection")) { + return true; + } + } + return false; + } + + private static void shutdown(ManagedChannel channel, Server server) throws Exception { + channel.shutdownNow(); + server.shutdownNow(); + server.awaitTermination(5, TimeUnit.SECONDS); + } + + private static int freePort() throws Exception { + try (ServerSocket socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorScopeTest.java b/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorScopeTest.java new file mode 100644 index 00000000000..54e82426c0c --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorScopeTest.java @@ -0,0 +1,136 @@ +package org.tron.core.services.filter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import java.lang.reflect.Field; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.core.db.Manager; +import org.tron.core.db2.core.Chainbase; + +/** + * Pins the cursor's scope without depending on thread-pool scheduling. + * + *

{@code interceptCall()} is driven on thread A and the returned listener's + * {@code onHalfClose()} on a different thread B, which is what gRPC's {@code SerializingExecutor} + * is free to do. An implementation that scoped the cursor around {@code interceptCall} instead of + * {@code onHalfClose} then fails here by construction rather than by luck. + */ +public class CursorInterceptorScopeTest { + + private ExecutorService threadA; + private ExecutorService threadB; + + private Manager manager; + private Chainbase.Cursor cursorDuringHandler; + private Chainbase.Cursor cursorAfterCall; + private Chainbase.Cursor current; + + @Before + public void setUp() { + threadA = Executors.newSingleThreadExecutor(r -> new Thread(r, "cursor-thread-A")); + threadB = Executors.newSingleThreadExecutor(r -> new Thread(r, "cursor-thread-B")); + + // a Manager whose cursor state is observable, standing in for the ThreadLocal in Chainbase + current = Chainbase.Cursor.HEAD; + manager = mock(Manager.class); + doAnswer(inv -> current = inv.getArgument(0)) + .when(manager).setCursor(any(Chainbase.Cursor.class)); + doAnswer(inv -> current = Chainbase.Cursor.HEAD).when(manager).resetCursor(); + } + + @After + public void tearDown() throws Exception { + threadA.shutdownNow(); + threadB.shutdownNow(); + threadA.awaitTermination(5, TimeUnit.SECONDS); + threadB.awaitTermination(5, TimeUnit.SECONDS); + } + + @Test + public void testHandlerSeesTheCursorWhenInterceptCallRanOnAnotherThread() throws Exception { + ServerCall.Listener listener = startCallOnThreadA(false); + + // the handler runs from onHalfClose, on a different thread than interceptCall + runOn(threadB, () -> { + listener.onHalfClose(); + return null; + }); + + Assert.assertEquals("handler must observe the SOLIDITY cursor", + Chainbase.Cursor.SOLIDITY, cursorDuringHandler); + Assert.assertEquals("cursor must be back at HEAD once the handler returns", + Chainbase.Cursor.HEAD, cursorAfterCall); + } + + @Test + public void testCursorIsRestoredOnThreadBWhenTheHandlerThrows() throws Exception { + ServerCall.Listener listener = startCallOnThreadA(true); + + try { + runOn(threadB, () -> { + listener.onHalfClose(); + return null; + }); + Assert.fail("expected the handler failure to propagate"); + } catch (Exception expected) { + // what matters is the cursor state below + } + + Assert.assertEquals("a throwing handler must still leave the cursor at HEAD", + Chainbase.Cursor.HEAD, current); + } + + /** Runs interceptCall on thread A and returns the listener, with a handler that records state. */ + private ServerCall.Listener startCallOnThreadA(boolean handlerThrows) throws Exception { + SolidityCursorInterceptor interceptor = new SolidityCursorInterceptor(); + Field dbManager = CursorServerInterceptor.class.getDeclaredField("dbManager"); + dbManager.setAccessible(true); + dbManager.set(interceptor, manager); + + @SuppressWarnings("unchecked") + ServerCall call = mock(ServerCall.class); + @SuppressWarnings("unchecked") + MethodDescriptor descriptor = mock(MethodDescriptor.class); + doAnswer(inv -> descriptor).when(call).getMethodDescriptor(); + + ServerCallHandler handler = (c, h) -> new ServerCall.Listener() { + @Override + public void onHalfClose() { + cursorDuringHandler = current; + if (handlerThrows) { + throw new IllegalStateException("boom"); + } + } + }; + + return runOn(threadA, () -> { + ServerCall.Listener l = interceptor.interceptCall(call, new Metadata(), handler); + cursorAfterCall = current; + return l; + }); + } + + private static T runOn(ExecutorService executor, Callable task) throws Exception { + try { + return executor.submit(task).get(5, TimeUnit.SECONDS); + } catch (java.util.concurrent.ExecutionException e) { + if (e.getCause() instanceof Exception) { + throw (Exception) e.getCause(); + } + throw e; + } + } +} From b5e9779e42e78bc35454c988eabb625c61d46b10 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 1 Sep 2026 15:24:05 +0800 Subject: [PATCH 12/16] fix comments --- .../org/tron/core/services/RpcApiService.java | 2 +- .../filter/CursorServerInterceptor.java | 36 ++++++------------- .../interfaceOnPBFT/RpcApiServiceOnPBFT.java | 10 +----- .../RpcApiServiceOnSolidity.java | 8 +---- 4 files changed, 13 insertions(+), 43 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/RpcApiService.java b/framework/src/main/java/org/tron/core/services/RpcApiService.java index 44e6b711028..fd3ab405334 100755 --- a/framework/src/main/java/org/tron/core/services/RpcApiService.java +++ b/framework/src/main/java/org/tron/core/services/RpcApiService.java @@ -182,7 +182,7 @@ public class RpcApiService extends RpcService { @Getter private DatabaseApi databaseApi = new DatabaseApi(); // WalletApi is the full protocol.Wallet impl (HEAD); WalletSolidityApi is its read-only subset, - // reused by the Solidity/PBFT cursor ports and pinned by WalletSolidityApiMethodSubsetTest. + // reused by the Solidity/PBFT cursor ports. private WalletApi walletApi = new WalletApi(); @Getter private WalletSolidityApi walletSolidityApi = new WalletSolidityApi(); diff --git a/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java b/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java index 6ea5d24f316..6414cff32a0 100644 --- a/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java +++ b/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java @@ -10,35 +10,21 @@ import org.tron.core.db2.core.Chainbase; /** - * Switches the current thread's read cursor for the duration of the synchronous handler callback - * and restores it afterwards, so a handler served through this interceptor reads from the snapshot - * its subclass selects. The services behind it never touch the cursor, so one instance can serve - * HEAD, SOLIDITY and PBFT semantics on different servers. - * - *

The scope is deliberately the handler callback, not the call: a call spans several listener - * callbacks and may span several threads, so call-lifetime scoping is not a safe model for a - * {@link ThreadLocal}. + * Switches the current thread's read cursor around the synchronous handler callback and restores it + * afterwards, so the handler reads from the snapshot its subclass selects (HEAD / SOLIDITY / PBFT); + * the services behind it never touch the cursor. * *

Two invariants it relies on: *

    - *
  • The bracket wraps {@code onHalfClose()}, not {@code interceptCall}: gRPC invokes the unary - * handler inline from {@code onHalfClose}, while {@code interceptCall} runs as a separate task and - * may land on another thread of the same pool — a {@code SerializingExecutor} orders the callbacks - * but does not pin them to one thread. The cursor is a {@link ThreadLocal}, so setting it elsewhere - * fails silently and the port serves HEAD data. - *
  • Database handlers must be synchronous: the cursor is reset when {@code onHalfClose} returns, - * so a handler that defers its reads to another executor, thread or future would read HEAD. Every - * method behind the cursor ports is a synchronous unary handler today, but that is an - * implementation invariant rather than a type-level guarantee — a future handler that moves a - * database read off this thread needs explicit cursor propagation. + *
  • The bracket wraps {@code onHalfClose()} — where gRPC runs the unary handler inline — not + * {@code interceptCall}, which may land on another pool thread; the cursor is a {@link ThreadLocal}, + * so setting it elsewhere fails silently and the port serves HEAD. + *
  • Handlers must be synchronous: the cursor is reset when {@code onHalfClose} returns, so a read + * deferred to another thread would read HEAD. *
* - *

The {@code finally} reset is mandatory: gRPC reuses a fixed thread pool, so a leftover cursor - * would leak into the next call on the same thread. - * - *

Attach this at the service level ({@code ServerInterceptors.intercept}) rather than the - * server level, so it stays inside the server-wide chain by construction instead of by registration - * order, and only brackets the services that actually read chain state. + *

The {@code finally} reset is mandatory: the fixed thread pool is reused, so a leftover cursor + * leaks into the next call on that thread. */ public abstract class CursorServerInterceptor implements ServerInterceptor { @@ -55,8 +41,6 @@ public ServerCall.Listener interceptCall( @Override public void onHalfClose() { try { - // PBFT additionally needs a head-to-pbft offset, computed inside Manager#setCursor - // per call. dbManager.setCursor(cursor); super.onHalfClose(); } finally { diff --git a/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/RpcApiServiceOnPBFT.java b/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/RpcApiServiceOnPBFT.java index 70dcdcb3b23..2e6d1bd59bd 100755 --- a/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/RpcApiServiceOnPBFT.java +++ b/framework/src/main/java/org/tron/core/services/interfaceOnPBFT/RpcApiServiceOnPBFT.java @@ -24,15 +24,7 @@ public RpcApiServiceOnPBFT() { executorName = "rpc-pbft-executor"; } - /** - * Binds the PBFT cursor to the two shared services rather than to the server. A service-level - * interceptor lives inside the {@code ServerServiceDefinition}, so it always sits between the - * server-level chain and the handler regardless of registration order, and it reaches only these - * two services — the server-level chain (rate limiter, api access, lite-fullnode, prometheus) is - * left exactly as the base class builds it, and reflection is not bracketed. That last point - * matters here: switching to the PBFT cursor reads the head and latest-pbft block numbers, so it - * should not run for calls that never touch chain state. - */ + /** PBFT cursor bound at the service level, so it brackets only these two read services. */ @Override protected void addService(NettyServerBuilder serverBuilder) { serverBuilder.addService( diff --git a/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/RpcApiServiceOnSolidity.java b/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/RpcApiServiceOnSolidity.java index 7f70f7ba01f..f0c7b1468d2 100755 --- a/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/RpcApiServiceOnSolidity.java +++ b/framework/src/main/java/org/tron/core/services/interfaceOnSolidity/RpcApiServiceOnSolidity.java @@ -24,13 +24,7 @@ public RpcApiServiceOnSolidity() { executorName = "rpc-solidity-executor"; } - /** - * Binds the SOLIDITY cursor to the two shared services rather than to the server. A service-level - * interceptor lives inside the {@code ServerServiceDefinition}, so it always sits between the - * server-level chain and the handler regardless of registration order, and it reaches only these - * two services — the server-level chain (rate limiter, api access, lite-fullnode, prometheus) is - * left exactly as the base class builds it, and reflection is not bracketed. - */ + /** SOLIDITY cursor bound at the service level, so it brackets only these two read services. */ @Override protected void addService(NettyServerBuilder serverBuilder) { serverBuilder.addService( From 670da6f56a53bc23d39b02ae6cedbb11ef9964f4 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 1 Sep 2026 15:47:08 +0800 Subject: [PATCH 13/16] style(rpc): wrap cursor interceptor javadoc to 100 columns --- .../tron/core/services/filter/CursorServerInterceptor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java b/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java index 6414cff32a0..74d14daede9 100644 --- a/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java +++ b/framework/src/main/java/org/tron/core/services/filter/CursorServerInterceptor.java @@ -17,8 +17,8 @@ *

Two invariants it relies on: *

    *
  • The bracket wraps {@code onHalfClose()} — where gRPC runs the unary handler inline — not - * {@code interceptCall}, which may land on another pool thread; the cursor is a {@link ThreadLocal}, - * so setting it elsewhere fails silently and the port serves HEAD. + * {@code interceptCall}, which may land on another pool thread; the cursor is a + * {@link ThreadLocal}, so setting it elsewhere fails silently and the port serves HEAD. *
  • Handlers must be synchronous: the cursor is reset when {@code onHalfClose} returns, so a read * deferred to another thread would read HEAD. *
From 726c428dd13df1c2a8153291f0b8f8c01e0f9e55 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 1 Sep 2026 15:47:08 +0800 Subject: [PATCH 14/16] fix(rpc): return after onError to close the call once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A handler that called responseObserver.onError(...) and then fell through to responseObserver.onCompleted() closed the call twice; the second close() hits checkState(!closeCalled, "call already closed") in ServerCallImpl and throws. Clients were unaffected — onError had already closed the call with the error status — but every failed call cost a server-side IllegalStateException. WalletApi already used the return form in the handlers it had been hardened in; 24 handlers in the same file still fell through. Adds the missing return to all of them: 17 in WalletApi and 7 shared helpers reachable from both WalletApi and WalletSolidityApi. --- .../org/tron/core/services/RpcApiService.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/framework/src/main/java/org/tron/core/services/RpcApiService.java b/framework/src/main/java/org/tron/core/services/RpcApiService.java index fd3ab405334..8e9802b52fb 100755 --- a/framework/src/main/java/org/tron/core/services/RpcApiService.java +++ b/framework/src/main/java/org/tron/core/services/RpcApiService.java @@ -1607,6 +1607,7 @@ public void getPaginatedNowWitnessList(PaginatedMessage request, wallet.getPaginatedNowWitnessList(request.getOffset(), request.getLimit())); } catch (MaintenanceUnavailableException e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); } @@ -1636,6 +1637,7 @@ public void getDelegatedResourceV2(DelegatedResourceMessage request, ); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); } @@ -1648,6 +1650,7 @@ public void getDelegatedResourceAccountIndex(BytesMessage request, .onNext(wallet.getDelegatedResourceAccountIndex(request.getValue())); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); } @@ -1660,6 +1663,7 @@ public void getDelegatedResourceAccountIndexV2(BytesMessage request, .onNext(wallet.getDelegatedResourceAccountIndexV2(request.getValue())); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); } @@ -1672,6 +1676,7 @@ public void getCanDelegatedMaxSize(GrpcAPI.CanDelegatedMaxSizeRequestMessage req request.getOwnerAddress(), request.getType())); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); @@ -1685,6 +1690,7 @@ public void getAvailableUnfreezeCount(GrpcAPI.GetAvailableUnfreezeCountRequestMe request.getOwnerAddress())); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); @@ -1700,6 +1706,7 @@ public void getCanWithdrawUnfreezeAmount(CanWithdrawUnfreezeAmountRequestMessage )); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); } @@ -1711,6 +1718,7 @@ public void getBandwidthPrices(EmptyMessage request, responseObserver.onNext(wallet.getBandwidthPrices()); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); } @@ -1722,6 +1730,7 @@ public void getEnergyPrices(EmptyMessage request, responseObserver.onNext(wallet.getEnergyPrices()); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); } @@ -1733,6 +1742,7 @@ public void getMemoFee(EmptyMessage request, responseObserver.onNext(wallet.getMemoFeePrices()); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); } @@ -1789,6 +1799,7 @@ public void getNodeInfo(EmptyMessage request, StreamObserver responseO responseObserver.onNext(nodeInfoService.getNodeInfo().transferToProtoEntity()); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); } @@ -2265,6 +2276,7 @@ public void getTransactionInfoByBlockNum(NumberMessage request, responseObserver.onNext(wallet.getTransactionInfoByBlockNum(request.getNum())); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); @@ -2294,6 +2306,7 @@ public void getMarketOrderByAccount(BytesMessage request, responseObserver.onNext(marketOrderList); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); } @@ -2309,6 +2322,7 @@ public void getMarketOrderById(BytesMessage request, responseObserver.onNext(marketOrder); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); } @@ -2323,6 +2337,7 @@ public void getMarketPriceByPair(MarketOrderPair request, responseObserver.onNext(marketPriceList); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); } @@ -2337,6 +2352,7 @@ public void getMarketOrderListByPair(org.tron.protos.Protocol.MarketOrderPair re responseObserver.onNext(orderPairList); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); } @@ -2349,6 +2365,7 @@ public void getMarketPairList(EmptyMessage request, responseObserver.onNext(pairList); } catch (Exception e) { responseObserver.onError(getRunTimeException(e)); + return; } responseObserver.onCompleted(); } @@ -2398,6 +2415,7 @@ public void getRewardInfoCommon(BytesMessage request, responseObserver.onNext(builder.build()); } catch (Exception e) { responseObserver.onError(e); + return; } responseObserver.onCompleted(); } @@ -2411,6 +2429,7 @@ public void getBurnTrxCommon(EmptyMessage request, responseObserver.onNext(builder.build()); } catch (Exception e) { responseObserver.onError(e); + return; } responseObserver.onCompleted(); } @@ -2426,6 +2445,7 @@ public void getBrokerageInfoCommon(BytesMessage request, responseObserver.onNext(builder.build()); } catch (Exception e) { responseObserver.onError(e); + return; } responseObserver.onCompleted(); } @@ -2452,6 +2472,7 @@ public void getTransactionFromPendingCommon(BytesMessage request, responseObserver.onNext(transactionCapsule == null ? null : transactionCapsule.getInstance()); } catch (Exception e) { responseObserver.onError(e); + return; } responseObserver.onCompleted(); } @@ -2464,6 +2485,7 @@ public void getTransactionListFromPendingCommon(EmptyMessage request, responseObserver.onNext(builder.build()); } catch (Exception e) { responseObserver.onError(e); + return; } responseObserver.onCompleted(); } @@ -2476,6 +2498,7 @@ public void getPendingSizeCommon(EmptyMessage request, responseObserver.onNext(builder.build()); } catch (Exception e) { responseObserver.onError(e); + return; } responseObserver.onCompleted(); } @@ -2491,6 +2514,7 @@ public void getBlockCommon(GrpcAPI.BlockReq request, } else { responseObserver.onError(getRunTimeException(e)); } + return; } responseObserver.onCompleted(); } From 8e38bb905aa7089e3bf2c412b4e5455ade222015 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 1 Sep 2026 16:12:48 +0800 Subject: [PATCH 15/16] test(rpc): cover the error path, cursor wiring and pbft reads Three gaps the existing suite left open: RpcApiServiceErrorPathTest drives every unary handler of WalletApi and WalletSolidityApi with collaborators that throw, and asserts none of them terminates the call more than once. Reverting the return fix makes it fail on getDelegatedResourceV2, getPendingSize and getBlock, so it also reaches the shared *Common helpers. CursorInterceptorWiringTest runs the real addService of the solidity and pbft services against a mock builder and asserts both shared read services are registered as intercepted definitions. Dropping ServerInterceptors.intercept left every existing test green while the port silently served HEAD; this is the grpc counterpart of CursorFilterInstallationTest. RpcApiServicesTest now drives getPaginatedNowWitnessList and getTransactionInfoByBlockNum on the pbft stub as well, pinning the one intentional behaviour change of the merge. --- .../services/RpcApiServiceErrorPathTest.java | 140 ++++++++++++++++++ .../core/services/RpcApiServicesTest.java | 2 + .../filter/CursorInterceptorWiringTest.java | 101 +++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 framework/src/test/java/org/tron/core/services/RpcApiServiceErrorPathTest.java create mode 100644 framework/src/test/java/org/tron/core/services/filter/CursorInterceptorWiringTest.java diff --git a/framework/src/test/java/org/tron/core/services/RpcApiServiceErrorPathTest.java b/framework/src/test/java/org/tron/core/services/RpcApiServiceErrorPathTest.java new file mode 100644 index 00000000000..16437f139e7 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/RpcApiServiceErrorPathTest.java @@ -0,0 +1,140 @@ +package org.tron.core.services; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +import com.google.protobuf.Message; +import io.grpc.stub.StreamObserver; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.stubbing.Answer; +import org.tron.core.Wallet; +import org.tron.core.metrics.MetricsApiService; +import org.tron.core.services.RpcApiService.WalletApi; +import org.tron.core.services.RpcApiService.WalletSolidityApi; +import org.tron.core.utils.TransactionUtil; + +/** + * Pins the one-terminal-event rule on the gRPC error path: a handler that reports a failure through + * {@code onError} must not fall through to {@code onCompleted}. gRPC rejects the second close with + * {@code IllegalStateException("call already closed")}, so a handler doing both costs a server-side + * exception on every failed call while the client sees nothing extra. + * + *

The rule is checked for every handler rather than for the ones that were fixed, because the + * shape is trivially reintroduced by copying a neighbouring handler. + */ +public class RpcApiServiceErrorPathTest { + + /** Minimum handlers that must actually fail, so the sweep cannot silently cover nothing. */ + private static final int MIN_EXERCISED = 20; + + @Test + public void testWalletApiTerminatesTheCallOnce() throws Exception { + assertSingleTerminalEvent(WalletApi.class); + } + + @Test + public void testWalletSolidityApiTerminatesTheCallOnce() throws Exception { + assertSingleTerminalEvent(WalletSolidityApi.class); + } + + /** + * Drives every unary handler of the given service class with collaborators that throw, and + * asserts none of them terminates the call more than once. + */ + private static void assertSingleTerminalEvent(Class apiClass) throws Exception { + RpcApiService service = mock(RpcApiService.class, + withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); + injectThrowingCollaborators(service); + Object api = apiClass.getDeclaredConstructor(RpcApiService.class).newInstance(service); + + int exercised = 0; + for (Method method : apiClass.getDeclaredMethods()) { + if (!isUnaryHandler(method)) { + continue; + } + Message request = (Message) method.getParameterTypes()[0] + .getMethod("getDefaultInstance").invoke(null); + TerminalRecorder recorder = new TerminalRecorder(); + try { + method.invoke(api, request, recorder); + } catch (InvocationTargetException e) { + // a handler that lets the failure escape cannot have closed the call twice + continue; + } + Assert.assertTrue( + method.getName() + " terminated the call " + recorder.events.size() + " times " + + recorder.events + "; onError must be followed by return", + recorder.events.size() <= 1); + if (!recorder.events.isEmpty()) { + exercised++; + } + } + Assert.assertTrue( + apiClass.getSimpleName() + " exercised only " + exercised + " handlers, expected at least " + + MIN_EXERCISED + " — the sweep is no longer reaching the handler bodies", + exercised >= MIN_EXERCISED); + } + + private static boolean isUnaryHandler(Method method) { + Class[] params = method.getParameterTypes(); + return Modifier.isPublic(method.getModifiers()) + && method.getReturnType() == void.class + && params.length == 2 + && Message.class.isAssignableFrom(params[0]) + && params[1] == StreamObserver.class; + } + + /** + * Replaces the service's collaborators with mocks that throw on every call, so each handler takes + * its own error path, and binds a real {@code WalletApi} for the solidity handlers to delegate + * to. + */ + private static void injectThrowingCollaborators(RpcApiService service) throws Exception { + Answer throwing = invocation -> { + throw new RuntimeException("collaborator unavailable"); + }; + set(service, "wallet", mock(Wallet.class, withSettings().defaultAnswer(throwing))); + set(service, "transactionUtil", + mock(TransactionUtil.class, withSettings().defaultAnswer(throwing))); + set(service, "nodeInfoService", + mock(NodeInfoService.class, withSettings().defaultAnswer(throwing))); + set(service, "metricsApiService", + mock(MetricsApiService.class, withSettings().defaultAnswer(throwing))); + set(service, "walletApi", + WalletApi.class.getDeclaredConstructor(RpcApiService.class).newInstance(service)); + } + + private static void set(RpcApiService service, String name, Object value) throws Exception { + Field field = RpcApiService.class.getDeclaredField(name); + field.setAccessible(true); + field.set(service, value); + } + + /** Counts terminal events instead of closing a real call. */ + private static final class TerminalRecorder implements StreamObserver { + + private final List events = new ArrayList<>(); + + @Override + public void onNext(Object value) { + } + + @Override + public void onError(Throwable t) { + events.add("onError"); + } + + @Override + public void onCompleted() { + events.add("onCompleted"); + } + } +} diff --git a/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java b/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java index c3ac5800971..3df54b9c0f9 100644 --- a/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java +++ b/framework/src/test/java/org/tron/core/services/RpcApiServicesTest.java @@ -277,6 +277,7 @@ public void testGetPaginatedNowWitnessList() { .setOffset(0).setLimit(5).build(); assertNotNull(blockingStubFull.getPaginatedNowWitnessList(paginatedMessage)); assertNotNull(blockingStubSolidity.getPaginatedNowWitnessList(paginatedMessage)); + assertNotNull(blockingStubPBFT.getPaginatedNowWitnessList(paginatedMessage)); } @Test @@ -673,6 +674,7 @@ public void testGetTransactionInfoByBlockNum() { NumberMessage message = NumberMessage.newBuilder().setNum(1).build(); assertNotNull(blockingStubFull.getTransactionInfoByBlockNum(message)); assertNotNull(blockingStubSolidity.getTransactionInfoByBlockNum(message)); + assertNotNull(blockingStubPBFT.getTransactionInfoByBlockNum(message)); } @Test diff --git a/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorWiringTest.java b/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorWiringTest.java new file mode 100644 index 00000000000..17bf11ce09e --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorWiringTest.java @@ -0,0 +1,101 @@ +package org.tron.core.services.filter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.withSettings; + +import io.grpc.BindableService; +import io.grpc.ServerServiceDefinition; +import io.grpc.netty.NettyServerBuilder; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.tron.api.DatabaseGrpc; +import org.tron.api.WalletSolidityGrpc; +import org.tron.core.services.RpcApiService; +import org.tron.core.services.interfaceOnPBFT.RpcApiServiceOnPBFT; +import org.tron.core.services.interfaceOnSolidity.RpcApiServiceOnSolidity; + +/** + * Guards that each cursor gRPC service registers the shared read services through its + * cursor interceptor. Nothing else catches a dropped interceptor: the services would still be + * served and every response would still look well-formed, only resolved against HEAD instead of the + * solidified or PBFT snapshot. This is the gRPC counterpart of CursorFilterInstallationTest. + * + *

Registering without the interceptor binds the {@code addService(BindableService)} overload + * rather than the {@code addService(ServerServiceDefinition)} one, so the two are distinguishable + * here. What the interceptor does once attached is pinned by CursorInterceptorScopeTest and + * GrpcInterceptorProbeTest. + */ +public class CursorInterceptorWiringTest { + + private static final Set SHARED_READ_SERVICES = new HashSet<>( + Arrays.asList(DatabaseGrpc.SERVICE_NAME, WalletSolidityGrpc.SERVICE_NAME)); + + @Test + public void testSolidityServiceRegistersBothReadServicesThroughTheCursor() throws Exception { + Assert.assertEquals(SHARED_READ_SERVICES, + interceptedServices(RpcApiServiceOnSolidity.class, new SolidityCursorInterceptor())); + } + + @Test + public void testPbftServiceRegistersBothReadServicesThroughTheCursor() throws Exception { + Assert.assertEquals(SHARED_READ_SERVICES, + interceptedServices(RpcApiServiceOnPBFT.class, new PbftCursorInterceptor())); + } + + /** + * Runs the service's real addService against a mock builder and returns the names of the services + * it registered as intercepted definitions, failing if any was registered unintercepted. + */ + private static Set interceptedServices(Class serviceClass, + CursorServerInterceptor interceptor) throws Exception { + RpcApiService rpcApiService = mock(RpcApiService.class); + given(rpcApiService.getDatabaseApi()).willReturn(RpcApiService.DatabaseApi.class + .getDeclaredConstructor(RpcApiService.class).newInstance(rpcApiService)); + given(rpcApiService.getWalletSolidityApi()).willReturn(RpcApiService.WalletSolidityApi.class + .getDeclaredConstructor(RpcApiService.class).newInstance(rpcApiService)); + + Object service = mock(serviceClass, withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS)); + inject(serviceClass, service, rpcApiService); + inject(serviceClass, service, interceptor); + + NettyServerBuilder builder = mock(NettyServerBuilder.class); + Method addService = serviceClass.getDeclaredMethod("addService", NettyServerBuilder.class); + addService.setAccessible(true); + addService.invoke(service, builder); + + verify(builder, never()).addService(any(BindableService.class)); + ArgumentCaptor registered = + ArgumentCaptor.forClass(ServerServiceDefinition.class); + verify(builder, times(2)).addService(registered.capture()); + + Set names = new HashSet<>(); + for (ServerServiceDefinition definition : registered.getAllValues()) { + names.add(definition.getServiceDescriptor().getName()); + } + return names; + } + + /** Sets the one declared field the value fits; the two injected types are unrelated. */ + private static void inject(Class serviceClass, Object service, Object value) throws Exception { + for (Field field : serviceClass.getDeclaredFields()) { + if (field.getType().isInstance(value)) { + field.setAccessible(true); + field.set(service, value); + return; + } + } + Assert.fail(serviceClass.getSimpleName() + " has no field for " + value.getClass().getName()); + } +} From 8b58c55d6de9e7f266b646a3f62ab159ec234d29 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Tue, 1 Sep 2026 16:22:49 +0800 Subject: [PATCH 16/16] test(rpc): drop probe tests that guard nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three of the tests written while designing the cursor interceptor were exploration scaffolding, not regression guards: CursorInterceptorAttachmentTest drove probe services only, so switching the production code back to server-level attachment left it green; the wiring it was meant to justify is now covered by CursorInterceptorWiringTest, and the reasoning belongs in the PR. WalletSolidityApiMethodSubsetTest asserted every WalletSolidity method exists on WalletApi, which the compiler enforces now that the handlers delegate by name. It would also fail on a legitimate WalletSolidity-only method implemented in place. GrpcInterceptorProbeTest kept an ordering assertion whose premise no longer holds — service-level attachment made registration order irrelevant to the cursor services — plus two library-property tests already covered through the production interceptor, and printed traces left over from probing. What that last class did earn is kept as CursorInterceptorServerTest: a real server proving gRPC runs the handler inline from onHalfClose on the same thread, which CursorInterceptorScopeTest cannot show on its synthetic harness and which fails silently in production if it breaks. Also drops two javadoc references to the deleted subset test. --- .../org/tron/core/services/RpcApiService.java | 8 +- .../WalletSolidityApiMethodSubsetTest.java | 47 --- .../CursorInterceptorAttachmentTest.java | 195 --------- .../filter/CursorInterceptorServerTest.java | 123 ++++++ .../filter/CursorInterceptorWiringTest.java | 2 +- .../filter/GrpcInterceptorProbeTest.java | 379 ------------------ 6 files changed, 128 insertions(+), 626 deletions(-) delete mode 100644 framework/src/test/java/org/tron/core/services/WalletSolidityApiMethodSubsetTest.java delete mode 100644 framework/src/test/java/org/tron/core/services/filter/CursorInterceptorAttachmentTest.java create mode 100644 framework/src/test/java/org/tron/core/services/filter/CursorInterceptorServerTest.java delete mode 100644 framework/src/test/java/org/tron/core/services/filter/GrpcInterceptorProbeTest.java diff --git a/framework/src/main/java/org/tron/core/services/RpcApiService.java b/framework/src/main/java/org/tron/core/services/RpcApiService.java index 8e9802b52fb..5bea36bf632 100755 --- a/framework/src/main/java/org/tron/core/services/RpcApiService.java +++ b/framework/src/main/java/org/tron/core/services/RpcApiService.java @@ -365,8 +365,8 @@ public void getDynamicProperties(EmptyMessage request, /** * WalletSolidityApi is the full implementation of the {@code protocol.WalletSolidity} gRPC - * service. Every method here is read-only and also present on {@link WalletApi}: this is a - * read-only subset of {@code WalletApi}, guarded by {@code WalletSolidityApiMethodSubsetTest}. + * service. Every method here is read-only and also present on {@link WalletApi}, so each one + * delegates to the shared {@code WalletApi} singleton instead of repeating its body. */ public class WalletSolidityApi extends WalletSolidityImplBase { @@ -667,8 +667,8 @@ private TransactionListExtention transactionList2Extention(TransactionList trans /** * WalletApi is the full implementation of the {@code protocol.Wallet} gRPC service, including - * write and build endpoints. {@link WalletSolidityApi} is the read-only subset of this surface, - * pinned by {@code WalletSolidityApiMethodSubsetTest}. + * write and build endpoints. {@link WalletSolidityApi} is the read-only subset of this surface + * and delegates its handlers here. */ public class WalletApi extends WalletImplBase { diff --git a/framework/src/test/java/org/tron/core/services/WalletSolidityApiMethodSubsetTest.java b/framework/src/test/java/org/tron/core/services/WalletSolidityApiMethodSubsetTest.java deleted file mode 100644 index 10a22f683ee..00000000000 --- a/framework/src/test/java/org/tron/core/services/WalletSolidityApiMethodSubsetTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.tron.core.services; - -import io.grpc.stub.StreamObserver; -import java.util.Arrays; -import java.util.Set; -import java.util.TreeSet; -import java.util.stream.Collectors; -import org.junit.Assert; -import org.junit.Test; - -/** - * Guards the precondition for the future WalletApi / WalletSolidityApi dedup (clean-rpc.md §10). - * - *

{@code WalletSolidityApi} (serving {@code protocol.WalletSolidity}) and {@code WalletApi} - * (serving {@code protocol.Wallet}) implement the same read methods twice as byte-identical glue - * over the shared {@code Wallet} object. Deduping means replacing the WalletSolidity bodies with a - * one-line delegation, which is only sound while every WalletSolidity gRPC method also exists on - * WalletApi. This test pins that subset relationship so the invariant cannot silently break. - */ -public class WalletSolidityApiMethodSubsetTest { - - /** Signatures of the gRPC unary handlers a service impl overrides: {@code name(requestType)}. */ - private static Set grpcHandlerSignatures(Class impl) { - return Arrays.stream(impl.getDeclaredMethods()) - .filter(m -> m.getReturnType() == void.class) - .filter(m -> m.getParameterCount() == 2) - .filter(m -> m.getParameterTypes()[1] == StreamObserver.class) - .map(m -> m.getName() + "(" + m.getParameterTypes()[0].getName() + ")") - .collect(Collectors.toCollection(TreeSet::new)); - } - - @Test - public void testWalletSolidityApiMethodsAllExistOnWalletApi() throws ClassNotFoundException { - Class walletApi = Class.forName("org.tron.core.services.RpcApiService$WalletApi"); - Class walletSolidityApi = - Class.forName("org.tron.core.services.RpcApiService$WalletSolidityApi"); - - Set onWalletApi = grpcHandlerSignatures(walletApi); - Set solidityOnly = new TreeSet<>(grpcHandlerSignatures(walletSolidityApi)); - solidityOnly.removeAll(onWalletApi); - - Assert.assertTrue( - "these WalletSolidityApi methods have no WalletApi counterpart and cannot be dedup'd by " - + "delegating to a shared Wallet handler (see clean-rpc.md §10): " + solidityOnly, - solidityOnly.isEmpty()); - } -} diff --git a/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorAttachmentTest.java b/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorAttachmentTest.java deleted file mode 100644 index d573a379a7c..00000000000 --- a/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorAttachmentTest.java +++ /dev/null @@ -1,195 +0,0 @@ -package org.tron.core.services.filter; - -import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Metadata; -import io.grpc.Server; -import io.grpc.ServerBuilder; -import io.grpc.ServerCall; -import io.grpc.ServerCallHandler; -import io.grpc.ServerInterceptor; -import io.grpc.ServerInterceptors; -import io.grpc.protobuf.services.ProtoReflectionService; -import io.grpc.reflection.v1alpha.ServerReflectionGrpc; -import io.grpc.reflection.v1alpha.ServerReflectionRequest; -import io.grpc.reflection.v1alpha.ServerReflectionResponse; -import io.grpc.stub.StreamObserver; -import java.net.ServerSocket; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import org.junit.Assert; -import org.junit.Test; -import org.tron.api.DatabaseGrpc; -import org.tron.api.DatabaseGrpc.DatabaseImplBase; -import org.tron.api.GrpcAPI.EmptyMessage; -import org.tron.protos.Protocol.Block; - -/** - * Why the cursor interceptor is attached to the service definitions rather than to the server. - * - *

Both wirings put it innermost, so the call order a read sees is the same either way — the - * first test pins that, so the switch is provably behaviour-preserving. What differs is reach: a - * server-level interceptor brackets every service on the port, including the streaming reflection - * service, while a service-level one reaches only the services it is bound to. The second test - * pins that, which is the reason for the choice: switching the PBFT cursor reads chain state, and - * a call that never touches the database should not pay for it. - */ -public class CursorInterceptorAttachmentTest { - - private static final List TRACE = new CopyOnWriteArrayList<>(); - - /** Records when its listener runs, i.e. where it sits in the chain. */ - private static class OrderProbe implements ServerInterceptor { - - private final String tag; - - OrderProbe(String tag) { - this.tag = tag; - } - - @Override - public ServerCall.Listener interceptCall( - ServerCall call, Metadata headers, ServerCallHandler next) { - return new SimpleForwardingServerCallListener(next.startCall(call, headers)) { - @Override - public void onHalfClose() { - TRACE.add(tag); - super.onHalfClose(); - } - }; - } - } - - /** Records at interceptCall, so it also registers for streaming calls. */ - private static class AttachProbe implements ServerInterceptor { - - @Override - public ServerCall.Listener interceptCall( - ServerCall call, Metadata headers, ServerCallHandler next) { - TRACE.add("attached:" + call.getMethodDescriptor().getFullMethodName()); - return next.startCall(call, headers); - } - } - - private static class ProbeDatabaseApi extends DatabaseImplBase { - - @Override - public void getNowBlock(EmptyMessage request, StreamObserver observer) { - TRACE.add("handler"); - observer.onNext(Block.getDefaultInstance()); - observer.onCompleted(); - } - } - - @Test - public void testServiceLevelAttachmentKeepsTheSameCallOrder() throws Exception { - Assert.assertEquals("attaching the cursor per service must not reorder the chain", - callOrder(false), callOrder(true)); - // and it is innermost, immediately before the handler - List order = callOrder(true); - Assert.assertEquals("cursor must run last before the handler", - "cursor", order.get(order.size() - 2)); - Assert.assertEquals("handler", order.get(order.size() - 1)); - } - - @Test - public void testServiceLevelAttachmentLeavesReflectionAlone() throws Exception { - Assert.assertTrue("a server-level interceptor brackets reflection too", - reflectionIsBracketed(false)); - Assert.assertFalse("a service-level interceptor must not bracket reflection", - reflectionIsBracketed(true)); - } - - /** Drives one unary call and returns the observed chain order. */ - private List callOrder(boolean serviceLevel) throws Exception { - TRACE.clear(); - int port = freePort(); - ServerBuilder builder = ServerBuilder.forPort(port); - OrderProbe cursor = new OrderProbe("cursor"); - if (serviceLevel) { - builder.addService(ServerInterceptors.intercept(new ProbeDatabaseApi(), cursor)); - } else { - builder.addService(new ProbeDatabaseApi()).intercept(cursor); - } - // the server-level chain, registered exactly as RpcService#addInterceptor does - builder.intercept(new OrderProbe("rateLimiter")); - builder.intercept(new OrderProbe("apiAccess")); - builder.intercept(new OrderProbe("liteFnQuery")); - builder.intercept(new OrderProbe("prometheus")); - - Server server = builder.build().start(); - ManagedChannel channel = - ManagedChannelBuilder.forAddress("127.0.0.1", port).usePlaintext().build(); - try { - DatabaseGrpc.newBlockingStub(channel).getNowBlock(EmptyMessage.getDefaultInstance()); - } finally { - shutdown(channel, server); - } - return new ArrayList<>(TRACE); - } - - /** Drives one reflection call and reports whether the cursor interceptor saw it. */ - private boolean reflectionIsBracketed(boolean serviceLevel) throws Exception { - TRACE.clear(); - int port = freePort(); - ServerBuilder builder = ServerBuilder.forPort(port); - AttachProbe cursor = new AttachProbe(); - if (serviceLevel) { - builder.addService(ServerInterceptors.intercept(new ProbeDatabaseApi(), cursor)); - } else { - builder.addService(new ProbeDatabaseApi()).intercept(cursor); - } - builder.addService(ProtoReflectionService.newInstance()); - - Server server = builder.build().start(); - ManagedChannel channel = - ManagedChannelBuilder.forAddress("127.0.0.1", port).usePlaintext().build(); - CountDownLatch done = new CountDownLatch(1); - try { - StreamObserver request = - ServerReflectionGrpc.newStub(channel).serverReflectionInfo( - new StreamObserver() { - @Override - public void onNext(ServerReflectionResponse value) { - } - - @Override - public void onError(Throwable t) { - done.countDown(); - } - - @Override - public void onCompleted() { - done.countDown(); - } - }); - request.onNext(ServerReflectionRequest.newBuilder().setListServices("").build()); - request.onCompleted(); - done.await(5, TimeUnit.SECONDS); - } finally { - shutdown(channel, server); - } - for (String entry : TRACE) { - if (entry.startsWith("attached:") && entry.contains("ServerReflection")) { - return true; - } - } - return false; - } - - private static void shutdown(ManagedChannel channel, Server server) throws Exception { - channel.shutdownNow(); - server.shutdownNow(); - server.awaitTermination(5, TimeUnit.SECONDS); - } - - private static int freePort() throws Exception { - try (ServerSocket socket = new ServerSocket(0)) { - return socket.getLocalPort(); - } - } -} diff --git a/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorServerTest.java b/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorServerTest.java new file mode 100644 index 00000000000..57babc52051 --- /dev/null +++ b/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorServerTest.java @@ -0,0 +1,123 @@ +package org.tron.core.services.filter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.stub.StreamObserver; +import java.lang.reflect.Field; +import java.net.ServerSocket; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.tron.api.DatabaseGrpc; +import org.tron.api.DatabaseGrpc.DatabaseImplBase; +import org.tron.api.GrpcAPI.EmptyMessage; +import org.tron.core.db.Manager; +import org.tron.core.db2.core.Chainbase; +import org.tron.protos.Protocol.Block; + +/** + * Drives the production interceptor through a real gRPC server, which is the only place the + * assumption it rests on can be checked: that gRPC runs the handler inline from + * {@code onHalfClose}, on the same thread. The cursor is a {@link ThreadLocal}, so if that stops + * holding the cursor never reaches the read path and the port serves HEAD data with no error — + * responses stay well-formed, so nothing else notices. + * + *

CursorInterceptorScopeTest covers the interceptor's own logic on a synthetic harness; this is + * the end-to-end half. + */ +public class CursorInterceptorServerTest { + + private ExecutorService executor; + + @Before + public void setUp() { + // a fixed thread pool mirrors the production server configuration + executor = Executors.newFixedThreadPool(2, r -> { + Thread thread = new Thread(r); + thread.setName("cursor-rpc-executor-" + thread.getId()); + return thread; + }); + } + + @After + public void tearDown() throws Exception { + if (executor != null) { + executor.shutdown(); + if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } + } + + @Test + public void testCursorIsSetAndRestoredOnTheHandlerThread() throws Exception { + final List setOn = new CopyOnWriteArrayList<>(); + final List resetOn = new CopyOnWriteArrayList<>(); + final String[] handlerOn = new String[1]; + + Manager manager = mock(Manager.class); + doAnswer(inv -> setOn.add(Thread.currentThread().getName())) + .when(manager).setCursor(any(Chainbase.Cursor.class)); + doAnswer(inv -> resetOn.add(Thread.currentThread().getName())) + .when(manager).resetCursor(); + + SolidityCursorInterceptor interceptor = new SolidityCursorInterceptor(); + Field dbManager = CursorServerInterceptor.class.getDeclaredField("dbManager"); + dbManager.setAccessible(true); + dbManager.set(interceptor, manager); + + int port = freePort(); + Server server = ServerBuilder.forPort(port) + .executor(executor) + .addService(new DatabaseImplBase() { + @Override + public void getNowBlock(EmptyMessage request, StreamObserver observer) { + handlerOn[0] = Thread.currentThread().getName(); + observer.onNext(Block.getDefaultInstance()); + observer.onCompleted(); + } + }) + .intercept(interceptor) + .build() + .start(); + + ManagedChannel channel = ManagedChannelBuilder.forAddress("127.0.0.1", port) + .usePlaintext().directExecutor().build(); + try { + DatabaseGrpc.newBlockingStub(channel).getNowBlock(EmptyMessage.getDefaultInstance()); + + Assert.assertEquals("cursor must be set exactly once per call", 1, setOn.size()); + Assert.assertEquals("cursor must be restored exactly once per call", 1, resetOn.size()); + Assert.assertNotNull("handler did not run", handlerOn[0]); + // the ThreadLocal cursor only reaches the read path if it is set on the handler's thread + Assert.assertEquals("cursor was set on a thread other than the handler's", + handlerOn[0], setOn.get(0)); + Assert.assertEquals("cursor was restored on a thread other than the handler's", + handlerOn[0], resetOn.get(0)); + verify(manager).setCursor(Chainbase.Cursor.SOLIDITY); + } finally { + channel.shutdownNow(); + server.shutdownNow(); + server.awaitTermination(5, TimeUnit.SECONDS); + } + } + + private static int freePort() throws Exception { + try (ServerSocket socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorWiringTest.java b/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorWiringTest.java index 17bf11ce09e..5f403b9d742 100644 --- a/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorWiringTest.java +++ b/framework/src/test/java/org/tron/core/services/filter/CursorInterceptorWiringTest.java @@ -35,7 +35,7 @@ *

Registering without the interceptor binds the {@code addService(BindableService)} overload * rather than the {@code addService(ServerServiceDefinition)} one, so the two are distinguishable * here. What the interceptor does once attached is pinned by CursorInterceptorScopeTest and - * GrpcInterceptorProbeTest. + * CursorInterceptorServerTest. */ public class CursorInterceptorWiringTest { diff --git a/framework/src/test/java/org/tron/core/services/filter/GrpcInterceptorProbeTest.java b/framework/src/test/java/org/tron/core/services/filter/GrpcInterceptorProbeTest.java deleted file mode 100644 index 798c26dca86..00000000000 --- a/framework/src/test/java/org/tron/core/services/filter/GrpcInterceptorProbeTest.java +++ /dev/null @@ -1,379 +0,0 @@ -package org.tron.core.services.filter; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Metadata; -import io.grpc.Server; -import io.grpc.ServerBuilder; -import io.grpc.ServerCall; -import io.grpc.ServerCallHandler; -import io.grpc.ServerInterceptor; -import io.grpc.stub.StreamObserver; -import java.lang.reflect.Field; -import java.net.ServerSocket; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.tron.api.DatabaseGrpc; -import org.tron.api.DatabaseGrpc.DatabaseImplBase; -import org.tron.api.GrpcAPI.EmptyMessage; -import org.tron.core.db.Manager; -import org.tron.core.db2.core.Chainbase; -import org.tron.protos.Protocol.Block; - -/** - * Pins down the two gRPC properties {@link CursorServerInterceptor} relies on. - * - *

First, which of two registered interceptors ends up innermost, i.e. closest to the handler. - * This determines whether a cursor interceptor must be registered before or after the interceptors - * added by the base service class. - * - *

Second, whether {@code Listener.onHalfClose()} runs on the same thread as the handler. The - * cursor is a {@link ThreadLocal}, so a cursor set anywhere else has no effect on the read path and - * fails silently, serving HEAD data from a cursor port. - */ -public class GrpcInterceptorProbeTest { - - /** Phase and thread of every observed step, in occurrence order. */ - private static final List TRACE = new CopyOnWriteArrayList<>(); - - private static String handlerThread; - - private Server server; - private ManagedChannel channel; - private ExecutorService executor; - - /** Records its position in the call flow without altering behaviour. */ - private static class ProbeInterceptor implements ServerInterceptor { - - private final String tag; - - ProbeInterceptor(String tag) { - this.tag = tag; - } - - @Override - public ServerCall.Listener interceptCall( - ServerCall call, Metadata headers, ServerCallHandler next) { - record(tag, "interceptCall"); - return new SimpleForwardingServerCallListener(next.startCall(call, headers)) { - @Override - public void onHalfClose() { - record(tag, "onHalfClose-IN"); - super.onHalfClose(); - record(tag, "onHalfClose-OUT"); - } - }; - } - } - - private static void record(String tag, String phase) { - String line = String.format("%-14s %-16s thread=%s", - "[" + tag + "]", phase, Thread.currentThread().getName()); - TRACE.add(line); - System.out.println(line); - } - - /** Minimal synchronous unary service, matching the shape of the cursor-port services. */ - private static class ProbeDatabaseApi extends DatabaseImplBase { - @Override - public void getNowBlock(EmptyMessage request, StreamObserver observer) { - handlerThread = Thread.currentThread().getName(); - record("HANDLER", "execute"); - observer.onNext(Block.getDefaultInstance()); - observer.onCompleted(); - } - } - - @Before - public void setUp() throws Exception { - TRACE.clear(); - handlerThread = null; - int port = freePort(); - - // A fixed thread pool mirrors the production server configuration. - executor = Executors.newFixedThreadPool(2, r -> { - Thread t = new Thread(r); - t.setName("probe-rpc-executor-" + t.getId()); - return t; - }); - - server = ServerBuilder.forPort(port) - .executor(executor) - .addService(new ProbeDatabaseApi()) - .intercept(new ProbeInterceptor("A")) - .intercept(new ProbeInterceptor("B")) - .build() - .start(); - - channel = ManagedChannelBuilder.forAddress("127.0.0.1", port) - .usePlaintext().directExecutor().build(); - } - - @After - public void tearDown() throws Exception { - if (channel != null) { - channel.shutdownNow(); - } - if (server != null) { - server.shutdownNow(); - } - if (executor != null) { - executor.shutdown(); - if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { - executor.shutdownNow(); - } - } - } - - /** - * Registration order versus nesting depth. The interceptor whose {@code onHalfClose} runs later - * is the innermost one; the printed conclusion states where a cursor interceptor belongs. - */ - @Test - public void testInterceptorOrdering() { - DatabaseGrpc.newBlockingStub(channel).getNowBlock(EmptyMessage.getDefaultInstance()); - - int aIn = indexOf("[A]", "onHalfClose-IN"); - int bIn = indexOf("[B]", "onHalfClose-IN"); - Assert.assertTrue("no onHalfClose captured for A", aIn >= 0); - Assert.assertTrue("no onHalfClose captured for B", bIn >= 0); - - System.out.println("\n===== ordering ====="); - System.out.println("registered: intercept(A) then intercept(B)"); - System.out.println(bIn > aIn - ? "B (registered last) is innermost -> register the cursor interceptor LAST" - : "A (registered first) is innermost -> register the cursor interceptor FIRST"); - System.out.println("====================\n"); - - int handlerIdx = indexOf("[HANDLER]", "execute"); - Assert.assertTrue("handler did not run inside both interceptors", - handlerIdx > aIn && handlerIdx > bIn); - - // The innermost interceptor enters onHalfClose last. RpcApiServiceOnSolidity / - // RpcApiServiceOnPBFT register their cursor interceptor BEFORE super.addInterceptor(...) - // precisely because the first-registered one ends up innermost, wrapping the handler alone. - // Asserted, not merely printed: if a gRPC upgrade flips this, those two services silently - // start bracketing the other interceptors instead of the handler. - Assert.assertTrue( - "first-registered interceptor must be innermost; the cursor services depend on it", - aIn > bIn); - } - - /** - * Thread affinity between {@code onHalfClose} and the handler. They must coincide for a - * ThreadLocal cursor set in the interceptor to be visible to the read path. - */ - @Test - public void testThreadAffinity() { - DatabaseGrpc.newBlockingStub(channel).getNowBlock(EmptyMessage.getDefaultInstance()); - - String halfCloseThread = threadOf("[B]", "onHalfClose-IN"); - Assert.assertNotNull("no thread captured for onHalfClose", halfCloseThread); - Assert.assertNotNull("no thread captured for the handler", handlerThread); - - System.out.println("\n===== thread affinity ====="); - System.out.println("onHalfClose thread = " + halfCloseThread); - System.out.println("handler thread = " + handlerThread); - System.out.println(halfCloseThread.equals(handlerThread) - ? "same thread -> a cursor set in onHalfClose reaches the handler" - : "different threads -> a ThreadLocal cursor cannot reach the handler"); - System.out.println("===========================\n"); - - Assert.assertEquals( - "onHalfClose and the handler must share a thread for the ThreadLocal cursor to apply", - handlerThread, halfCloseThread); - } - - /** - * End-to-end check that a ThreadLocal written in {@code onHalfClose} is observable by the - * handler, which is exactly how the cursor reaches the read path. - */ - @Test - public void testThreadLocalPropagation() throws Exception { - final ThreadLocal probe = new ThreadLocal<>(); - final String[] seenByHandler = new String[1]; - - int port = freePort(); - Server s = ServerBuilder.forPort(port) - .executor(executor) - .addService(new DatabaseImplBase() { - @Override - public void getNowBlock(EmptyMessage req, StreamObserver obs) { - seenByHandler[0] = probe.get(); - obs.onNext(Block.getDefaultInstance()); - obs.onCompleted(); - } - }) - .intercept(new ServerInterceptor() { - @Override - public ServerCall.Listener interceptCall( - ServerCall call, Metadata headers, ServerCallHandler next) { - return new SimpleForwardingServerCallListener(next.startCall(call, headers)) { - @Override - public void onHalfClose() { - try { - probe.set("SET_BY_INTERCEPTOR"); - super.onHalfClose(); - } finally { - probe.remove(); - } - } - }; - } - }) - .build() - .start(); - - ManagedChannel ch = ManagedChannelBuilder.forAddress("127.0.0.1", port) - .usePlaintext().directExecutor().build(); - try { - DatabaseGrpc.newBlockingStub(ch).getNowBlock(EmptyMessage.getDefaultInstance()); - System.out.println("\n===== ThreadLocal propagation ====="); - System.out.println("value seen by handler = " + seenByHandler[0]); - System.out.println("===================================\n"); - - Assert.assertEquals( - "a ThreadLocal set in onHalfClose must be visible to the handler", - "SET_BY_INTERCEPTOR", seenByHandler[0]); - } finally { - ch.shutdownNow(); - s.shutdownNow(); - s.awaitTermination(5, TimeUnit.SECONDS); - } - } - - /** - * The production interceptor itself, not a stand-in: the cursor must be set on the thread that - * runs the handler and must be restored before the call ends, even when the handler throws. - */ - @Test - public void testProductionInterceptorSetsCursorOnTheHandlerThread() throws Exception { - final List setOn = new CopyOnWriteArrayList<>(); - final List resetOn = new CopyOnWriteArrayList<>(); - final String[] handlerOn = new String[1]; - - Manager manager = mock(Manager.class); - doAnswer(inv -> setOn.add(Thread.currentThread().getName())) - .when(manager).setCursor(any(Chainbase.Cursor.class)); - doAnswer(inv -> resetOn.add(Thread.currentThread().getName())) - .when(manager).resetCursor(); - - SolidityCursorInterceptor interceptor = new SolidityCursorInterceptor(); - Field dbManager = CursorServerInterceptor.class.getDeclaredField("dbManager"); - dbManager.setAccessible(true); - dbManager.set(interceptor, manager); - - int port = freePort(); - Server s = ServerBuilder.forPort(port) - .executor(executor) - .addService(new DatabaseImplBase() { - @Override - public void getNowBlock(EmptyMessage req, StreamObserver obs) { - handlerOn[0] = Thread.currentThread().getName(); - obs.onNext(Block.getDefaultInstance()); - obs.onCompleted(); - } - }) - .intercept(interceptor) - .build() - .start(); - - ManagedChannel ch = ManagedChannelBuilder.forAddress("127.0.0.1", port) - .usePlaintext().directExecutor().build(); - try { - DatabaseGrpc.newBlockingStub(ch).getNowBlock(EmptyMessage.getDefaultInstance()); - - Assert.assertEquals("cursor must be set exactly once per call", 1, setOn.size()); - Assert.assertEquals("cursor must be restored exactly once per call", 1, resetOn.size()); - Assert.assertNotNull("handler did not run", handlerOn[0]); - // the ThreadLocal cursor only reaches the read path if it is set on the handler's thread - Assert.assertEquals("cursor was set on a thread other than the handler's", - handlerOn[0], setOn.get(0)); - Assert.assertEquals("cursor was restored on a thread other than the handler's", - handlerOn[0], resetOn.get(0)); - verify(manager).setCursor(Chainbase.Cursor.SOLIDITY); - } finally { - ch.shutdownNow(); - s.shutdownNow(); - s.awaitTermination(5, TimeUnit.SECONDS); - } - } - - /** A handler that throws must still leave the cursor restored, or it leaks into the next call. */ - @Test - public void testProductionInterceptorRestoresCursorWhenHandlerThrows() throws Exception { - Manager manager = mock(Manager.class); - SolidityCursorInterceptor interceptor = new SolidityCursorInterceptor(); - Field dbManager = CursorServerInterceptor.class.getDeclaredField("dbManager"); - dbManager.setAccessible(true); - dbManager.set(interceptor, manager); - - int port = freePort(); - Server s = ServerBuilder.forPort(port) - .executor(executor) - .addService(new DatabaseImplBase() { - @Override - public void getNowBlock(EmptyMessage req, StreamObserver obs) { - throw new IllegalStateException("boom"); - } - }) - .intercept(interceptor) - .build() - .start(); - - ManagedChannel ch = ManagedChannelBuilder.forAddress("127.0.0.1", port) - .usePlaintext().directExecutor().build(); - try { - try { - DatabaseGrpc.newBlockingStub(ch).getNowBlock(EmptyMessage.getDefaultInstance()); - Assert.fail("expected the handler failure to surface"); - } catch (RuntimeException expected) { - // the call fails; what matters is the cursor below - } - verify(manager).setCursor(Chainbase.Cursor.SOLIDITY); - verify(manager).resetCursor(); - } finally { - ch.shutdownNow(); - s.shutdownNow(); - s.awaitTermination(5, TimeUnit.SECONDS); - } - } - - private static int freePort() throws Exception { - try (ServerSocket socket = new ServerSocket(0)) { - return socket.getLocalPort(); - } - } - - private int indexOf(String tag, String phase) { - for (int i = 0; i < TRACE.size(); i++) { - String line = TRACE.get(i); - if (line.contains(tag) && line.contains(phase)) { - return i; - } - } - return -1; - } - - private String threadOf(String tag, String phase) { - int i = indexOf(tag, phase); - if (i < 0) { - return null; - } - String line = TRACE.get(i); - return line.substring(line.indexOf("thread=") + "thread=".length()).trim(); - } -}