From 3c26b260fd37ae82de1c0568b4552b11ecc2b1bd Mon Sep 17 00:00:00 2001 From: Asuka Date: Fri, 8 May 2026 10:43:28 +0800 Subject: [PATCH 1/3] feat(vm): add node-level vm.constantCallTimeoutMs for constant calls Operators may set any positive integer to extend the per-call deadline for the constant-call APIs (triggerconstantcontract, triggersmartcontract dispatched to view/pure functions, estimateenergy, eth_call, eth_estimateGas, and others). The configured value is used verbatim. Replaces unsafe use of --debug, which also extends block-processing. --- .../org/tron/core/actuator/VMActuator.java | 11 +++++++-- .../common/parameter/CommonParameter.java | 12 ++++++++++ .../org/tron/core/config/args/VmConfig.java | 24 +++++++++++++++++-- .../java/org/tron/core/config/args/Args.java | 1 + framework/src/main/resources/config.conf | 12 ++++++++++ 5 files changed, 56 insertions(+), 4 deletions(-) diff --git a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java index 9da41574ff0..ff6a90f16f6 100644 --- a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java @@ -401,6 +401,10 @@ private void create() long maxCpuTimeOfOneTx = rootRepository.getDynamicPropertiesStore() .getMaxCpuTimeOfOneTx() * VMConstant.ONE_THOUSAND; long thisTxCPULimitInUs = (long) (maxCpuTimeOfOneTx * getCpuLimitInUsRatio()); + long constantCallTimeoutMs = CommonParameter.getInstance().getConstantCallTimeoutMs(); + if (isConstantCall && constantCallTimeoutMs > 0L) { + thisTxCPULimitInUs = constantCallTimeoutMs * VMConstant.ONE_THOUSAND; + } long vmStartInUs = System.nanoTime() / VMConstant.ONE_THOUSAND; long vmShouldEndInUs = vmStartInUs + thisTxCPULimitInUs; ProgramInvoke programInvoke = ProgramInvokeFactory @@ -514,8 +518,11 @@ private void call() long maxCpuTimeOfOneTx = rootRepository.getDynamicPropertiesStore() .getMaxCpuTimeOfOneTx() * VMConstant.ONE_THOUSAND; - long thisTxCPULimitInUs = - (long) (maxCpuTimeOfOneTx * getCpuLimitInUsRatio()); + long thisTxCPULimitInUs = (long) (maxCpuTimeOfOneTx * getCpuLimitInUsRatio()); + long constantCallTimeoutMs = CommonParameter.getInstance().getConstantCallTimeoutMs(); + if (isConstantCall && constantCallTimeoutMs > 0L) { + thisTxCPULimitInUs = constantCallTimeoutMs * VMConstant.ONE_THOUSAND; + } long vmStartInUs = System.nanoTime() / VMConstant.ONE_THOUSAND; long vmShouldEndInUs = vmStartInUs + thisTxCPULimitInUs; ProgramInvoke programInvoke = ProgramInvokeFactory diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index a73158a718a..30e26d89b26 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -58,6 +58,18 @@ public class CommonParameter { @Getter @Setter public double maxTimeRatio = calcMaxTimeRatio(); + /** + * Max TVM execution time (ms) for constant calls — covers + * triggerconstantcontract, triggersmartcontract dispatched to view/pure + * functions, estimateenergy, eth_call, eth_estimateGas, and any other + * RPC routed through Wallet#callConstantContract. 0 = use the same + * deadline as block processing (current behaviour). When operators set + * this in config the value must be positive; validated at config-load + * in VmConfig. + */ + @Getter + @Setter + public long constantCallTimeoutMs = 0L; @Getter @Setter public boolean saveInternalTx; diff --git a/common/src/main/java/org/tron/core/config/args/VmConfig.java b/common/src/main/java/org/tron/core/config/args/VmConfig.java index d583cf4c601..712e3375e1c 100644 --- a/common/src/main/java/org/tron/core/config/args/VmConfig.java +++ b/common/src/main/java/org/tron/core/config/args/VmConfig.java @@ -2,6 +2,7 @@ import com.typesafe.config.Config; import com.typesafe.config.ConfigBeanFactory; +import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -15,6 +16,8 @@ @Setter public class VmConfig { + private static final String CONSTANT_CALL_TIMEOUT_MS_KEY = "constantCallTimeoutMs"; + private boolean supportConstant = false; private long maxEnergyLimitForConstant = 100_000_000L; private int lruCacheSize = 500; @@ -27,6 +30,11 @@ public class VmConfig { private boolean saveInternalTx = false; private boolean saveFeaturedInternalTx = false; private boolean saveCancelAllUnfreezeV2Details = false; + // Excluded from ConfigBeanFactory binding (no setter): the property is + // intentionally absent from reference.conf so {@code Config#hasPath} alone + // signals operator opt-in. Bound manually in {@link #fromConfig}. + @Setter(AccessLevel.NONE) + private long constantCallTimeoutMs = 0L; /** * Create VmConfig from the "vm" section of the application config. @@ -36,11 +44,11 @@ public class VmConfig { public static VmConfig fromConfig(Config config) { Config vmSection = config.getConfig("vm"); VmConfig vmConfig = ConfigBeanFactory.create(vmSection, VmConfig.class); - vmConfig.postProcess(); + vmConfig.postProcess(vmSection); return vmConfig; } - private void postProcess() { + private void postProcess(Config vmSection) { // clamp maxEnergyLimitForConstant if (maxEnergyLimitForConstant < 3_000_000L) { maxEnergyLimitForConstant = 3_000_000L; @@ -60,5 +68,17 @@ private void postProcess() { logger.warn("Configuring [vm.saveCancelAllUnfreezeV2Details] won't work as " + "vm.saveInternalTx or vm.saveFeaturedInternalTx is off."); } + + // constantCallTimeoutMs is excluded from ConfigBeanFactory binding (no + // setter) and intentionally absent from reference.conf, so hasPath alone + // tells us whether the operator opted in. Only positive values are valid. + if (vmSection.hasPath(CONSTANT_CALL_TIMEOUT_MS_KEY)) { + long value = vmSection.getLong(CONSTANT_CALL_TIMEOUT_MS_KEY); + if (value <= 0L) { + throw new IllegalArgumentException( + "vm.constantCallTimeoutMs must be > 0 when configured, got " + value); + } + constantCallTimeoutMs = value; + } } } diff --git a/framework/src/main/java/org/tron/core/config/args/Args.java b/framework/src/main/java/org/tron/core/config/args/Args.java index f91c6a437ac..fb9a4dce835 100644 --- a/framework/src/main/java/org/tron/core/config/args/Args.java +++ b/framework/src/main/java/org/tron/core/config/args/Args.java @@ -212,6 +212,7 @@ private static void applyVmConfig(VmConfig vm) { PARAMETER.saveInternalTx = vm.isSaveInternalTx(); PARAMETER.saveFeaturedInternalTx = vm.isSaveFeaturedInternalTx(); PARAMETER.saveCancelAllUnfreezeV2Details = vm.isSaveCancelAllUnfreezeV2Details(); + PARAMETER.constantCallTimeoutMs = vm.getConstantCallTimeoutMs(); } // Old applyStorageConfig removed — merged into applyStorageConfig() diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 369924074bc..2a0c21809af 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -710,6 +710,18 @@ vm = { # Indicates the max retry time for executing transaction in estimating energy. Default 3. # estimateEnergyMaxRetry = 3 + + # Max TVM execution time (ms) for constant calls — applies to + # triggerconstantcontract, triggersmartcontract dispatched to view/pure + # functions, estimateenergy, eth_call, eth_estimateGas, and any other RPC + # routed through the constant-call path. When set, must be a positive + # integer and is used verbatim as the per-call deadline (no clamp against + # the network's maxCpuTimeOfOneTx). Omit the property entirely to keep the + # default behaviour of sharing the block-processing deadline. Migration + # note: if previously running --debug to extend constant calls, switch to + # this option (--debug also extends block-processing, which is unsafe; see + # issue #6266). + # constantCallTimeoutMs = 100 } # These parameters are designed for private chain testing only and cannot be freely switched on or off in production systems. From 8b7901fdc1d3265cc13c5e7cbec49fc2510c71b3 Mon Sep 17 00:00:00 2001 From: Asuka Date: Fri, 8 May 2026 10:43:32 +0800 Subject: [PATCH 2/3] test(vm): cover constantCallTimeoutMs explicit-configure validation --- .../tron/core/config/args/VmConfigTest.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/common/src/test/java/org/tron/core/config/args/VmConfigTest.java b/common/src/test/java/org/tron/core/config/args/VmConfigTest.java index b134fe00c2b..f63b43a4bfc 100644 --- a/common/src/test/java/org/tron/core/config/args/VmConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/VmConfigTest.java @@ -88,4 +88,55 @@ public void testEstimateEnergyMaxRetryBoundaryValues() { assertEquals(3, VmConfig.fromConfig( withRef("vm { estimateEnergyMaxRetry = 3 }")).getEstimateEnergyMaxRetry()); } + + // =========================================================================== + // Constant-call timeout (issue #6681). The validation rule: any positive + // value is accepted, but zero/negative is rejected ONLY when the operator + // explicitly set the property in their config. Absence keeps the in-Java + // default (0L = "share the block-processing deadline"). + // =========================================================================== + + @Test + public void testConstantCallTimeoutDefaultWhenAbsent() { + // No path in the config, no entry in reference.conf -> default 0L kept, + // no validation triggered. + VmConfig vm = VmConfig.fromConfig(withRef()); + assertEquals(0L, vm.getConstantCallTimeoutMs()); + } + + @Test + public void testConstantCallTimeoutAcceptsAnyPositiveValue() { + assertEquals(1L, VmConfig.fromConfig( + withRef("vm { constantCallTimeoutMs = 1 }")).getConstantCallTimeoutMs()); + assertEquals(50L, VmConfig.fromConfig( + withRef("vm { constantCallTimeoutMs = 50 }")).getConstantCallTimeoutMs()); + assertEquals(500L, VmConfig.fromConfig( + withRef("vm { constantCallTimeoutMs = 500 }")).getConstantCallTimeoutMs()); + assertEquals(5_000L, VmConfig.fromConfig( + withRef("vm { constantCallTimeoutMs = 5000 }")).getConstantCallTimeoutMs()); + } + + @Test + public void testConstantCallTimeoutZeroRejectedWhenExplicitlyConfigured() { + // Operator wrote `= 0` in config -> treated as a misconfiguration even + // though it equals the in-Java default. Forces an explicit positive value. + try { + VmConfig.fromConfig(withRef("vm { constantCallTimeoutMs = 0 }")); + org.junit.Assert.fail("expected IllegalArgumentException for explicit 0"); + } catch (IllegalArgumentException ex) { + org.junit.Assert.assertTrue(ex.getMessage(), + ex.getMessage().contains("constantCallTimeoutMs")); + } + } + + @Test + public void testConstantCallTimeoutNegativeRejected() { + try { + VmConfig.fromConfig(withRef("vm { constantCallTimeoutMs = -1 }")); + org.junit.Assert.fail("expected IllegalArgumentException for negative ms"); + } catch (IllegalArgumentException ex) { + org.junit.Assert.assertTrue(ex.getMessage(), + ex.getMessage().contains("constantCallTimeoutMs")); + } + } } From 057bf4e21705ab96df80b3084d4bb9d38f29daa6 Mon Sep 17 00:00:00 2001 From: Asuka Date: Sat, 9 May 2026 10:45:03 +0800 Subject: [PATCH 3/3] fix(vm): harden constant call timeout deadline --- .../org/tron/core/actuator/VMActuator.java | 28 +++++++++---------- .../tron/core/actuator/VMActuatorTest.java | 23 +++++++++++++++ .../common/parameter/CommonParameter.java | 4 +-- .../org/tron/core/config/args/VmConfig.java | 12 ++++++-- .../tron/core/config/args/VmConfigTest.java | 19 +++++++++++-- framework/src/main/resources/config.conf | 12 ++++---- 6 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 actuator/src/test/java/org/tron/core/actuator/VMActuatorTest.java diff --git a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java index ff6a90f16f6..1b0e8a6637f 100644 --- a/actuator/src/main/java/org/tron/core/actuator/VMActuator.java +++ b/actuator/src/main/java/org/tron/core/actuator/VMActuator.java @@ -398,13 +398,9 @@ private void create() byte[] ops = newSmartContract.getBytecode().toByteArray(); rootInternalTx = new InternalTransaction(trx, trxType); - long maxCpuTimeOfOneTx = rootRepository.getDynamicPropertiesStore() - .getMaxCpuTimeOfOneTx() * VMConstant.ONE_THOUSAND; - long thisTxCPULimitInUs = (long) (maxCpuTimeOfOneTx * getCpuLimitInUsRatio()); - long constantCallTimeoutMs = CommonParameter.getInstance().getConstantCallTimeoutMs(); - if (isConstantCall && constantCallTimeoutMs > 0L) { - thisTxCPULimitInUs = constantCallTimeoutMs * VMConstant.ONE_THOUSAND; - } + long thisTxCPULimitInUs = calculateCpuLimitInUs(isConstantCall, + rootRepository.getDynamicPropertiesStore().getMaxCpuTimeOfOneTx(), + getCpuLimitInUsRatio(), CommonParameter.getInstance().getConstantCallTimeoutMs()); long vmStartInUs = System.nanoTime() / VMConstant.ONE_THOUSAND; long vmShouldEndInUs = vmStartInUs + thisTxCPULimitInUs; ProgramInvoke programInvoke = ProgramInvokeFactory @@ -516,13 +512,9 @@ private void call() energyLimit = getTotalEnergyLimit(creator, caller, contract, feeLimit, callValue); } - long maxCpuTimeOfOneTx = rootRepository.getDynamicPropertiesStore() - .getMaxCpuTimeOfOneTx() * VMConstant.ONE_THOUSAND; - long thisTxCPULimitInUs = (long) (maxCpuTimeOfOneTx * getCpuLimitInUsRatio()); - long constantCallTimeoutMs = CommonParameter.getInstance().getConstantCallTimeoutMs(); - if (isConstantCall && constantCallTimeoutMs > 0L) { - thisTxCPULimitInUs = constantCallTimeoutMs * VMConstant.ONE_THOUSAND; - } + long thisTxCPULimitInUs = calculateCpuLimitInUs(isConstantCall, + rootRepository.getDynamicPropertiesStore().getMaxCpuTimeOfOneTx(), + getCpuLimitInUsRatio(), CommonParameter.getInstance().getConstantCallTimeoutMs()); long vmStartInUs = System.nanoTime() / VMConstant.ONE_THOUSAND; long vmShouldEndInUs = vmStartInUs + thisTxCPULimitInUs; ProgramInvoke programInvoke = ProgramInvokeFactory @@ -699,6 +691,14 @@ private double getCpuLimitInUsRatio() { return cpuLimitRatio; } + static long calculateCpuLimitInUs(boolean isConstantCall, long maxCpuTimeOfOneTxMs, + double cpuLimitInUsRatio, long constantCallTimeoutMs) { + if (isConstantCall && constantCallTimeoutMs > 0L) { + return constantCallTimeoutMs * VMConstant.ONE_THOUSAND; + } + return (long) (maxCpuTimeOfOneTxMs * VMConstant.ONE_THOUSAND * cpuLimitInUsRatio); + } + public long getTotalEnergyLimitWithFixRatio(AccountCapsule creator, AccountCapsule caller, TriggerSmartContract contract, long feeLimit, long callValue) throws ContractValidateException { diff --git a/actuator/src/test/java/org/tron/core/actuator/VMActuatorTest.java b/actuator/src/test/java/org/tron/core/actuator/VMActuatorTest.java new file mode 100644 index 00000000000..240c606e2e9 --- /dev/null +++ b/actuator/src/test/java/org/tron/core/actuator/VMActuatorTest.java @@ -0,0 +1,23 @@ +package org.tron.core.actuator; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class VMActuatorTest { + + @Test + public void testConstantCallUsesConfiguredTimeoutVerbatim() { + assertEquals(123_000L, VMActuator.calculateCpuLimitInUs(true, 80L, 5.0, 123L)); + } + + @Test + public void testConstantCallWithoutConfiguredTimeoutUsesNetworkDeadline() { + assertEquals(400_000L, VMActuator.calculateCpuLimitInUs(true, 80L, 5.0, 0L)); + } + + @Test + public void testNonConstantCallIgnoresConfiguredTimeout() { + assertEquals(400_000L, VMActuator.calculateCpuLimitInUs(false, 80L, 5.0, 123L)); + } +} diff --git a/common/src/main/java/org/tron/common/parameter/CommonParameter.java b/common/src/main/java/org/tron/common/parameter/CommonParameter.java index 30e26d89b26..ab48e0c9442 100644 --- a/common/src/main/java/org/tron/common/parameter/CommonParameter.java +++ b/common/src/main/java/org/tron/common/parameter/CommonParameter.java @@ -64,8 +64,8 @@ public class CommonParameter { * functions, estimateenergy, eth_call, eth_estimateGas, and any other * RPC routed through Wallet#callConstantContract. 0 = use the same * deadline as block processing (current behaviour). When operators set - * this in config the value must be positive; validated at config-load - * in VmConfig. + * this in config the value must be positive and fit VM deadline conversion; + * validated at config-load in VmConfig. */ @Getter @Setter diff --git a/common/src/main/java/org/tron/core/config/args/VmConfig.java b/common/src/main/java/org/tron/core/config/args/VmConfig.java index 712e3375e1c..00ba85aa6cc 100644 --- a/common/src/main/java/org/tron/core/config/args/VmConfig.java +++ b/common/src/main/java/org/tron/core/config/args/VmConfig.java @@ -9,7 +9,8 @@ /** * VM configuration bean. Field names match config.conf keys under the "vm" section. - * Bound automatically via ConfigBeanFactory — no manual key constants needed. + * Most fields are bound automatically via ConfigBeanFactory; opt-in fields that + * must stay absent from reference.conf are bound manually after hasPath checks. */ @Slf4j @Getter @@ -17,6 +18,7 @@ public class VmConfig { private static final String CONSTANT_CALL_TIMEOUT_MS_KEY = "constantCallTimeoutMs"; + static final long MAX_CONSTANT_CALL_TIMEOUT_MS = Long.MAX_VALUE / 1_000L; private boolean supportConstant = false; private long maxEnergyLimitForConstant = 100_000_000L; @@ -71,13 +73,19 @@ private void postProcess(Config vmSection) { // constantCallTimeoutMs is excluded from ConfigBeanFactory binding (no // setter) and intentionally absent from reference.conf, so hasPath alone - // tells us whether the operator opted in. Only positive values are valid. + // tells us whether the operator opted in. Only positive values that can be + // safely converted to microseconds are valid. if (vmSection.hasPath(CONSTANT_CALL_TIMEOUT_MS_KEY)) { long value = vmSection.getLong(CONSTANT_CALL_TIMEOUT_MS_KEY); if (value <= 0L) { throw new IllegalArgumentException( "vm.constantCallTimeoutMs must be > 0 when configured, got " + value); } + if (value > MAX_CONSTANT_CALL_TIMEOUT_MS) { + throw new IllegalArgumentException( + "vm.constantCallTimeoutMs must be <= " + MAX_CONSTANT_CALL_TIMEOUT_MS + + " to fit VM deadline conversion, got " + value); + } constantCallTimeoutMs = value; } } diff --git a/common/src/test/java/org/tron/core/config/args/VmConfigTest.java b/common/src/test/java/org/tron/core/config/args/VmConfigTest.java index f63b43a4bfc..e406ef24e7b 100644 --- a/common/src/test/java/org/tron/core/config/args/VmConfigTest.java +++ b/common/src/test/java/org/tron/core/config/args/VmConfigTest.java @@ -91,9 +91,10 @@ public void testEstimateEnergyMaxRetryBoundaryValues() { // =========================================================================== // Constant-call timeout (issue #6681). The validation rule: any positive - // value is accepted, but zero/negative is rejected ONLY when the operator - // explicitly set the property in their config. Absence keeps the in-Java - // default (0L = "share the block-processing deadline"). + // value that fits VM deadline conversion is accepted, but zero/negative is + // rejected ONLY when the operator explicitly set the property in their + // config. Absence keeps the in-Java default (0L = "share the + // block-processing deadline"). // =========================================================================== @Test @@ -139,4 +140,16 @@ public void testConstantCallTimeoutNegativeRejected() { ex.getMessage().contains("constantCallTimeoutMs")); } } + + @Test + public void testConstantCallTimeoutOverflowRejected() { + long value = VmConfig.MAX_CONSTANT_CALL_TIMEOUT_MS + 1L; + try { + VmConfig.fromConfig(withRef("vm { constantCallTimeoutMs = " + value + " }")); + org.junit.Assert.fail("expected IllegalArgumentException for overflowing ms"); + } catch (IllegalArgumentException ex) { + org.junit.Assert.assertTrue(ex.getMessage(), + ex.getMessage().contains("deadline conversion")); + } + } } diff --git a/framework/src/main/resources/config.conf b/framework/src/main/resources/config.conf index 2a0c21809af..9c8ab69a9e5 100644 --- a/framework/src/main/resources/config.conf +++ b/framework/src/main/resources/config.conf @@ -715,12 +715,12 @@ vm = { # triggerconstantcontract, triggersmartcontract dispatched to view/pure # functions, estimateenergy, eth_call, eth_estimateGas, and any other RPC # routed through the constant-call path. When set, must be a positive - # integer and is used verbatim as the per-call deadline (no clamp against - # the network's maxCpuTimeOfOneTx). Omit the property entirely to keep the - # default behaviour of sharing the block-processing deadline. Migration - # note: if previously running --debug to extend constant calls, switch to - # this option (--debug also extends block-processing, which is unsafe; see - # issue #6266). + # integer that fits VM deadline conversion and is used verbatim as the + # per-call deadline (no clamp against the network's maxCpuTimeOfOneTx). + # Omit the property entirely to keep the default behaviour of sharing the + # block-processing deadline. Migration note: if previously running --debug + # to extend constant calls, switch to this option (--debug also extends + # block-processing, which is unsafe; see issue #6266). # constantCallTimeoutMs = 100 }