Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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());
Expand All @@ -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());
Expand Down
22 changes: 19 additions & 3 deletions framework/src/main/java/org/tron/core/db/Manager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SHOULD] switchFork has two applyBlock failure paths that don't clear the cache.

Copy link
Copy Markdown
Collaborator Author

@xxo1shine xxo1shine May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repeated chain switching is a historical oversight and will not be addressed in this PR. It can be discussed separately in a later issue.

throw throwable;
}
long newSolidNum = getDynamicPropertiesStore().getLatestSolidifiedBlockNum();
Expand Down Expand Up @@ -2378,6 +2379,16 @@ private void reOrgContractTrigger() {
getDynamicPropertiesStore().getLatestBlockHeaderHash());
}
}
clearSolidityContractTriggerCache(getHeadBlockNum());
}

private void clearSolidityContractTriggerCache(long blockNum) {
if (eventPluginLoaded
&& (EventPluginLoader.getInstance().isSolidityEventTriggerEnable()
|| EventPluginLoader.getInstance().isSolidityLogTriggerEnable())) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SHOULD] clearSolidityContractTriggerCache guard is narrower than the write side; redundancy path leaks the map.
ContractTriggerCapsule.java:162 ( the event-as-log redundancy path) also writes to solidityContractLogTriggerMap under this combination:

isContractLogTriggerEnable      = true
isContractLogTriggerRedundancy  = true
isSolidityLogTriggerRedundancy  = true
isSolidityLogTriggerEnable      = false
isSolidityEventTriggerEnable    = false

outer condition (isContractLogEnable && isContractLogRedundancy) || ... is true → enters the redundancy block; inner isSolidityLogTriggerRedundancy=true → writes to the solidity log map. But both clear-side guard predicates are false → clear is skipped.

Subtler still: the consumer postSolidityTrigger is also gated by isSolidityLogTriggerEnable, so under this configuration the consumer does not run either. The result: solidityContractLogTriggerMap accumulates one entry per event-bearing block, never consumed, never cleared — a memory leak. It does not produce Issue #6678's "duplicate downstream events" symptom (no downstream is wired up), but since the PR formalizes the clearing responsibility, the clear's semantics should be aligned with the write side.

Suggestion (preferred): simply drop the guard. ConcurrentHashMap.remove(key) on a non-existent key is an O(1) bucket lookup returning null — the cost is negligible. The guard was only a defensive optimization; without it, write-side condition changes won't desync the clear:

private void clearSolidityContractTriggerCache(long blockNum) {
  Args.getSolidityContractLogTriggerMap().remove(blockNum);
  Args.getSolidityContractEventTriggerMap().remove(blockNum);
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling only isContractLogTriggerEnable = true and isContractLogTriggerRedundancy = true will not write data; only enabling the solid event switch will write data.

Args.getSolidityContractLogTriggerMap().remove(blockNum);
Args.getSolidityContractEventTriggerMap().remove(blockNum);
}
}

private void postContractTrigger(final TransactionTrace trace, boolean remove, String blockHash) {
Expand All @@ -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
Comment thread
yanghang8612 marked this conversation as resolved.
// without blocking on actual I/O.
try {
contractTriggerCapsule.processTrigger();
} catch (Throwable throwable) {
logger.warn("Post contract trigger failed.", throwable);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@
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;
import org.apache.commons.collections4.CollectionUtils;
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 {
Expand Down Expand Up @@ -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());
Expand Down
115 changes: 115 additions & 0 deletions framework/src/test/java/org/tron/core/db/ManagerMockTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<ContractTrigger> 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);
}
}

}
Loading
Loading