From 40cc72062d63468ca78582007718892d38d47612 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Thu, 5 Nov 2026 19:43:03 +0800 Subject: [PATCH 1/4] fix(event): limit contract ABI size when decoding events Matching a log to an event (ContractTriggerCapsule and BlockEventGet) rebuilds and hashes a signature string for every ABI entry, synchronously during block application. An ABI is bounded only by the transaction size limit, so the work of decoding a log grows with the ABI with no useful bound of its own. Add size checks to ContractEventParserAbi and apply them on the event-subscription path, which is node-local: it affects only what subscribers receive, not block validity. The check reads each log's own ABI with no shared state, so one contract or transaction cannot affect decoding of another: - an ABI with more than MAX_ABI_ENTRIES entries, or more than MAX_ABI_EVENTS non-anonymous events, is not decoded; its logs are delivered as raw contract logs; - an entry with more than MAX_ABI_ENTRY_PARAMS parameters, or an identifier longer than MAX_ABI_IDENTIFIER_LENGTH, is skipped so its signature is never built. Real contracts stay well within these limits; an over-limit ABI simply has its logs delivered undecoded, which subscribers can still decode client-side. --- .../logsfilter/ContractEventParserAbi.java | 45 +++++++ .../capsule/ContractTriggerCapsule.java | 6 + .../core/services/event/BlockEventGet.java | 5 + .../ContractEventParserAbiTest.java | 124 ++++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 framework/src/test/java/org/tron/common/logsfilter/ContractEventParserAbiTest.java diff --git a/framework/src/main/java/org/tron/common/logsfilter/ContractEventParserAbi.java b/framework/src/main/java/org/tron/common/logsfilter/ContractEventParserAbi.java index a06ccfb5579..ed91e4cb5d9 100644 --- a/framework/src/main/java/org/tron/common/logsfilter/ContractEventParserAbi.java +++ b/framework/src/main/java/org/tron/common/logsfilter/ContractEventParserAbi.java @@ -12,6 +12,11 @@ @Slf4j(topic = "Parser") public class ContractEventParserAbi extends ContractEventParser { + public static final int MAX_ABI_ENTRIES = 1024; + public static final int MAX_ABI_EVENTS = 512; + public static final int MAX_ABI_ENTRY_PARAMS = 64; + public static final int MAX_ABI_IDENTIFIER_LENGTH = 256; + /** * parse Event Topic into map NOTICE: In solidity, Indexed Dynamic types's topic is just * EVENT_INDEXED_ARGS @@ -111,4 +116,44 @@ private static boolean topicsMatched(List topicList, ABI.Entry entry) { } return inputSize == topicList.size(); } + + /** True if the ABI declares more entries than {@link #MAX_ABI_ENTRIES}. */ + public static boolean hasTooManyEntries(ABI abi) { + return abi.getEntrysCount() > MAX_ABI_ENTRIES; + } + + /** True if the ABI declares more non-anonymous events than {@link #MAX_ABI_EVENTS}. */ + public static boolean hasTooManyEvents(ABI abi) { + int events = 0; + for (ABI.Entry entry : abi.getEntrysList()) { + if (entry.getType() == ABI.Entry.EntryType.Event && !entry.getAnonymous() + && ++events > MAX_ABI_EVENTS) { + return true; + } + } + return false; + } + + /** + * True if an entry exceeds the per-entry limits on parameter count + * ({@link #MAX_ABI_ENTRY_PARAMS}) or identifier length ({@link #MAX_ABI_IDENTIFIER_LENGTH}). + */ + public static boolean isEntryTooLarge(ABI.Entry entry) { + if (entry.getInputsCount() > MAX_ABI_ENTRY_PARAMS) { + return true; + } + if (isIdentifierTooLong(entry.getName())) { + return true; + } + for (ABI.Entry.Param param : entry.getInputsList()) { + if (isIdentifierTooLong(param.getType()) || isIdentifierTooLong(param.getName())) { + return true; + } + } + return false; + } + + private static boolean isIdentifierTooLong(String identifier) { + return identifier != null && identifier.length() > MAX_ABI_IDENTIFIER_LENGTH; + } } 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 3ebb5102bba..9fe756c828b 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 @@ -55,6 +55,8 @@ public void processTrigger() { if (abi != null && abi.getEntrysCount() > 0 && topics != null && !topics.isEmpty() && !ArrayUtils.isEmpty(topics.get(0).getData()) + && !ContractEventParserAbi.hasTooManyEntries(abi) + && !ContractEventParserAbi.hasTooManyEvents(abi) && Args.getInstance().getStorage().isContractParseSwitch()) { String logHash = topics.get(0).toString(); @@ -62,6 +64,10 @@ public void processTrigger() { if (entry.getType() != ABI.Entry.EntryType.Event || entry.getAnonymous()) { continue; } + if (ContractEventParserAbi.isEntryTooLarge(entry)) { + // Entry outside the ABI size limits: skip it instead of building its signature. + continue; + } String signature = entry.getName() + "("; String signatureFull = entry.getName() + "("; diff --git a/framework/src/main/java/org/tron/core/services/event/BlockEventGet.java b/framework/src/main/java/org/tron/core/services/event/BlockEventGet.java index 21fc6e1950e..c2833c20e62 100644 --- a/framework/src/main/java/org/tron/core/services/event/BlockEventGet.java +++ b/framework/src/main/java/org/tron/core/services/event/BlockEventGet.java @@ -235,6 +235,8 @@ private ContractTrigger processTrigger(ContractTrigger contractTrigger) { if (abi != null && abi.getEntrysCount() > 0 && topics != null && !topics.isEmpty() && !ArrayUtils.isEmpty(topics.get(0).getData()) + && !ContractEventParserAbi.hasTooManyEntries(abi) + && !ContractEventParserAbi.hasTooManyEvents(abi) && Args.getInstance().getStorage().isContractParseSwitch()) { String logHash = topics.get(0).toString(); @@ -243,6 +245,9 @@ private ContractTrigger processTrigger(ContractTrigger contractTrigger) { || entry.getAnonymous()) { continue; } + if (ContractEventParserAbi.isEntryTooLarge(entry)) { + continue; + } String signature = entry.getName() + "("; String signatureFull = entry.getName() + "("; diff --git a/framework/src/test/java/org/tron/common/logsfilter/ContractEventParserAbiTest.java b/framework/src/test/java/org/tron/common/logsfilter/ContractEventParserAbiTest.java new file mode 100644 index 00000000000..20d683b1afa --- /dev/null +++ b/framework/src/test/java/org/tron/common/logsfilter/ContractEventParserAbiTest.java @@ -0,0 +1,124 @@ +package org.tron.common.logsfilter; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.tron.common.logsfilter.ContractEventParserAbi.MAX_ABI_ENTRIES; +import static org.tron.common.logsfilter.ContractEventParserAbi.MAX_ABI_ENTRY_PARAMS; +import static org.tron.common.logsfilter.ContractEventParserAbi.MAX_ABI_EVENTS; +import static org.tron.common.logsfilter.ContractEventParserAbi.MAX_ABI_IDENTIFIER_LENGTH; +import static org.tron.common.logsfilter.ContractEventParserAbi.hasTooManyEntries; +import static org.tron.common.logsfilter.ContractEventParserAbi.hasTooManyEvents; +import static org.tron.common.logsfilter.ContractEventParserAbi.isEntryTooLarge; + +import org.junit.Test; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI; + +public class ContractEventParserAbiTest { + + private ABI.Entry.Param param(String type, String name) { + return ABI.Entry.Param.newBuilder().setType(type).setName(name).build(); + } + + private String repeat(int n) { + StringBuilder sb = new StringBuilder(n); + for (int i = 0; i < n; i++) { + sb.append('A'); + } + return sb.toString(); + } + + private ABI abiWithEntries(int count) { + ABI.Builder b = ABI.newBuilder(); + for (int i = 0; i < count; i++) { + b.addEntrys(ABI.Entry.newBuilder().setType(ABI.Entry.EntryType.Event)); + } + return b.build(); + } + + @Test + public void normalSizedAbiIsDecoded() { + assertFalse(hasTooManyEntries(abiWithEntries(MAX_ABI_ENTRIES))); + } + + @Test + public void oversizedAbiIsNotDecoded() { + assertTrue(hasTooManyEntries(abiWithEntries(MAX_ABI_ENTRIES + 1))); + } + + @Test + public void emptyAbiIsWithinLimit() { + assertFalse(hasTooManyEntries(ABI.getDefaultInstance())); + } + + @Test + public void eventCountAtBoundIsAllowed() { + assertFalse(hasTooManyEvents(abiWithEntries(MAX_ABI_EVENTS))); + } + + @Test + public void tooManyEventsIsRejected() { + assertTrue(hasTooManyEvents(abiWithEntries(MAX_ABI_EVENTS + 1))); + } + + @Test + public void anonymousEventsAreNotCounted() { + ABI.Builder b = ABI.newBuilder(); + for (int i = 0; i < MAX_ABI_EVENTS + 1; i++) { + b.addEntrys(ABI.Entry.newBuilder().setType(ABI.Entry.EntryType.Event).setAnonymous(true)); + } + assertFalse(hasTooManyEvents(b.build())); + } + + @Test + public void normalEntryIsWithinBound() { + ABI.Entry entry = ABI.Entry.newBuilder().setName("Transfer") + .addInputs(param("address", "from")).addInputs(param("uint256", "value")).build(); + assertFalse(isEntryTooLarge(entry)); + } + + @Test + public void overLongEventNameIsRejected() { + ABI.Entry entry = ABI.Entry.newBuilder() + .setName(repeat(MAX_ABI_IDENTIFIER_LENGTH + 1)).build(); + assertTrue(isEntryTooLarge(entry)); + } + + @Test + public void overLongParamTypeIsRejected() { + ABI.Entry entry = ABI.Entry.newBuilder().setName("E") + .addInputs(param(repeat(MAX_ABI_IDENTIFIER_LENGTH + 1), "x")).build(); + assertTrue(isEntryTooLarge(entry)); + } + + @Test + public void overLongParamNameIsRejected() { + ABI.Entry entry = ABI.Entry.newBuilder().setName("E") + .addInputs(param("uint256", repeat(MAX_ABI_IDENTIFIER_LENGTH + 1))).build(); + assertTrue(isEntryTooLarge(entry)); + } + + @Test + public void tooManyParamsIsRejected() { + ABI.Entry.Builder b = ABI.Entry.newBuilder().setName("E"); + for (int i = 0; i <= MAX_ABI_ENTRY_PARAMS; i++) { + b.addInputs(param("uint256", "p")); + } + assertTrue(isEntryTooLarge(b.build())); + } + + @Test + public void paramCountAtBoundIsAllowed() { + ABI.Entry.Builder b = ABI.Entry.newBuilder().setName("E"); + for (int i = 0; i < MAX_ABI_ENTRY_PARAMS; i++) { + b.addInputs(param("uint256", "p")); + } + assertFalse(isEntryTooLarge(b.build())); + } + + @Test + public void identifierExactlyAtBoundIsAllowed() { + ABI.Entry entry = ABI.Entry.newBuilder() + .setName(repeat(MAX_ABI_IDENTIFIER_LENGTH)).build(); + assertFalse(isEntryTooLarge(entry)); + } +} From a82450d902c08e63578a99206004b188bf2e30f8 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Mon, 9 Nov 2026 19:10:39 +0800 Subject: [PATCH 2/4] test(event): add behavior tests for the ABI size guard on both decode paths Also make the predicate tests exercise the entry-count and event-count limits independently (mixed function/event ABIs). --- .../logsfilter/ContractAbiSizeLimitTest.java | 254 ++++++++++++++++++ .../ContractEventParserAbiTest.java | 40 ++- 2 files changed, 283 insertions(+), 11 deletions(-) create mode 100644 framework/src/test/java/org/tron/common/logsfilter/ContractAbiSizeLimitTest.java diff --git a/framework/src/test/java/org/tron/common/logsfilter/ContractAbiSizeLimitTest.java b/framework/src/test/java/org/tron/common/logsfilter/ContractAbiSizeLimitTest.java new file mode 100644 index 00000000000..4ec4f1430e9 --- /dev/null +++ b/framework/src/test/java/org/tron/common/logsfilter/ContractAbiSizeLimitTest.java @@ -0,0 +1,254 @@ +package org.tron.common.logsfilter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.tron.common.logsfilter.ContractEventParserAbi.MAX_ABI_ENTRIES; +import static org.tron.common.logsfilter.ContractEventParserAbi.MAX_ABI_EVENTS; +import static org.tron.common.logsfilter.ContractEventParserAbi.MAX_ABI_IDENTIFIER_LENGTH; + +import com.google.common.collect.Lists; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.ArgumentCaptor; +import org.tron.common.TestConstants; +import org.tron.common.crypto.Hash; +import org.tron.common.logsfilter.capsule.ContractTriggerCapsule; +import org.tron.common.logsfilter.capsule.RawData; +import org.tron.common.logsfilter.trigger.ContractEventTrigger; +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; +import org.tron.core.services.event.BlockEventGet; +import org.tron.protos.contract.SmartContractOuterClass.SmartContract.ABI; + +/** + * Behavior tests for the ABI-size guard on both event-subscription decode paths — version 0 + * ({@link ContractTriggerCapsule#processTrigger()}) and version 1 + * ({@link BlockEventGet} {@code processTrigger}). Each over-limit case carries a topic0 that + * matches an event present in the ABI, so without the guard it would decode; the test therefore + * fails if the call-site guard is removed, not merely if the predicate changes. + */ +public class ContractAbiSizeLimitTest { + + @ClassRule + public static final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @BeforeClass + public static void init() throws IOException { + Args.setParam(new String[] {"--output-directory", temporaryFolder.newFolder().toString()}, + TestConstants.TEST_CONF); + Args.getInstance().getStorage().setContractParseSwitch(true); + } + + @AfterClass + public static void destroy() { + Args.clearParam(); + } + + private ABI.Entry event(String name, String... paramTypes) { + ABI.Entry.Builder e = ABI.Entry.newBuilder() + .setType(ABI.Entry.EntryType.Event).setAnonymous(false).setName(name); + for (String t : paramTypes) { + e.addInputs(ABI.Entry.Param.newBuilder().setType(t)); + } + return e.build(); + } + + private ABI.Entry function(String name) { + return ABI.Entry.newBuilder().setType(ABI.Entry.EntryType.Function).setName(name).build(); + } + + /** topic0 = sha3("name(type1,type2)"), i.e. what a log matching this event would carry. */ + private DataWord topic0(String name, String... paramTypes) { + StringBuilder sig = new StringBuilder(name).append("("); + for (int i = 0; i < paramTypes.length; i++) { + if (i > 0) { + sig.append(","); + } + sig.append(paramTypes[i]); + } + sig.append(")"); + return new DataWord(Hash.sha3(sig.toString().getBytes())); + } + + private ContractTrigger trigger(ABI abi, DataWord topic0) { + byte[] address = new byte[20]; + List topics = Lists.newArrayList(topic0); + byte[] data = new byte[0]; + ContractTrigger t = new ContractTrigger(); + t.setBlockNumber(1L); + t.setRemoved(false); + t.setAbi(abi); + t.setLogInfo(new LogInfo(address, topics, data)); + t.setRawData(new RawData(address, topics, data)); + return t; + } + + /** + * Runs the version-1 path: {@link BlockEventGet} {@code processTrigger} returns the decoded + * event trigger, or the raw log trigger when the ABI is rejected. + */ + private ContractTrigger v1Process(ContractTrigger input) throws Exception { + Method m = BlockEventGet.class.getDeclaredMethod("processTrigger", ContractTrigger.class); + m.setAccessible(true); + return (ContractTrigger) m.invoke(new BlockEventGet(), input); + } + + @Test + public void v1_inLimitAbiIsDecodedToEvent() throws Exception { + ABI abi = ABI.newBuilder().addEntrys(event("Transfer", "address", "uint256")).build(); + ContractTrigger out = v1Process(trigger(abi, topic0("Transfer", "address", "uint256"))); + Assert.assertTrue(out instanceof ContractEventTrigger); + Assert.assertEquals("Transfer", ((ContractEventTrigger) out).getEventName()); + } + + @Test + public void v1_overEventLimitFallsBackToRawLog() throws Exception { + ABI.Builder b = ABI.newBuilder(); + for (int i = 0; i < MAX_ABI_EVENTS; i++) { + b.addEntrys(event("Filler" + i)); + } + b.addEntrys(event("Transfer", "address", "uint256")); // 513th event, matches topic0 + ContractTrigger in = trigger(b.build(), topic0("Transfer", "address", "uint256")); + ContractTrigger out = v1Process(in); + Assert.assertTrue(out instanceof ContractLogTrigger); + Assert.assertFalse(out instanceof ContractEventTrigger); + // raw data is preserved + Assert.assertEquals(in.getLogInfo().getHexTopics(), ((ContractLogTrigger) out).getTopicList()); + Assert.assertEquals(in.getLogInfo().getHexData(), ((ContractLogTrigger) out).getData()); + } + + @Test + public void v1_overEntryLimitFallsBackToRawLog() throws Exception { + ABI.Builder b = ABI.newBuilder(); + for (int i = 0; i < MAX_ABI_ENTRIES; i++) { + b.addEntrys(function("f" + i)); + } + b.addEntrys(event("Transfer", "address", "uint256")); // 1025th entry, matches topic0 + ContractTrigger out = v1Process(trigger(b.build(), topic0("Transfer", "address", "uint256"))); + Assert.assertTrue(out instanceof ContractLogTrigger); + Assert.assertFalse(out instanceof ContractEventTrigger); + } + + @Test + public void v1_oversizedEntrySkippedButValidEntryMatches() throws Exception { + ABI abi = ABI.newBuilder() + .addEntrys(event(repeatA(MAX_ABI_IDENTIFIER_LENGTH + 1))) // oversized name, skipped + .addEntrys(event("Transfer", "address", "uint256")) // valid, matches + .build(); + ContractTrigger out = v1Process(trigger(abi, topic0("Transfer", "address", "uint256"))); + Assert.assertTrue(out instanceof ContractEventTrigger); + Assert.assertEquals("Transfer", ((ContractEventTrigger) out).getEventName()); + } + + @Test + public void v1_oversizedMatchingEntryIsSkippedAndFallsBackToRawLog() throws Exception { + // The over-long entry is itself the one the log matches: the per-entry skip is the only + // thing preventing it from being decoded, so dropping that guard would turn this into an + // event instead of a raw log. + String bigName = repeatA(MAX_ABI_IDENTIFIER_LENGTH + 1); + ABI abi = ABI.newBuilder().addEntrys(event(bigName)).build(); + ContractTrigger out = v1Process(trigger(abi, topic0(bigName))); + Assert.assertTrue(out instanceof ContractLogTrigger); + Assert.assertFalse(out instanceof ContractEventTrigger); + } + + @Test + public void v0_inLimitAbiIsDecodedToEvent() throws Exception { + EventPluginLoader mock = contractTriggersEnabled(); + EventPluginLoader original = swapLoader(mock); + try { + ABI abi = ABI.newBuilder().addEntrys(event("Transfer", "address", "uint256")).build(); + new ContractTriggerCapsule(trigger(abi, topic0("Transfer", "address", "uint256"))) + .processTrigger(); + ArgumentCaptor captor = + ArgumentCaptor.forClass(ContractEventTrigger.class); + verify(mock).postContractEventTrigger(captor.capture()); + verify(mock, never()).postContractLogTrigger(any()); + Assert.assertEquals("Transfer", captor.getValue().getEventName()); + } finally { + swapLoader(original); + } + } + + @Test + public void v0_overEventLimitFallsBackToRawLog() throws Exception { + ABI.Builder b = ABI.newBuilder(); + for (int i = 0; i < MAX_ABI_EVENTS; i++) { + b.addEntrys(event("Filler" + i)); + } + b.addEntrys(event("Transfer", "address", "uint256")); // would decode without the guard + v0AssertFallsBackToRawLog(b.build(), topic0("Transfer", "address", "uint256")); + } + + @Test + public void v0_overEntryLimitFallsBackToRawLog() throws Exception { + ABI.Builder b = ABI.newBuilder(); + for (int i = 0; i < MAX_ABI_ENTRIES; i++) { + b.addEntrys(function("f" + i)); + } + b.addEntrys(event("Transfer", "address", "uint256")); // would decode without the guard + v0AssertFallsBackToRawLog(b.build(), topic0("Transfer", "address", "uint256")); + } + + @Test + public void v0_oversizedMatchingEntryFallsBackToRawLog() throws Exception { + String bigName = repeatA(MAX_ABI_IDENTIFIER_LENGTH + 1); + ABI abi = ABI.newBuilder().addEntrys(event(bigName)).build(); + v0AssertFallsBackToRawLog(abi, topic0(bigName)); + } + + private void v0AssertFallsBackToRawLog(ABI abi, DataWord topic0) throws Exception { + EventPluginLoader mock = contractTriggersEnabled(); + EventPluginLoader original = swapLoader(mock); + try { + ContractTrigger in = trigger(abi, topic0); + new ContractTriggerCapsule(in).processTrigger(); + ArgumentCaptor captor = + ArgumentCaptor.forClass(ContractLogTrigger.class); + verify(mock, never()).postContractEventTrigger(any()); + verify(mock).postContractLogTrigger(captor.capture()); + // raw data is preserved + Assert.assertEquals(in.getLogInfo().getHexTopics(), captor.getValue().getTopicList()); + Assert.assertEquals(in.getLogInfo().getHexData(), captor.getValue().getData()); + } finally { + swapLoader(original); + } + } + + private EventPluginLoader contractTriggersEnabled() { + EventPluginLoader mock = mock(EventPluginLoader.class); + when(mock.isContractEventTriggerEnable()).thenReturn(true); + when(mock.isContractLogTriggerEnable()).thenReturn(true); + return mock; + } + + private EventPluginLoader swapLoader(EventPluginLoader loader) throws Exception { + Field f = EventPluginLoader.class.getDeclaredField("instance"); + f.setAccessible(true); + EventPluginLoader previous = (EventPluginLoader) f.get(null); + f.set(null, loader); + return previous; + } + + private String repeatA(int n) { + StringBuilder sb = new StringBuilder(n); + for (int i = 0; i < n; i++) { + sb.append('A'); + } + return sb.toString(); + } +} diff --git a/framework/src/test/java/org/tron/common/logsfilter/ContractEventParserAbiTest.java b/framework/src/test/java/org/tron/common/logsfilter/ContractEventParserAbiTest.java index 20d683b1afa..d2dfaa3ceb5 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/ContractEventParserAbiTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/ContractEventParserAbiTest.java @@ -27,37 +27,55 @@ private String repeat(int n) { return sb.toString(); } - private ABI abiWithEntries(int count) { + /** Build an ABI with the given number of function and (non-anonymous) event entries. */ + private ABI abiOf(int functions, int events) { ABI.Builder b = ABI.newBuilder(); - for (int i = 0; i < count; i++) { + for (int i = 0; i < functions; i++) { + b.addEntrys(ABI.Entry.newBuilder().setType(ABI.Entry.EntryType.Function)); + } + for (int i = 0; i < events; i++) { b.addEntrys(ABI.Entry.newBuilder().setType(ABI.Entry.EntryType.Event)); } return b.build(); } + // The entry-count limit and the event-count limit are exercised independently below: each case + // stays within one limit and crosses only the other, so a test cannot pass by accident if one + // of the two guards were removed. + @Test - public void normalSizedAbiIsDecoded() { - assertFalse(hasTooManyEntries(abiWithEntries(MAX_ABI_ENTRIES))); + public void atBothBoundsIsDecoded() { + // 1,023 functions + 1 event = 1,024 entries, 1 event: within both limits. + ABI abi = abiOf(MAX_ABI_ENTRIES - 1, 1); + assertFalse(hasTooManyEntries(abi)); + assertFalse(hasTooManyEvents(abi)); } @Test - public void oversizedAbiIsNotDecoded() { - assertTrue(hasTooManyEntries(abiWithEntries(MAX_ABI_ENTRIES + 1))); + public void onlyEntryLimitExceeded() { + // 1,024 functions + 1 event = 1,025 entries, 1 event: only the entry limit is exceeded. + ABI abi = abiOf(MAX_ABI_ENTRIES, 1); + assertTrue(hasTooManyEntries(abi)); + assertFalse(hasTooManyEvents(abi)); } @Test - public void emptyAbiIsWithinLimit() { - assertFalse(hasTooManyEntries(ABI.getDefaultInstance())); + public void onlyEventLimitExceeded() { + // 513 events + 3 functions = 516 entries, 513 events: only the event limit is exceeded. + ABI abi = abiOf(3, MAX_ABI_EVENTS + 1); + assertFalse(hasTooManyEntries(abi)); + assertTrue(hasTooManyEvents(abi)); } @Test public void eventCountAtBoundIsAllowed() { - assertFalse(hasTooManyEvents(abiWithEntries(MAX_ABI_EVENTS))); + assertFalse(hasTooManyEvents(abiOf(0, MAX_ABI_EVENTS))); } @Test - public void tooManyEventsIsRejected() { - assertTrue(hasTooManyEvents(abiWithEntries(MAX_ABI_EVENTS + 1))); + public void emptyAbiIsWithinLimit() { + assertFalse(hasTooManyEntries(ABI.getDefaultInstance())); + assertFalse(hasTooManyEvents(ABI.getDefaultInstance())); } @Test From 9b78007dfe39f0f3dd36122b662920e0a8cb7a0b Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Mon, 9 Nov 2026 19:37:50 +0800 Subject: [PATCH 3/4] perf(event): check parse switch before ABI size guards Short-circuit isContractParseSwitch() ahead of hasTooManyEntries/ hasTooManyEvents in both decode paths so a parsing-disabled node skips the O(n) ABI scan per log. --- .../common/logsfilter/capsule/ContractTriggerCapsule.java | 4 ++-- .../main/java/org/tron/core/services/event/BlockEventGet.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 9fe756c828b..1845bb6ede9 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 @@ -55,9 +55,9 @@ public void processTrigger() { if (abi != null && abi.getEntrysCount() > 0 && topics != null && !topics.isEmpty() && !ArrayUtils.isEmpty(topics.get(0).getData()) + && Args.getInstance().getStorage().isContractParseSwitch() && !ContractEventParserAbi.hasTooManyEntries(abi) - && !ContractEventParserAbi.hasTooManyEvents(abi) - && Args.getInstance().getStorage().isContractParseSwitch()) { + && !ContractEventParserAbi.hasTooManyEvents(abi)) { String logHash = topics.get(0).toString(); for (ABI.Entry entry : abi.getEntrysList()) { diff --git a/framework/src/main/java/org/tron/core/services/event/BlockEventGet.java b/framework/src/main/java/org/tron/core/services/event/BlockEventGet.java index c2833c20e62..38a688bc8a0 100644 --- a/framework/src/main/java/org/tron/core/services/event/BlockEventGet.java +++ b/framework/src/main/java/org/tron/core/services/event/BlockEventGet.java @@ -235,9 +235,9 @@ private ContractTrigger processTrigger(ContractTrigger contractTrigger) { if (abi != null && abi.getEntrysCount() > 0 && topics != null && !topics.isEmpty() && !ArrayUtils.isEmpty(topics.get(0).getData()) + && Args.getInstance().getStorage().isContractParseSwitch() && !ContractEventParserAbi.hasTooManyEntries(abi) - && !ContractEventParserAbi.hasTooManyEvents(abi) - && Args.getInstance().getStorage().isContractParseSwitch()) { + && !ContractEventParserAbi.hasTooManyEvents(abi)) { String logHash = topics.get(0).toString(); for (SmartContractOuterClass.SmartContract.ABI.Entry entry : abi.getEntrysList()) { From 8f619d290ed110f1da5193f6dc23b7031531b357 Mon Sep 17 00:00:00 2001 From: 0xbigapple Date: Tue, 10 Nov 2026 14:50:35 +0800 Subject: [PATCH 4/4] test(event): close mutant gaps in ABI size guard tests Use non-empty log data so the raw-log data-preservation assertions compare real hex, and add a v0 oversized-then-valid-entry case so a continue->break mutation in ContractTriggerCapsule is caught. --- .../logsfilter/ContractAbiSizeLimitTest.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/framework/src/test/java/org/tron/common/logsfilter/ContractAbiSizeLimitTest.java b/framework/src/test/java/org/tron/common/logsfilter/ContractAbiSizeLimitTest.java index 4ec4f1430e9..e8c49286bc7 100644 --- a/framework/src/test/java/org/tron/common/logsfilter/ContractAbiSizeLimitTest.java +++ b/framework/src/test/java/org/tron/common/logsfilter/ContractAbiSizeLimitTest.java @@ -87,7 +87,9 @@ private DataWord topic0(String name, String... paramTypes) { private ContractTrigger trigger(ABI abi, DataWord topic0) { byte[] address = new byte[20]; List topics = Lists.newArrayList(topic0); - byte[] data = new byte[0]; + byte[] data = new byte[64]; + data[31] = 0x2a; + data[63] = 0x7b; ContractTrigger t = new ContractTrigger(); t.setBlockNumber(1L); t.setRemoved(false); @@ -211,6 +213,26 @@ public void v0_oversizedMatchingEntryFallsBackToRawLog() throws Exception { v0AssertFallsBackToRawLog(abi, topic0(bigName)); } + @Test + public void v0_oversizedEntrySkippedButValidEntryMatches() throws Exception { + EventPluginLoader mock = contractTriggersEnabled(); + EventPluginLoader original = swapLoader(mock); + try { + ABI abi = ABI.newBuilder() + .addEntrys(event(repeatA(MAX_ABI_IDENTIFIER_LENGTH + 1))) // oversized name, skipped + .addEntrys(event("Transfer", "address", "uint256")) // valid, matches + .build(); + new ContractTriggerCapsule(trigger(abi, topic0("Transfer", "address", "uint256"))) + .processTrigger(); + ArgumentCaptor captor = + ArgumentCaptor.forClass(ContractEventTrigger.class); + verify(mock).postContractEventTrigger(captor.capture()); + Assert.assertEquals("Transfer", captor.getValue().getEventName()); + } finally { + swapLoader(original); + } + } + private void v0AssertFallsBackToRawLog(ABI abi, DataWord topic0) throws Exception { EventPluginLoader mock = contractTriggersEnabled(); EventPluginLoader original = swapLoader(mock);