diff --git a/framework/src/main/java/org/tron/core/db/Manager.java b/framework/src/main/java/org/tron/core/db/Manager.java index 6ccd024091d..5906d609ce2 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -1033,23 +1033,6 @@ public void eraseBlock() { } } - public void pushVerifiedBlock(BlockCapsule block) throws ContractValidateException, - ContractExeException, ValidateSignatureException, AccountResourceInsufficientException, - TransactionExpirationException, TooBigTransactionException, DupTransactionException, - TaposException, ValidateScheduleException, ReceiptCheckErrException, - VMIllegalException, TooBigTransactionResultException, UnLinkedBlockException, - NonCommonBlockException, BadNumberBlockException, BadBlockException, ZksnarkException, - EventBloomException { - block.generatedByMyself = true; - long start = System.currentTimeMillis(); - pushBlock(block); - logger.info("Push block cost: {} ms, blockNum: {}, blockHash: {}, trx count: {}.", - System.currentTimeMillis() - start, - block.getNum(), - block.getBlockId(), - block.getTransactions().size()); - } - private void applyBlock(BlockCapsule block) throws ContractValidateException, ContractExeException, ValidateSignatureException, AccountResourceInsufficientException, TransactionExpirationException, TooBigTransactionException, DupTransactionException, diff --git a/framework/src/main/java/org/tron/core/net/TronNetDelegate.java b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java index 100bad179bf..b3fba9fa6a6 100644 --- a/framework/src/main/java/org/tron/core/net/TronNetDelegate.java +++ b/framework/src/main/java/org/tron/core/net/TronNetDelegate.java @@ -233,9 +233,25 @@ public Message getData(Sha256Hash hash, InventoryType type) throws P2pException } } + public void pushVerifiedBlock(BlockCapsule block) throws P2pException { + block.generatedByMyself = true; + long start = System.currentTimeMillis(); + processBlock(block, true); + if (!hitDown) { + logger.info("Push block cost: {} ms, blockNum: {}, blockHash: {}, trx count: {}.", + System.currentTimeMillis() - start, + block.getNum(), + block.getBlockId(), + block.getTransactions().size()); + } + } + public void processBlock(BlockCapsule block, boolean isSync) throws P2pException { + // Use <= rather than == because pushBlock may commit multiple blocks in a single + // batch write, causing the DB header number to jump past the target block number + // and never equal it exactly. if (!hitDown && dbManager.getLatestSolidityNumShutDown() > 0 - && dbManager.getLatestSolidityNumShutDown() == dbManager.getDynamicPropertiesStore() + && dbManager.getLatestSolidityNumShutDown() <= dbManager.getDynamicPropertiesStore() .getLatestBlockHeaderNumberFromDB()) { logger.info("Begin shutdown, currentBlockNum:{}, DbBlockNum:{}, solidifiedBlockNum:{}", diff --git a/framework/src/main/java/org/tron/program/FullNode.java b/framework/src/main/java/org/tron/program/FullNode.java index 95257d77f8e..a7d204de025 100644 --- a/framework/src/main/java/org/tron/program/FullNode.java +++ b/framework/src/main/java/org/tron/program/FullNode.java @@ -2,6 +2,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.util.ObjectUtils; import org.tron.common.application.Application; import org.tron.common.application.ApplicationFactory; import org.tron.common.application.TronApplicationContext; @@ -28,19 +29,24 @@ public static void main(String[] args) { LogService.load(parameter.getLogbackPath()); - if (parameter.isSolidityNode()) { - SolidityNode.start(); - return; - } if (parameter.isKeystoreFactory()) { KeystoreFactory.start(); return; } - logger.info("Full node running."); - if (Args.getInstance().isDebug()) { - logger.info("in debug mode, it won't check energy time"); + if (parameter.isSolidityNode()) { + logger.info("Solidity node is running."); + if (ObjectUtils.isEmpty(parameter.getTrustNodeAddr())) { + throw new TronError(new IllegalArgumentException("Trust node is not set."), + TronError.ErrCode.SOLID_NODE_INIT); + } + parameter.setP2pDisable(true); } else { - logger.info("not in debug mode, it will check energy time"); + logger.info("Full node running."); + if (Args.getInstance().isDebug()) { + logger.info("in debug mode, it won't check energy time"); + } else { + logger.info("not in debug mode, it will check energy time"); + } } // init metrics first @@ -55,6 +61,10 @@ public static void main(String[] args) { Application appT = ApplicationFactory.create(context); context.registerShutdownHook(); appT.startup(); + if (parameter.isSolidityNode()) { + SolidityNode node = context.getBean(SolidityNode.class); + node.run(); + } appT.blockUntilShutdown(); } diff --git a/framework/src/main/java/org/tron/program/SolidityNode.java b/framework/src/main/java/org/tron/program/SolidityNode.java index 6ffa3b3ce92..39739113f39 100644 --- a/framework/src/main/java/org/tron/program/SolidityNode.java +++ b/framework/src/main/java/org/tron/program/SolidityNode.java @@ -1,227 +1,225 @@ -package org.tron.program; - -import static org.tron.core.config.Parameter.ChainConstant.BLOCK_PRODUCED_INTERVAL; - -import com.google.common.annotations.VisibleForTesting; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.util.ObjectUtils; -import org.tron.common.application.Application; -import org.tron.common.application.ApplicationFactory; -import org.tron.common.application.TronApplicationContext; -import org.tron.common.client.DatabaseGrpcClient; -import org.tron.common.es.ExecutorServiceManager; -import org.tron.common.parameter.CommonParameter; -import org.tron.common.prometheus.Metrics; -import org.tron.core.ChainBaseManager; -import org.tron.core.capsule.BlockCapsule; -import org.tron.core.config.DefaultConfig; -import org.tron.core.db.Manager; -import org.tron.core.exception.TronError; -import org.tron.protos.Protocol.Block; - -@Slf4j(topic = "app") -public class SolidityNode { - - private Manager dbManager; - - private ChainBaseManager chainBaseManager; - - private DatabaseGrpcClient databaseGrpcClient; - - private AtomicLong ID = new AtomicLong(); - - private AtomicLong remoteBlockNum = new AtomicLong(); - - private LinkedBlockingDeque blockQueue = new LinkedBlockingDeque<>(100); - - private int exceptionSleepTime = 1000; - - private volatile boolean flag = true; - - private ExecutorService getBlockEs; - private ExecutorService processBlockEs; - - public SolidityNode(Manager dbManager) { - this.dbManager = dbManager; - this.chainBaseManager = dbManager.getChainBaseManager(); - resolveCompatibilityIssueIfUsingFullNodeDatabase(); - ID.set(chainBaseManager.getDynamicPropertiesStore().getLatestSolidifiedBlockNum()); - databaseGrpcClient = new DatabaseGrpcClient(CommonParameter.getInstance().getTrustNodeAddr()); - remoteBlockNum.set(getLastSolidityBlockNum()); - } - - /** - * Start the SolidityNode. - */ - public static void start() { - logger.info("Solidity node is running."); - CommonParameter parameter = CommonParameter.getInstance(); - if (ObjectUtils.isEmpty(parameter.getTrustNodeAddr())) { - throw new TronError(new IllegalArgumentException("Trust node is not set."), - TronError.ErrCode.SOLID_NODE_INIT); - } - // init metrics first - Metrics.init(); - - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.setAllowCircularReferences(false); - TronApplicationContext context = - new TronApplicationContext(beanFactory); - context.register(DefaultConfig.class); - context.refresh(); - Application appT = ApplicationFactory.create(context); - context.registerShutdownHook(); - appT.startup(); - SolidityNode node = new SolidityNode(appT.getDbManager()); - node.run(); - awaitShutdown(appT, node); - } - - @VisibleForTesting - static void awaitShutdown(Application appT, SolidityNode node) { - try { - appT.blockUntilShutdown(); - } finally { - // SolidityNode is created manually rather than managed by Spring/Application, - // so its executors must be shut down explicitly on exit. - node.shutdown(); - } - } - - private void run() { - try { - getBlockEs = ExecutorServiceManager.newSingleThreadExecutor("solid-get-block"); - processBlockEs = ExecutorServiceManager.newSingleThreadExecutor("solid-process-block"); - getBlockEs.execute(this::getBlock); - processBlockEs.execute(this::processBlock); - logger.info("Success to start solid node, ID: {}, remoteBlockNum: {}.", ID.get(), - remoteBlockNum); - } catch (Exception e) { - logger.error("Failed to start solid node, address: {}.", - CommonParameter.getInstance().getTrustNodeAddr()); - throw new TronError(e, TronError.ErrCode.SOLID_NODE_INIT); - } - } - - public void shutdown() { - flag = false; - // Signal both pools before awaiting either so they drain concurrently - getBlockEs.shutdown(); - processBlockEs.shutdown(); - ExecutorServiceManager.shutdownAndAwaitTermination(getBlockEs, "solid-get-block"); - ExecutorServiceManager.shutdownAndAwaitTermination(processBlockEs, "solid-process-block"); - } - - private void getBlock() { - long blockNum = ID.incrementAndGet(); - while (flag) { - try { - if (blockNum > remoteBlockNum.get()) { - sleep(BLOCK_PRODUCED_INTERVAL); - remoteBlockNum.set(getLastSolidityBlockNum()); - continue; - } - Block block = getBlockByNum(blockNum); - blockQueue.put(block); - blockNum = ID.incrementAndGet(); - } catch (Exception e) { - logger.error("Failed to get block {}, reason: {}.", blockNum, e.getMessage()); - sleep(exceptionSleepTime); - } - } - } - - private void processBlock() { - while (flag) { - try { - Block block = blockQueue.take(); - loopProcessBlock(block); - } catch (Exception e) { - logger.error(e.getMessage()); - sleep(exceptionSleepTime); - } - } - } - - private void loopProcessBlock(Block block) { - while (flag) { - long blockNum = block.getBlockHeader().getRawData().getNumber(); - try { - dbManager.pushVerifiedBlock(new BlockCapsule(block)); - chainBaseManager.getDynamicPropertiesStore().saveLatestSolidifiedBlockNum(blockNum); - logger - .info("Success to process block: {}, blockQueueSize: {}.", blockNum, blockQueue.size()); - return; - } catch (Exception e) { - logger.error("Failed to process block {}.", new BlockCapsule(block), e); - sleep(exceptionSleepTime); - block = getBlockByNum(blockNum); - } - } - } - - private Block getBlockByNum(long blockNum) { - while (flag) { - try { - long time = System.currentTimeMillis(); - Block block = databaseGrpcClient.getBlock(blockNum); - long num = block.getBlockHeader().getRawData().getNumber(); - if (num == blockNum) { - logger.info("Success to get block: {}, cost: {}ms.", - blockNum, System.currentTimeMillis() - time); - return block; - } else { - logger.warn("Get block id not the same , {}, {}.", num, blockNum); - sleep(exceptionSleepTime); - } - } catch (Exception e) { - logger.error("Failed to get block: {}, reason: {}.", blockNum, e.getMessage()); - sleep(exceptionSleepTime); - } - } - return null; - } - - private long getLastSolidityBlockNum() { - while (flag) { - try { - long time = System.currentTimeMillis(); - long blockNum = databaseGrpcClient.getDynamicProperties().getLastSolidityBlockNum(); - logger.info("Get last remote solid blockNum: {}, remoteBlockNum: {}, cost: {}.", - blockNum, remoteBlockNum, System.currentTimeMillis() - time); - return blockNum; - } catch (Exception e) { - logger.error("Failed to get last solid blockNum: {}, reason: {}.", remoteBlockNum.get(), - e.getMessage()); - sleep(exceptionSleepTime); - } - } - return 0; - } - - public void sleep(long time) { - try { - Thread.sleep(time); - } catch (Exception e1) { - logger.error(e1.getMessage()); - } - } - - private void resolveCompatibilityIssueIfUsingFullNodeDatabase() { - long lastSolidityBlockNum = - chainBaseManager.getDynamicPropertiesStore().getLatestSolidifiedBlockNum(); - long headBlockNum = chainBaseManager.getHeadBlockNum(); - logger.info("headBlockNum:{}, solidityBlockNum:{}, diff:{}", - headBlockNum, lastSolidityBlockNum, headBlockNum - lastSolidityBlockNum); - if (lastSolidityBlockNum < headBlockNum) { - logger.info("use fullNode database, headBlockNum:{}, solidityBlockNum:{}, diff:{}", - headBlockNum, lastSolidityBlockNum, headBlockNum - lastSolidityBlockNum); - chainBaseManager.getDynamicPropertiesStore().saveLatestSolidifiedBlockNum(headBlockNum); - } - } -} +package org.tron.program; + +import static org.tron.core.config.Parameter.ChainConstant.BLOCK_PRODUCED_INTERVAL; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.stereotype.Component; +import org.tron.common.client.DatabaseGrpcClient; +import org.tron.common.es.ExecutorServiceManager; +import org.tron.common.parameter.CommonParameter; +import org.tron.core.ChainBaseManager; +import org.tron.core.capsule.BlockCapsule; +import org.tron.core.config.args.Args; +import org.tron.core.exception.TronError; +import org.tron.core.net.TronNetDelegate; +import org.tron.protos.Protocol.Block; + +@Slf4j(topic = "app") +@Conditional(SolidityNode.SolidityCondition.class) +@Component +public class SolidityNode implements ApplicationListener { + + @Autowired + private ChainBaseManager chainBaseManager; + + @Autowired + private TronNetDelegate tronNetDelegate; + + private DatabaseGrpcClient databaseGrpcClient; + + private final AtomicLong ID = new AtomicLong(); + + private final AtomicLong remoteBlockNum = new AtomicLong(); + + private final LinkedBlockingDeque blockQueue = new LinkedBlockingDeque<>(100); + + private final int exceptionSleepTime = 1000; + + private volatile boolean flag = true; + + private final String getBlockName = "get-block"; + private final String processBlockName = "process-block"; + + private ExecutorService getBlockExecutor; + private ExecutorService processBlockExecutor; + + @PostConstruct + private void init() { + resolveCompatibilityIssueIfUsingFullNodeDatabase(); + ID.set(chainBaseManager.getDynamicPropertiesStore().getLatestSolidifiedBlockNum()); + getBlockExecutor = ExecutorServiceManager.newSingleThreadExecutor(getBlockName); + processBlockExecutor = ExecutorServiceManager.newSingleThreadExecutor(processBlockName); + } + + @Override + public void onApplicationEvent(@NonNull ContextClosedEvent event) { + flag = false; // invoke earlier than @PreDestroy + } + + @PreDestroy + private void shutdown() { + flag = false; + ExecutorServiceManager.shutdownAndAwaitTermination(getBlockExecutor, getBlockName); + ExecutorServiceManager.shutdownAndAwaitTermination(processBlockExecutor, processBlockName); + if (databaseGrpcClient != null) { + databaseGrpcClient.shutdown(); + } + } + + public void run() { + try { + databaseGrpcClient = new DatabaseGrpcClient(CommonParameter.getInstance().getTrustNodeAddr()); + remoteBlockNum.set(getLastSolidityBlockNum()); + + getBlockExecutor.submit(this::getBlock); + processBlockExecutor.submit(this::processSolidityBlock); + logger.info("Success to start solid node, ID: {}, remoteBlockNum: {}.", ID.get(), + remoteBlockNum); + } catch (Exception e) { + logger.error("Failed to start solid node, address: {}.", + CommonParameter.getInstance().getTrustNodeAddr()); + throw new TronError(e, TronError.ErrCode.SOLID_NODE_INIT); + } + } + + private void getBlock() { + long blockNum = ID.incrementAndGet(); + while (flag && !tronNetDelegate.isHitDown()) { + try { + if (blockNum > remoteBlockNum.get()) { + sleep(BLOCK_PRODUCED_INTERVAL); + remoteBlockNum.set(getLastSolidityBlockNum()); + continue; + } + Block block = getBlockByNum(blockNum); + blockQueue.put(block); + blockNum = ID.incrementAndGet(); + } catch (Exception e) { + logger.error("Failed to get block {}, reason: {}.", blockNum, e.getMessage()); + sleep(exceptionSleepTime); + } + } + } + + private void processSolidityBlock() { + while (flag && !tronNetDelegate.isHitDown()) { + try { + Block block = blockQueue.poll(exceptionSleepTime, TimeUnit.MILLISECONDS); + if (block == null) { + continue; + } + loopProcessBlock(block); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.info("processSolidityBlock interrupted."); + return; + } catch (Exception e) { + logger.error(e.getMessage()); + sleep(exceptionSleepTime); + } + } + } + + private void loopProcessBlock(Block block) { + while (flag) { + long blockNum = block.getBlockHeader().getRawData().getNumber(); + try { + tronNetDelegate.pushVerifiedBlock(new BlockCapsule(block)); + if (!tronNetDelegate.isHitDown()) { + chainBaseManager.getDynamicPropertiesStore().saveLatestSolidifiedBlockNum(blockNum); + logger.info("Success to process block: {}, blockQueueSize: {}.", + blockNum, blockQueue.size()); + } + return; + } catch (Exception e) { + logger.error("Failed to process block {}.", new BlockCapsule(block), e); + sleep(exceptionSleepTime); + block = getBlockByNum(blockNum); + } + } + } + + private Block getBlockByNum(long blockNum) { + while (flag) { + try { + long time = System.currentTimeMillis(); + Block block = databaseGrpcClient.getBlock(blockNum); + long num = block.getBlockHeader().getRawData().getNumber(); + if (num == blockNum) { + logger.info("Success to get block: {}, cost: {}ms.", + blockNum, System.currentTimeMillis() - time); + return block; + } else { + logger.warn("Get block id not the same , {}, {}.", num, blockNum); + sleep(exceptionSleepTime); + } + } catch (Exception e) { + logger.error("Failed to get block: {}, reason: {}.", blockNum, e.getMessage()); + sleep(exceptionSleepTime); + } + } + //throw RuntimeException instead of return null to avoid NullPointException + throw new RuntimeException("SolidityNode is closing."); + } + + private long getLastSolidityBlockNum() { + while (flag) { + try { + long time = System.currentTimeMillis(); + long blockNum = databaseGrpcClient.getDynamicProperties().getLastSolidityBlockNum(); + logger.info("Get last remote solid blockNum: {}, remoteBlockNum: {}, cost: {}.", + blockNum, remoteBlockNum, System.currentTimeMillis() - time); + return blockNum; + } catch (Exception e) { + logger.error("Failed to get last solid blockNum: {}, reason: {}.", remoteBlockNum.get(), + e.getMessage()); + sleep(exceptionSleepTime); + } + } + return 0; + } + + public void sleep(long time) { + try { + Thread.sleep(time); + } catch (Exception e1) { + logger.error(e1.getMessage()); + } + } + + private void resolveCompatibilityIssueIfUsingFullNodeDatabase() { + long lastSolidityBlockNum = + chainBaseManager.getDynamicPropertiesStore().getLatestSolidifiedBlockNum(); + long headBlockNum = chainBaseManager.getHeadBlockNum(); + logger.info("headBlockNum:{}, solidityBlockNum:{}, diff:{}", + headBlockNum, lastSolidityBlockNum, headBlockNum - lastSolidityBlockNum); + if (lastSolidityBlockNum < headBlockNum) { + logger.info("use fullNode database, headBlockNum:{}, solidityBlockNum:{}, diff:{}", + headBlockNum, lastSolidityBlockNum, headBlockNum - lastSolidityBlockNum); + chainBaseManager.getDynamicPropertiesStore().saveLatestSolidifiedBlockNum(headBlockNum); + } + } + + static class SolidityCondition implements Condition { + + @Override + public boolean matches(@NonNull ConditionContext context, + @NonNull AnnotatedTypeMetadata metadata) { + return Args.getInstance().isSolidityNode(); + } + } +} diff --git a/framework/src/test/java/org/tron/core/db/ManagerTest.java b/framework/src/test/java/org/tron/core/db/ManagerTest.java index a07fb291f34..4a6c9c612c9 100755 --- a/framework/src/test/java/org/tron/core/db/ManagerTest.java +++ b/framework/src/test/java/org/tron/core/db/ManagerTest.java @@ -310,12 +310,6 @@ public void transactionTest() { } catch (Exception e) { Assert.assertTrue(e instanceof TaposException); } - try { - dbManager.pushVerifiedBlock(chainManager.getHead()); - dbManager.getBlockChainHashesOnFork(chainManager.getHeadBlockId()); - } catch (Exception e) { - Assert.assertTrue(e instanceof TaposException); - } } @Test diff --git a/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java b/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java index 30659bde5d3..669912877e8 100644 --- a/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java +++ b/framework/src/test/java/org/tron/core/net/TronNetDelegateTest.java @@ -2,6 +2,7 @@ import static org.mockito.Mockito.mock; +import com.google.protobuf.ByteString; import java.lang.reflect.Field; import org.junit.Assert; import org.junit.Test; @@ -12,6 +13,8 @@ import org.tron.core.ChainBaseManager; import org.tron.core.capsule.BlockCapsule; import org.tron.core.config.args.Args; +import org.tron.core.db.Manager; +import org.tron.core.store.DynamicPropertiesStore; public class TronNetDelegateTest { @@ -49,4 +52,84 @@ public void test() throws Exception { Assert.assertTrue(!tronNetDelegate.isBlockUnsolidified()); } + + // ── pushVerifiedBlock tests ─────────────────────────────────────────────────── + + /** + * When hitDown is already true, processBlock returns immediately without + * calling pushBlock and pushVerifiedBlock must not throw. + */ + @Test + public void testPushVerifiedBlockSkipsWhenHitDown() throws Exception { + Args.setParam(new String[] {}, TestConstants.TEST_CONF); + TronNetDelegate tronNetDelegate = new TronNetDelegate(); + setField(tronNetDelegate, "hitDown", true); + + BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0L, ByteString.EMPTY); + tronNetDelegate.pushVerifiedBlock(block); + + Assert.assertTrue(block.generatedByMyself); + Assert.assertTrue(tronNetDelegate.isHitDown()); + } + + /** + * When the conditional-shutdown threshold is reached, processBlock must set + * hitDown=true and return without calling pushBlock. + */ + @Test + public void testPushVerifiedBlockTriggersShutdown() throws Exception { + Args.setParam(new String[] {}, TestConstants.TEST_CONF); + TronNetDelegate tronNetDelegate = new TronNetDelegate(); + tronNetDelegate.init(); + tronNetDelegate.setExit(false); // prevent System.exit(0) in hit-thread + + Manager dbManager = Mockito.mock(Manager.class); + Mockito.when(dbManager.getLatestSolidityNumShutDown()).thenReturn(50L); + DynamicPropertiesStore store = Mockito.mock(DynamicPropertiesStore.class); + Mockito.when(store.getLatestBlockHeaderNumberFromDB()).thenReturn(50L); + Mockito.when(dbManager.getDynamicPropertiesStore()).thenReturn(store); + setField(tronNetDelegate, "dbManager", dbManager); + + BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0L, ByteString.EMPTY); + try { + tronNetDelegate.pushVerifiedBlock(block); + } finally { + tronNetDelegate.close(); + } + + Assert.assertTrue(tronNetDelegate.isHitDown()); + Mockito.verify(dbManager, Mockito.never()).pushBlock(Mockito.any()); + } + + /** + * On the normal (non-shutdown) path pushBlock must be called exactly once. + */ + @Test + public void testPushVerifiedBlockPushesBlock() throws Exception { + Args.setParam(new String[] {}, TestConstants.TEST_CONF); + TronNetDelegate tronNetDelegate = new TronNetDelegate(); + + Manager dbManager = Mockito.mock(Manager.class); + Mockito.when(dbManager.getLatestSolidityNumShutDown()).thenReturn(0L); + Mockito.when(dbManager.getBlockedTimer()).thenReturn(new ThreadLocal<>()); + + ChainBaseManager chainBaseManager = Mockito.mock(ChainBaseManager.class); + Mockito.when(chainBaseManager.getHeadBlockId()) + .thenReturn(new BlockCapsule.BlockId(Sha256Hash.ZERO_HASH, 0L)); + + setField(tronNetDelegate, "dbManager", dbManager); + setField(tronNetDelegate, "chainBaseManager", chainBaseManager); + + BlockCapsule block = new BlockCapsule(1, Sha256Hash.ZERO_HASH, 0L, ByteString.EMPTY); + tronNetDelegate.pushVerifiedBlock(block); + + Assert.assertTrue(block.generatedByMyself); + Mockito.verify(dbManager, Mockito.times(1)).pushBlock(Mockito.any()); + } + + private static void setField(Object obj, String name, Object value) throws Exception { + Field f = obj.getClass().getDeclaredField(name); + f.setAccessible(true); + f.set(obj, value); + } } diff --git a/framework/src/test/java/org/tron/program/SolidityNodeTest.java b/framework/src/test/java/org/tron/program/SolidityNodeTest.java index f5b525cd445..a02eb22364e 100755 --- a/framework/src/test/java/org/tron/program/SolidityNodeTest.java +++ b/framework/src/test/java/org/tron/program/SolidityNodeTest.java @@ -1,34 +1,41 @@ package org.tron.program; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThrows; -import static org.mockito.Mockito.doCallRealMethod; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.inOrder; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import com.google.protobuf.ByteString; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import javax.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; -import org.mockito.InOrder; +import org.mockito.Mockito; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.core.type.AnnotatedTypeMetadata; import org.tron.common.BaseTest; import org.tron.common.TestConstants; -import org.tron.common.application.Application; import org.tron.common.client.DatabaseGrpcClient; -import org.tron.common.es.ExecutorServiceManager; +import org.tron.common.utils.ByteArray; import org.tron.common.utils.PublicMethod; +import org.tron.core.ChainBaseManager; import org.tron.core.config.args.Args; -import org.tron.core.exception.TronError; +import org.tron.core.net.TronNetDelegate; import org.tron.core.services.RpcApiService; import org.tron.core.services.http.solidity.SolidityNodeHttpApiService; +import org.tron.core.store.DynamicPropertiesStore; +import org.tron.protos.Protocol; import org.tron.protos.Protocol.Block; import org.tron.protos.Protocol.DynamicProperties; @@ -39,6 +46,9 @@ public class SolidityNodeTest extends BaseTest { RpcApiService rpcApiService; @Resource SolidityNodeHttpApiService solidityNodeHttpApiService; + @Resource + SolidityNode solidityNode; + static int rpcPort = PublicMethod.chooseRandomPort(); static int solidityHttpPort = PublicMethod.chooseRandomPort(); @@ -51,18 +61,22 @@ public class SolidityNodeTest extends BaseTest { Args.getInstance().setSolidityHttpPort(solidityHttpPort); } - @Test - public void testSolidityArgs() { - Assert.assertNotNull(Args.getInstance().getTrustNodeAddr()); - Assert.assertTrue(Args.getInstance().isSolidityNode()); - String trustNodeAddr = Args.getInstance().getTrustNodeAddr(); - Args.getInstance().setTrustNodeAddr(null); - TronError thrown = assertThrows(TronError.class, - SolidityNode::start); - assertEquals(TronError.ErrCode.SOLID_NODE_INIT, thrown.getErrCode()); - Args.getInstance().setTrustNodeAddr(trustNodeAddr); + // ── helpers ────────────────────────────────────────────────────────────────── + + private boolean getFlag() throws Exception { + Field f = SolidityNode.class.getDeclaredField("flag"); + f.setAccessible(true); + return (boolean) f.get(solidityNode); } + private void setFlag(boolean value) throws Exception { + Field f = SolidityNode.class.getDeclaredField("flag"); + f.setAccessible(true); + f.set(solidityNode, value); + } + + // ── existing tests ──────────────────────────────────────────────────────────── + @Test public void testSolidityGrpcCall() { rpcApiService.start(); @@ -101,93 +115,541 @@ public void testSolidityNodeHttpApiService() { Assert.assertTrue(true); } - @Test - public void testAwaitShutdownAlwaysStopsNode() { - Application app = mock(Application.class); - SolidityNode node = mock(SolidityNode.class); + // ── new tests ───────────────────────────────────────────────────────────────── - SolidityNode.awaitShutdown(app, node); + /** + * @PostConstruct init() must create both executor services before run() is called. + */ + @Test + public void testExecutorsInitializedOnStartup() throws Exception { + Field getBlockF = SolidityNode.class.getDeclaredField("getBlockExecutor"); + getBlockF.setAccessible(true); + Field processBlockF = SolidityNode.class.getDeclaredField("processBlockExecutor"); + processBlockF.setAccessible(true); - InOrder inOrder = inOrder(app, node); - inOrder.verify(app).blockUntilShutdown(); - inOrder.verify(node).shutdown(); + assertNotNull(getBlockF.get(solidityNode)); + assertNotNull(processBlockF.get(solidityNode)); + assertFalse(((ExecutorService) getBlockF.get(solidityNode)).isShutdown()); + assertFalse(((ExecutorService) processBlockF.get(solidityNode)).isShutdown()); } + /** + * onApplicationEvent() must set flag=false so threads stop before + * other beans' @PreDestroy methods are called. + */ @Test - public void testAwaitShutdownStopsNodeWhenBlockedCallFails() { - Application app = mock(Application.class); - SolidityNode node = mock(SolidityNode.class); - RuntimeException expected = new RuntimeException("boom"); - doThrow(expected).when(app).blockUntilShutdown(); + public void testOnApplicationEventSetsFlagFalse() throws Exception { + assertTrue(getFlag()); + solidityNode.onApplicationEvent(mock(ContextClosedEvent.class)); + assertFalse(getFlag()); + setFlag(true); // restore shared bean + } - RuntimeException thrown = assertThrows(RuntimeException.class, - () -> SolidityNode.awaitShutdown(app, node)); - assertSame(expected, thrown); + /** + * getBlockByNum() must throw RuntimeException (not return null) when + * flag=false, to prevent NullPointerException in blockQueue.put(). + */ + @Test(timeout = 1000) + public void testGetBlockByNumThrowsWhenClosed() throws Exception { + setFlag(false); + try { + Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class); + m.setAccessible(true); + try { + m.invoke(solidityNode, 1L); + Assert.fail("Expected RuntimeException"); + } catch (InvocationTargetException e) { + assertTrue(e.getCause() instanceof RuntimeException); + assertEquals("SolidityNode is closing.", e.getCause().getMessage()); + } + } finally { + setFlag(true); + } + } - InOrder inOrder = inOrder(app, node); - inOrder.verify(app).blockUntilShutdown(); - inOrder.verify(node).shutdown(); + /** + * getLastSolidityBlockNum() must return 0 (not throw) when flag=false so + * getBlock()'s while(flag) loop exits quietly without a misleading error log. + */ + @Test(timeout = 1000) + public void testGetLastSolidityBlockNumReturnsZeroWhenClosed() throws Exception { + setFlag(false); + try { + Method m = SolidityNode.class.getDeclaredMethod("getLastSolidityBlockNum"); + m.setAccessible(true); + long result = (long) m.invoke(solidityNode); + assertEquals(0L, result); + } finally { + setFlag(true); + } } + /** + * SolidityCondition must match when --solidity is passed so the bean is + * registered in the Spring context. + */ @Test - public void testShutdownSetsFlagAndShutsDownExecutors() throws Exception { - SolidityNode node = mock(SolidityNode.class); - doCallRealMethod().when(node).shutdown(); + public void testSolidityConditionMatchesWhenSolidityFlagSet() { + assertTrue(Args.getInstance().isSolidityNode()); + SolidityNode.SolidityCondition condition = new SolidityNode.SolidityCondition(); + assertTrue(condition.matches( + mock(ConditionContext.class), + mock(AnnotatedTypeMetadata.class))); + } - ExecutorService es1 = ExecutorServiceManager.newSingleThreadExecutor("test-solid-get"); - ExecutorService es2 = ExecutorServiceManager.newSingleThreadExecutor("test-solid-process"); + // ── additional coverage tests ───────────────────────────────────────────────── - Field flagField = SolidityNode.class.getDeclaredField("flag"); - flagField.setAccessible(true); - flagField.set(node, true); + /** + * sleep() must return normally without throwing. + */ + @Test(timeout = 1000) + public void testSleepReturnsNormally() { + solidityNode.sleep(1); + } + + /** + * sleep() must swallow InterruptedException so callers are not surprised; + * the thread continues after waking. + */ + @Test(timeout = 5000) + public void testSleepHandlesInterrupt() throws InterruptedException { + Thread t = new Thread(() -> solidityNode.sleep(10_000)); + t.start(); + Thread.sleep(50); + t.interrupt(); + t.join(2000); + assertFalse("sleep() should have returned after interrupt", t.isAlive()); + } - Field getBlockEsField = SolidityNode.class.getDeclaredField("getBlockEs"); - getBlockEsField.setAccessible(true); - getBlockEsField.set(node, es1); + /** + * getBlockByNum() must return the block when the gRPC client returns a block + * whose number matches the requested number. + */ + @Test(timeout = 2000) + public void testGetBlockByNumReturnsMatchingBlock() throws Exception { + Block expected = blockWithNum(7L); + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + Mockito.when(mockClient.getBlock(7L)).thenReturn(expected); - Field processBlockEsField = SolidityNode.class.getDeclaredField("processBlockEs"); - processBlockEsField.setAccessible(true); - processBlockEsField.set(node, es2); + Field clientField = getField("databaseGrpcClient"); + Object orig = clientField.get(solidityNode); + clientField.set(solidityNode, mockClient); + try { + Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class); + m.setAccessible(true); + Block result = (Block) m.invoke(solidityNode, 7L); + assertEquals(7L, result.getBlockHeader().getRawData().getNumber()); + } finally { + clientField.set(solidityNode, orig); + } + } - node.shutdown(); + /** + * getLastSolidityBlockNum() must return the value obtained from the gRPC + * client when the call succeeds. + */ + @Test(timeout = 2000) + public void testGetLastSolidityBlockNumReturnsFetchedValue() throws Exception { + DynamicProperties props = DynamicProperties.newBuilder() + .setLastSolidityBlockNum(99L).build(); + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + Mockito.when(mockClient.getDynamicProperties()).thenReturn(props); - Assert.assertFalse((boolean) flagField.get(node)); - Assert.assertTrue(es1.isShutdown()); - Assert.assertTrue(es2.isShutdown()); + Field clientField = getField("databaseGrpcClient"); + Object orig = clientField.get(solidityNode); + clientField.set(solidityNode, mockClient); + try { + Method m = SolidityNode.class.getDeclaredMethod("getLastSolidityBlockNum"); + m.setAccessible(true); + long result = (long) m.invoke(solidityNode); + assertEquals(99L, result); + } finally { + clientField.set(solidityNode, orig); + } } + /** + * loopProcessBlock() must persist the solidified block num when pushVerifiedBlock + * succeeds and hitDown is false. + */ + @Test(timeout = 5000) + public void testLoopProcessBlockSavesBlockNumWhenNotHitDown() throws Exception { + TronNetDelegate mockDelegate = mock(TronNetDelegate.class); + Mockito.when(mockDelegate.isHitDown()).thenReturn(false); + + long origSolidified = chainBaseManager.getDynamicPropertiesStore() + .getLatestSolidifiedBlockNum(); + Field delegateField = getField("tronNetDelegate"); + Object origDelegate = delegateField.get(solidityNode); + delegateField.set(solidityNode, mockDelegate); + try { + invokeLoopProcessBlock(blockWithNum(55L)); + assertEquals(55L, chainBaseManager.getDynamicPropertiesStore() + .getLatestSolidifiedBlockNum()); + } finally { + chainBaseManager.getDynamicPropertiesStore() + .saveLatestSolidifiedBlockNum(origSolidified); + delegateField.set(solidityNode, origDelegate); + } + } + + /** + * loopProcessBlock() must NOT persist the solidified block num when hitDown + * is true, because the block was never pushed to BlockStore. + */ + @Test(timeout = 2000) + public void testLoopProcessBlockSkipsSaveWhenHitDown() throws Exception { + TronNetDelegate mockDelegate = mock(TronNetDelegate.class); + Mockito.when(mockDelegate.isHitDown()).thenReturn(true); + + long origSolidified = chainBaseManager.getDynamicPropertiesStore() + .getLatestSolidifiedBlockNum(); + Field delegateField = getField("tronNetDelegate"); + Object origDelegate = delegateField.get(solidityNode); + delegateField.set(solidityNode, mockDelegate); + try { + invokeLoopProcessBlock(blockWithNum(56L)); + assertEquals(origSolidified, chainBaseManager.getDynamicPropertiesStore() + .getLatestSolidifiedBlockNum()); + } finally { + delegateField.set(solidityNode, origDelegate); + } + } + + /** + * resolveCompatibilityIssueIfUsingFullNodeDatabase() must update the solidified + * block num to match headBlockNum when solidity lags behind. + */ + @Test(timeout = 2000) + public void testResolveCompatibilityIssueWhenSolidityLagsHead() throws Exception { + DynamicPropertiesStore mockStore = mock(DynamicPropertiesStore.class); + Mockito.when(mockStore.getLatestSolidifiedBlockNum()).thenReturn(3L); + ChainBaseManager mockCbm = mock(ChainBaseManager.class); + Mockito.when(mockCbm.getDynamicPropertiesStore()).thenReturn(mockStore); + Mockito.when(mockCbm.getHeadBlockNum()).thenReturn(10L); + + Field cbmField = getField("chainBaseManager"); + Object orig = cbmField.get(solidityNode); + cbmField.set(solidityNode, mockCbm); + try { + Method m = SolidityNode.class.getDeclaredMethod( + "resolveCompatibilityIssueIfUsingFullNodeDatabase"); + m.setAccessible(true); + m.invoke(solidityNode); + } finally { + cbmField.set(solidityNode, orig); + } + Mockito.verify(mockStore).saveLatestSolidifiedBlockNum(10L); + } + + // ── shutdown / databaseGrpcClient lifecycle ────────────────────────────────── + + /** + * When databaseGrpcClient is non-null at shutdown time, its shutdown() must + * be called to close the gRPC channel. + */ @Test - public void testRunInitializesNamedExecutors() throws Exception { - rpcApiService.start(); - String originalAddr = Args.getInstance().getTrustNodeAddr(); - Args.getInstance().setTrustNodeAddr("127.0.0.1:" + rpcPort); + public void testShutdownCallsDatabaseClientShutdown() throws Exception { + // Use a standalone instance so we don't destroy the shared Spring executor services. + SolidityNode node = new SolidityNode(); + + DynamicPropertiesStore mockStore = mock(DynamicPropertiesStore.class); + ChainBaseManager mockCbm = mock(ChainBaseManager.class); + Mockito.when(mockCbm.getDynamicPropertiesStore()).thenReturn(mockStore); + Mockito.when(mockCbm.getHeadBlockNum()).thenReturn(0L); + getField("chainBaseManager").set(node, mockCbm); + + Method initM = SolidityNode.class.getDeclaredMethod("init"); + initM.setAccessible(true); + initM.invoke(node); + + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + getField("databaseGrpcClient").set(node, mockClient); + + Method shutdownM = SolidityNode.class.getDeclaredMethod("shutdown"); + shutdownM.setAccessible(true); + shutdownM.invoke(node); + + Mockito.verify(mockClient).shutdown(); + } + + // ── getBlock() ─────────────────────────────────────────────────────────────── + + /** + * getBlock() must fetch a block via gRPC, place it in blockQueue, then exit + * when flag becomes false after the first successful fetch. + */ + @Test(timeout = 5000) + @SuppressWarnings("unchecked") + public void testGetBlockProcessesOneBlock() throws Exception { + long origID = atomicLong("ID").get(); + long origRemote = atomicLong("remoteBlockNum").get(); + + atomicLong("ID").set(0L); + atomicLong("remoteBlockNum").set(2L); // blockNum=1 <= 2, no sleep needed + + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + Mockito.when(mockClient.getBlock(1L)).thenAnswer(inv -> { + setFlag(false); // stop the loop after this iteration + return blockWithNum(1L); + }); + + TronNetDelegate mockDelegate = mock(TronNetDelegate.class); + Mockito.when(mockDelegate.isHitDown()).thenReturn(false); + + Field clientField = getField("databaseGrpcClient"); + Field delegateField = getField("tronNetDelegate"); + Object origClient = clientField.get(solidityNode); + Object origDelegate = delegateField.get(solidityNode); + clientField.set(solidityNode, mockClient); + delegateField.set(solidityNode, mockDelegate); + + LinkedBlockingDeque queue = + (LinkedBlockingDeque) getField("blockQueue").get(solidityNode); + try { + Method m = SolidityNode.class.getDeclaredMethod("getBlock"); + m.setAccessible(true); + m.invoke(solidityNode); + + assertEquals(1, queue.size()); + assertEquals(1L, queue.peek().getBlockHeader().getRawData().getNumber()); + } finally { + setFlag(true); + queue.clear(); + atomicLong("ID").set(origID); + atomicLong("remoteBlockNum").set(origRemote); + clientField.set(solidityNode, origClient); + delegateField.set(solidityNode, origDelegate); + } + } + + // ── processSolidityBlock() ─────────────────────────────────────────────────── + + /** + * processSolidityBlock() must drain a block from the queue, process it, and + * exit when flag becomes false inside pushVerifiedBlock. + */ + @Test(timeout = 5000) + @SuppressWarnings("unchecked") + public void testProcessSolidityBlockProcessesQueuedBlock() throws Exception { + TronNetDelegate mockDelegate = mock(TronNetDelegate.class); + Mockito.when(mockDelegate.isHitDown()).thenReturn(false); + Mockito.doAnswer(inv -> { + setFlag(false); + return null; + }).when(mockDelegate).pushVerifiedBlock(Mockito.any()); + + long origSolidified = chainBaseManager.getDynamicPropertiesStore() + .getLatestSolidifiedBlockNum(); + Field delegateField = getField("tronNetDelegate"); + Object origDelegate = delegateField.get(solidityNode); + delegateField.set(solidityNode, mockDelegate); + + LinkedBlockingDeque queue = + (LinkedBlockingDeque) getField("blockQueue").get(solidityNode); + queue.put(blockWithNum(88L)); + try { + Method m = SolidityNode.class.getDeclaredMethod("processSolidityBlock"); + m.setAccessible(true); + m.invoke(solidityNode); + + assertEquals(88L, chainBaseManager.getDynamicPropertiesStore() + .getLatestSolidifiedBlockNum()); + } finally { + setFlag(true); + queue.clear(); + chainBaseManager.getDynamicPropertiesStore() + .saveLatestSolidifiedBlockNum(origSolidified); + delegateField.set(solidityNode, origDelegate); + } + } + + /** + * processSolidityBlock() must return cleanly when the thread is interrupted + * while waiting on blockQueue.poll(). + */ + @Test(timeout = 8000) + public void testProcessSolidityBlockHandlesInterrupt() throws Exception { + TronNetDelegate mockDelegate = mock(TronNetDelegate.class); + Mockito.when(mockDelegate.isHitDown()).thenReturn(false); + + Field delegateField = getField("tronNetDelegate"); + Object origDelegate = delegateField.get(solidityNode); + delegateField.set(solidityNode, mockDelegate); + + Method m = SolidityNode.class.getDeclaredMethod("processSolidityBlock"); + m.setAccessible(true); + Thread t = new Thread(() -> { + try { + m.invoke(solidityNode); + } catch (Exception ignored) { + // InvocationTargetException should not happen; the method handles interrupt internally + } + }); + try { + t.start(); + Thread.sleep(150); // let the thread enter blockQueue.poll(1000 ms) + t.interrupt(); + t.join(5000); + assertFalse("processSolidityBlock must exit after interrupt", t.isAlive()); + } finally { + setFlag(true); + delegateField.set(solidityNode, origDelegate); + } + } + + // ── loopProcessBlock() retry path ──────────────────────────────────────────── + + /** + * When pushVerifiedBlock throws, loopProcessBlock() must retry after sleeping, + * re-fetching the block via getBlockByNum, and ultimately succeed. + */ + @Test(timeout = 5000) + public void testLoopProcessBlockRetriesOnException() throws Exception { + TronNetDelegate mockDelegate = mock(TronNetDelegate.class); + Mockito.when(mockDelegate.isHitDown()).thenReturn(false); + Mockito.doThrow(new RuntimeException("push failed")) + .doNothing() + .when(mockDelegate).pushVerifiedBlock(Mockito.any()); + + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + Mockito.when(mockClient.getBlock(33L)).thenReturn(blockWithNum(33L)); + + long origSolidified = chainBaseManager.getDynamicPropertiesStore() + .getLatestSolidifiedBlockNum(); + Field delegateField = getField("tronNetDelegate"); + Field clientField = getField("databaseGrpcClient"); + Object origDelegate = delegateField.get(solidityNode); + Object origClient = clientField.get(solidityNode); + delegateField.set(solidityNode, mockDelegate); + clientField.set(solidityNode, mockClient); try { - SolidityNode node = new SolidityNode(dbManager); + invokeLoopProcessBlock(blockWithNum(33L)); + assertEquals(33L, chainBaseManager.getDynamicPropertiesStore() + .getLatestSolidifiedBlockNum()); + } catch (RuntimeException e) { + Assert.assertTrue(e.getMessage().contains("push failed")); + } finally { + chainBaseManager.getDynamicPropertiesStore() + .saveLatestSolidifiedBlockNum(origSolidified); + delegateField.set(solidityNode, origDelegate); + clientField.set(solidityNode, origClient); + } + } - Field flagField = SolidityNode.class.getDeclaredField("flag"); - flagField.setAccessible(true); - flagField.set(node, false); + // ── getBlockByNum() retry paths ────────────────────────────────────────────── - Method runMethod = SolidityNode.class.getDeclaredMethod("run"); - runMethod.setAccessible(true); - runMethod.invoke(node); + /** + * When the returned block number does not match, getBlockByNum() must warn + * and retry; it must throw RuntimeException when flag becomes false. + */ + @Test(timeout = 5000) + public void testGetBlockByNumWarnOnWrongNum() throws Exception { + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + Mockito.when(mockClient.getBlock(9L)).thenAnswer(inv -> { + setFlag(false); // cause the retry loop to exit + return blockWithNum(999L); // deliberately wrong number + }); - Field getBlockEsField = SolidityNode.class.getDeclaredField("getBlockEs"); - getBlockEsField.setAccessible(true); - Field processBlockEsField = SolidityNode.class.getDeclaredField("processBlockEs"); - processBlockEsField.setAccessible(true); + Field clientField = getField("databaseGrpcClient"); + Object orig = clientField.get(solidityNode); + clientField.set(solidityNode, mockClient); + try { + Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class); + m.setAccessible(true); + try { + m.invoke(solidityNode, 9L); + Assert.fail("Expected RuntimeException"); + } catch (InvocationTargetException e) { + assertTrue(e.getCause() instanceof RuntimeException); + } + } finally { + setFlag(true); + clientField.set(solidityNode, orig); + } + } - ExecutorService getBlockEs = (ExecutorService) getBlockEsField.get(node); - ExecutorService processBlockEs = (ExecutorService) processBlockEsField.get(node); + /** + * When the gRPC call throws, getBlockByNum() must log, sleep, and retry; + * on the second attempt it must return the correct block. + */ + @Test(timeout = 5000) + public void testGetBlockByNumRetriesOnException() throws Exception { + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + Mockito.when(mockClient.getBlock(8L)) + .thenThrow(new RuntimeException("rpc error")) + .thenReturn(blockWithNum(8L)); - Assert.assertNotNull(getBlockEs); - Assert.assertNotNull(processBlockEs); + Field clientField = getField("databaseGrpcClient"); + Object orig = clientField.get(solidityNode); + clientField.set(solidityNode, mockClient); + try { + Method m = SolidityNode.class.getDeclaredMethod("getBlockByNum", long.class); + m.setAccessible(true); + Block result = (Block) m.invoke(solidityNode, 8L); + assertEquals(8L, result.getBlockHeader().getRawData().getNumber()); + } finally { + clientField.set(solidityNode, orig); + } + } + + // ── getLastSolidityBlockNum() retry path ───────────────────────────────────── - ExecutorServiceManager.shutdownAndAwaitTermination(getBlockEs, "test-solid-get"); - ExecutorServiceManager.shutdownAndAwaitTermination(processBlockEs, "test-solid-process"); + /** + * When getDynamicProperties() throws, getLastSolidityBlockNum() must log, + * sleep, and retry; on the second attempt it must return the fetched value. + */ + @Test(timeout = 5000) + public void testGetLastSolidityBlockNumRetriesOnException() throws Exception { + DynamicProperties props = DynamicProperties.newBuilder() + .setLastSolidityBlockNum(50L).build(); + DatabaseGrpcClient mockClient = mock(DatabaseGrpcClient.class); + Mockito.when(mockClient.getDynamicProperties()) + .thenThrow(new RuntimeException("rpc error")) + .thenReturn(props); + + Field clientField = getField("databaseGrpcClient"); + Object orig = clientField.get(solidityNode); + clientField.set(solidityNode, mockClient); + try { + Method m = SolidityNode.class.getDeclaredMethod("getLastSolidityBlockNum"); + m.setAccessible(true); + long result = (long) m.invoke(solidityNode); + assertEquals(50L, result); } finally { - Args.getInstance().setTrustNodeAddr(originalAddr); - rpcApiService.stop(); + clientField.set(solidityNode, orig); } } + + // ── private helpers ────────────────────────────────────────────────────────── + + private static Field getField(String name) throws Exception { + Field f = SolidityNode.class.getDeclaredField(name); + f.setAccessible(true); + return f; + } + + private AtomicLong atomicLong(String name) throws Exception { + return (AtomicLong) getField(name).get(solidityNode); + } + + private static Block blockWithNum(long num) { + return Block.newBuilder() + .setBlockHeader( + Protocol.BlockHeader.newBuilder() + .setRawData( + Protocol.BlockHeader.raw.newBuilder() + .setNumber(num) + .setParentHash(ByteString.copyFrom(ByteArray.fromHexString( + "0x0000000000000000000000000000000000000000000000000000000000000000"))) + .build()) + .build()) + .build(); + } + + private void invokeLoopProcessBlock(Block block) throws Exception { + Method m = SolidityNode.class.getDeclaredMethod("loopProcessBlock", Block.class); + m.setAccessible(true); + m.invoke(solidityNode, block); + } }