diff --git a/framework/src/main/java/org/tron/common/logsfilter/capsule/ContractTriggerCapsule.java b/framework/src/main/java/org/tron/common/logsfilter/capsule/ContractTriggerCapsule.java index cc5b2ce473c..3ebb5102bba 100644 --- a/framework/src/main/java/org/tron/common/logsfilter/capsule/ContractTriggerCapsule.java +++ b/framework/src/main/java/org/tron/common/logsfilter/capsule/ContractTriggerCapsule.java @@ -135,11 +135,11 @@ public void processTrigger() { EventPluginLoader.getInstance().postContractEventTrigger((ContractEventTrigger) event); } - if (EventPluginLoader.getInstance().isSolidityEventTriggerEnable()) { + if (EventPluginLoader.getInstance().isSolidityEventTriggerEnable() + && !contractTrigger.isRemoved()) { boolean result = Args.getSolidityContractEventTriggerMap().computeIfAbsent(event .getBlockNumber(), listBlk -> new LinkedBlockingQueue()) .offer((ContractEventTrigger) event); - if (!result) { logger.info("too many triggers, solidity event trigger lost: {}", event.getUniqueId()); @@ -159,11 +159,11 @@ public void processTrigger() { EventPluginLoader.getInstance().postContractLogTrigger(logTrigger); } - if (EventPluginLoader.getInstance().isSolidityLogTriggerRedundancy()) { + if (EventPluginLoader.getInstance().isSolidityLogTriggerRedundancy() + && !contractTrigger.isRemoved()) { boolean result = Args.getSolidityContractLogTriggerMap().computeIfAbsent(event .getBlockNumber(), listBlk -> new LinkedBlockingQueue()) .offer(logTrigger); - if (!result) { logger.info("too many triggers, solidity log trigger lost: {}", logTrigger.getUniqueId()); @@ -175,11 +175,11 @@ public void processTrigger() { EventPluginLoader.getInstance().postContractLogTrigger((ContractLogTrigger) event); } - if (EventPluginLoader.getInstance().isSolidityLogTriggerEnable()) { + if (EventPluginLoader.getInstance().isSolidityLogTriggerEnable() + && !contractTrigger.isRemoved()) { boolean result = Args.getSolidityContractLogTriggerMap().computeIfAbsent(event .getBlockNumber(), listBlk -> new LinkedBlockingQueue()) .offer((ContractLogTrigger) event); - if (!result) { logger.info("too many triggers, solidity log trigger lost: {}", event.getUniqueId()); 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 cd1a61c01fe..e1d1a195337 100644 --- a/framework/src/main/java/org/tron/core/db/Manager.java +++ b/framework/src/main/java/org/tron/core/db/Manager.java @@ -1377,6 +1377,7 @@ public void pushBlock(final BlockCapsule block) } catch (Throwable throwable) { logger.error(throwable.getMessage(), throwable); khaosDb.removeBlk(block.getBlockId()); + clearSolidityContractTriggerCache(block.getNum()); throw throwable; } long newSolidNum = getDynamicPropertiesStore().getLatestSolidifiedBlockNum(); @@ -2378,6 +2379,16 @@ private void reOrgContractTrigger() { getDynamicPropertiesStore().getLatestBlockHeaderHash()); } } + clearSolidityContractTriggerCache(getHeadBlockNum()); + } + + private void clearSolidityContractTriggerCache(long blockNum) { + if (eventPluginLoaded + && (EventPluginLoader.getInstance().isSolidityEventTriggerEnable() + || EventPluginLoader.getInstance().isSolidityLogTriggerEnable())) { + Args.getSolidityContractLogTriggerMap().remove(blockNum); + Args.getSolidityContractEventTriggerMap().remove(blockNum); + } } private void postContractTrigger(final TransactionTrace trace, boolean remove, String blockHash) { @@ -2397,9 +2408,14 @@ private void postContractTrigger(final TransactionTrace trace, boolean remove, S .getLatestSolidifiedBlockNum()); contractTriggerCapsule.setBlockHash(blockHash); - if (!triggerCapsuleQueue.offer(contractTriggerCapsule)) { - logger.info("Too many triggers, contract log trigger lost: {}.", - trigger.getTransactionId()); + // Process synchronously to avoid race condition between async queue and + // reOrgContractTrigger cache clearing. Performance is not impacted because + // processTrigger() only enqueues events into the plugin's internal queue + // without blocking on actual I/O. + try { + contractTriggerCapsule.processTrigger(); + } catch (Throwable throwable) { + logger.warn("Post contract trigger failed.", throwable); } } } diff --git a/framework/src/test/java/org/tron/common/logsfilter/capsule/ContractTriggerCapsuleTest.java b/framework/src/test/java/org/tron/common/logsfilter/capsule/ContractTriggerCapsuleTest.java index 898447b3a75..14b86510fea 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/capsule/ContractTriggerCapsuleTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/capsule/ContractTriggerCapsuleTest.java @@ -3,8 +3,11 @@ import static com.google.common.collect.Lists.newArrayList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.beust.jcommander.internal.Lists; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import lombok.extern.slf4j.Slf4j; @@ -12,9 +15,12 @@ import org.apache.commons.lang3.ArrayUtils; import org.junit.Before; import org.junit.Test; +import org.tron.common.logsfilter.EventPluginLoader; +import org.tron.common.logsfilter.trigger.ContractLogTrigger; import org.tron.common.logsfilter.trigger.ContractTrigger; import org.tron.common.runtime.vm.DataWord; import org.tron.common.runtime.vm.LogInfo; +import org.tron.core.config.args.Args; @Slf4j public class ContractTriggerCapsuleTest { @@ -58,6 +64,45 @@ public void testSetAndGetContractTrigger() { } } + @Test + public void testRemovedTriggerNotWrittenToSolidityMap() throws Exception { + Args.getSolidityContractLogTriggerMap().clear(); + Args.getSolidityContractEventTriggerMap().clear(); + + EventPluginLoader mockLoader = mock(EventPluginLoader.class); + when(mockLoader.isSolidityLogTriggerEnable()).thenReturn(true); + when(mockLoader.isSolidityEventTriggerEnable()).thenReturn(false); + when(mockLoader.isContractLogTriggerEnable()).thenReturn(false); + when(mockLoader.isContractEventTriggerEnable()).thenReturn(false); + when(mockLoader.isSolidityLogTriggerRedundancy()).thenReturn(false); + when(mockLoader.isContractLogTriggerRedundancy()).thenReturn(false); + + Field instanceField = EventPluginLoader.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + EventPluginLoader originalInstance = (EventPluginLoader) instanceField.get(null); + instanceField.set(null, mockLoader); + + try { + ContractLogTrigger trigger = new ContractLogTrigger(); + trigger.setRemoved(true); + trigger.setBlockNumber(100L); + trigger.setTransactionId("abc"); + trigger.setContractAddress("0x01"); + LogInfo logInfo = new LogInfo(new byte[0], new ArrayList<>(), new byte[0]); + trigger.setLogInfo(logInfo); + + ContractTriggerCapsule capsule = new ContractTriggerCapsule(trigger); + capsule.processTrigger(); + + assertTrue(Args.getSolidityContractLogTriggerMap().isEmpty()); + assertTrue(Args.getSolidityContractEventTriggerMap().isEmpty()); + } finally { + instanceField.set(null, originalInstance); + Args.getSolidityContractLogTriggerMap().clear(); + Args.getSolidityContractEventTriggerMap().clear(); + } + } + @Test public void testLogInfo() { logger.info("log info to string: {}, ", logInfo.toString()); diff --git a/framework/src/test/java/org/tron/core/db/ManagerMockTest.java b/framework/src/test/java/org/tron/core/db/ManagerMockTest.java index 364b86c82b4..1e4b9a037ac 100644 --- a/framework/src/test/java/org/tron/core/db/ManagerMockTest.java +++ b/framework/src/test/java/org/tron/core/db/ManagerMockTest.java @@ -20,10 +20,14 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.junit.After; +import org.junit.Assert; import org.junit.Test; import org.mockito.MockedConstruction; import org.mockito.MockedStatic; @@ -32,8 +36,12 @@ import org.mockito.stubbing.Answer; import org.tron.common.cron.CronExpression; +import org.tron.common.logsfilter.EventPluginLoader; +import org.tron.common.logsfilter.trigger.ContractLogTrigger; +import org.tron.common.logsfilter.trigger.ContractTrigger; import org.tron.common.parameter.CommonParameter; import org.tron.common.runtime.ProgramResult; +import org.tron.common.runtime.vm.LogInfo; import org.tron.common.utils.Sha256Hash; import org.tron.core.ChainBaseManager; import org.tron.core.capsule.BlockCapsule; @@ -438,4 +446,111 @@ public void testReOrgLogsFilter() throws Exception { privateMethod.invoke(dbManager); } + @Test + public void testPostContractTriggerProcessesSync() throws Exception { + Manager dbManager = spy(new Manager()); + Field eventLoadedField = Manager.class.getDeclaredField("eventPluginLoaded"); + eventLoadedField.setAccessible(true); + eventLoadedField.set(dbManager, true); + + ChainBaseManager cbm = mock(ChainBaseManager.class); + DynamicPropertiesStore dps = mock(DynamicPropertiesStore.class); + when(dps.getLatestSolidifiedBlockNum()).thenReturn(0L); + when(cbm.getDynamicPropertiesStore()).thenReturn(dps); + Field cbmField = Manager.class.getDeclaredField("chainBaseManager"); + cbmField.setAccessible(true); + cbmField.set(dbManager, cbm); + + EventPluginLoader mockLoader = mock(EventPluginLoader.class); + when(mockLoader.isContractLogTriggerEnable()).thenReturn(false); + when(mockLoader.isContractEventTriggerEnable()).thenReturn(false); + when(mockLoader.isSolidityLogTriggerEnable()).thenReturn(true); + when(mockLoader.isSolidityEventTriggerEnable()).thenReturn(false); + + Field instanceField = EventPluginLoader.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + EventPluginLoader original = (EventPluginLoader) instanceField.get(null); + instanceField.set(null, mockLoader); + + Args.getSolidityContractLogTriggerMap().clear(); + + try { + ContractLogTrigger trigger = new ContractLogTrigger(); + trigger.setBlockNumber(200L); + trigger.setTransactionId("tx-id"); + trigger.setContractAddress("0x01"); + trigger.setLogInfo(new LogInfo(new byte[0], new ArrayList<>(), new byte[0])); + + TransactionTrace traceMock = mock(TransactionTrace.class); + ProgramResult resultMock = mock(ProgramResult.class); + when(traceMock.getRuntimeResult()).thenReturn(resultMock); + List triggers = new ArrayList<>(); + triggers.add(trigger); + when(resultMock.getTriggerList()).thenReturn(triggers); + + Method method = Manager.class.getDeclaredMethod("postContractTrigger", + TransactionTrace.class, boolean.class, String.class); + method.setAccessible(true); + method.invoke(dbManager, traceMock, false, "blockhash"); + + Assert.assertNotNull( + "synchronous processTrigger should populate solidity log map", + Args.getSolidityContractLogTriggerMap().get(200L)); + } finally { + instanceField.set(null, original); + eventLoadedField.set(dbManager, false); + Args.getSolidityContractLogTriggerMap().clear(); + } + } + + @Test + public void testPostContractTriggerSwallowsThrowable() throws Exception { + Manager dbManager = spy(new Manager()); + Field eventLoadedField = Manager.class.getDeclaredField("eventPluginLoaded"); + eventLoadedField.setAccessible(true); + eventLoadedField.set(dbManager, true); + + ChainBaseManager cbm = mock(ChainBaseManager.class); + DynamicPropertiesStore dps = mock(DynamicPropertiesStore.class); + when(dps.getLatestSolidifiedBlockNum()).thenReturn(0L); + when(cbm.getDynamicPropertiesStore()).thenReturn(dps); + Field cbmField = Manager.class.getDeclaredField("chainBaseManager"); + cbmField.setAccessible(true); + cbmField.set(dbManager, cbm); + + EventPluginLoader mockLoader = mock(EventPluginLoader.class); + when(mockLoader.isContractLogTriggerEnable()).thenReturn(false); + when(mockLoader.isContractEventTriggerEnable()).thenReturn(false); + when(mockLoader.isSolidityLogTriggerEnable()).thenReturn(true); + when(mockLoader.isSolidityEventTriggerEnable()).thenReturn(false); + + Field instanceField = EventPluginLoader.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + EventPluginLoader original = (EventPluginLoader) instanceField.get(null); + instanceField.set(null, mockLoader); + + try { + // null logInfo → processTrigger throws NPE on logInfo.getTopics() + ContractLogTrigger trigger = new ContractLogTrigger(); + trigger.setBlockNumber(300L); + trigger.setTransactionId("tx-id"); + trigger.setContractAddress("0x01"); + + TransactionTrace traceMock = mock(TransactionTrace.class); + ProgramResult resultMock = mock(ProgramResult.class); + when(traceMock.getRuntimeResult()).thenReturn(resultMock); + when(resultMock.getTriggerList()) + .thenReturn(Collections.singletonList((ContractTrigger) trigger)); + + Method method = Manager.class.getDeclaredMethod("postContractTrigger", + TransactionTrace.class, boolean.class, String.class); + method.setAccessible(true); + // catch (Throwable) absorbs the NPE — invocation must complete normally + method.invoke(dbManager, traceMock, false, "blockhash"); + } finally { + instanceField.set(null, original); + eventLoadedField.set(dbManager, false); + } + } + } \ No newline at end of file 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..6e2cabb6425 100755 --- a/framework/src/test/java/org/tron/core/db/ManagerTest.java +++ b/framework/src/test/java/org/tron/core/db/ManagerTest.java @@ -3,7 +3,9 @@ import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import static org.tron.common.utils.Commons.adjustAssetBalanceV2; import static org.tron.common.utils.Commons.adjustTotalShieldedPoolValue; import static org.tron.common.utils.Commons.getExchangeStoreFinal; @@ -16,12 +18,15 @@ import com.google.common.collect.Sets; import com.google.protobuf.Any; import com.google.protobuf.ByteString; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -32,6 +37,8 @@ import org.tron.common.BaseMethodTest; import org.tron.common.TestConstants; import org.tron.common.crypto.ECKey; +import org.tron.common.logsfilter.EventPluginLoader; +import org.tron.common.logsfilter.trigger.ContractLogTrigger; import org.tron.common.runtime.RuntimeImpl; import org.tron.common.utils.ByteArray; import org.tron.common.utils.Commons; @@ -1292,6 +1299,79 @@ public void blockTrigger() { Assert.assertEquals(TronError.ErrCode.EVENT_SUBSCRIBE_ERROR, thrown.getErrCode()); } + @Test + public void testReOrgContractTriggerClearsMap() throws Exception { + ReflectUtils.setFieldValue(dbManager, "eventPluginLoaded", true); + EventPluginLoader mockLoader = mock(EventPluginLoader.class); + // Disable contract triggers so reOrgContractTrigger skips the old-block fetch + // branch and proceeds to clearSolidityContractTriggerCache(getHeadBlockNum()). + when(mockLoader.isContractEventTriggerEnable()).thenReturn(false); + when(mockLoader.isContractLogTriggerEnable()).thenReturn(false); + when(mockLoader.isSolidityLogTriggerEnable()).thenReturn(true); + when(mockLoader.isSolidityEventTriggerEnable()).thenReturn(false); + Field instanceField = EventPluginLoader.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + EventPluginLoader originalLoader = (EventPluginLoader) instanceField.get(null); + instanceField.set(null, mockLoader); + + long headBlockNum = dbManager.getHeadBlockNum(); + Args.getSolidityContractLogTriggerMap() + .computeIfAbsent(headBlockNum, k -> new LinkedBlockingQueue<>()) + .offer(new ContractLogTrigger()); + Args.getSolidityContractEventTriggerMap() + .computeIfAbsent(headBlockNum, k -> new LinkedBlockingQueue<>()) + .offer(new org.tron.common.logsfilter.trigger.ContractEventTrigger()); + + try { + Method method = Manager.class.getDeclaredMethod("reOrgContractTrigger"); + method.setAccessible(true); + method.invoke(dbManager); + + Assert.assertNull(Args.getSolidityContractLogTriggerMap().get(headBlockNum)); + Assert.assertNull(Args.getSolidityContractEventTriggerMap().get(headBlockNum)); + } finally { + instanceField.set(null, originalLoader); + ReflectUtils.setFieldValue(dbManager, "eventPluginLoaded", false); + Args.getSolidityContractLogTriggerMap().clear(); + Args.getSolidityContractEventTriggerMap().clear(); + } + } + + @Test + public void testClearSolidityContractTriggerCache() throws Exception { + long blockNum = 999L; + ReflectUtils.setFieldValue(dbManager, "eventPluginLoaded", true); + EventPluginLoader mockLoader = mock(EventPluginLoader.class); + when(mockLoader.isSolidityLogTriggerEnable()).thenReturn(true); + when(mockLoader.isSolidityEventTriggerEnable()).thenReturn(true); + Field instanceField = EventPluginLoader.class.getDeclaredField("instance"); + instanceField.setAccessible(true); + EventPluginLoader originalLoader = (EventPluginLoader) instanceField.get(null); + instanceField.set(null, mockLoader); + + Args.getSolidityContractLogTriggerMap() + .computeIfAbsent(blockNum, k -> new LinkedBlockingQueue<>()) + .offer(new ContractLogTrigger()); + Args.getSolidityContractEventTriggerMap() + .computeIfAbsent(blockNum, k -> new LinkedBlockingQueue<>()); + Assert.assertFalse(Args.getSolidityContractLogTriggerMap().isEmpty()); + + try { + Method method = Manager.class.getDeclaredMethod("clearSolidityContractTriggerCache", + long.class); + method.setAccessible(true); + method.invoke(dbManager, blockNum); + + Assert.assertNull(Args.getSolidityContractLogTriggerMap().get(blockNum)); + Assert.assertNull(Args.getSolidityContractEventTriggerMap().get(blockNum)); + } finally { + instanceField.set(null, originalLoader); + ReflectUtils.setFieldValue(dbManager, "eventPluginLoaded", false); + Args.getSolidityContractLogTriggerMap().clear(); + Args.getSolidityContractEventTriggerMap().clear(); + } + } + public void adjustBalance(AccountStore accountStore, byte[] accountAddress, long amount) throws BalanceInsufficientException { Commons.adjustBalance(accountStore, accountAddress, amount,