Skip to content

Commit c8ccd9f

Browse files
committed
feat(vm): add node-level vm.constantCallTimeoutMs for constant calls
New config knob extends the TVM execution window for triggerconstantcontract / eth_call only; pairs with a concurrency cap that engages when the configured timeout exceeds the network's current maxCpuTimeOfOneTx. Issue #6681.
1 parent 980c707 commit c8ccd9f

8 files changed

Lines changed: 244 additions & 54 deletions

File tree

actuator/src/main/java/org/tron/core/actuator/VMActuator.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.tron.core.vm.VM;
4646
import org.tron.core.vm.VMConstant;
4747
import org.tron.core.vm.VMUtils;
48+
import org.tron.core.vm.VmTimeoutPolicy;
4849
import org.tron.core.vm.config.ConfigLoader;
4950
import org.tron.core.vm.config.VMConfig;
5051
import org.tron.core.vm.program.Program;
@@ -398,11 +399,11 @@ private void create()
398399
byte[] ops = newSmartContract.getBytecode().toByteArray();
399400
rootInternalTx = new InternalTransaction(trx, trxType);
400401

401-
long maxCpuTimeOfOneTx = rootRepository.getDynamicPropertiesStore()
402-
.getMaxCpuTimeOfOneTx() * VMConstant.ONE_THOUSAND;
403-
long thisTxCPULimitInUs = (long) (maxCpuTimeOfOneTx * getCpuLimitInUsRatio());
402+
long maxCpuTimeOfOneTxMs = rootRepository.getDynamicPropertiesStore().getMaxCpuTimeOfOneTx();
404403
long vmStartInUs = System.nanoTime() / VMConstant.ONE_THOUSAND;
405-
long vmShouldEndInUs = vmStartInUs + thisTxCPULimitInUs;
404+
long vmShouldEndInUs = VmTimeoutPolicy.computeVmShouldEndInUs(
405+
isConstantCall, vmStartInUs, maxCpuTimeOfOneTxMs, getCpuLimitInUsRatio(),
406+
CommonParameter.getInstance().getConstantCallTimeoutMs());
406407
ProgramInvoke programInvoke = ProgramInvokeFactory
407408
.createProgramInvoke(TrxType.TRX_CONTRACT_CREATION_TYPE, executorType, trx,
408409
tokenValue, tokenId, blockCap.getInstance(), rootRepository, vmStartInUs,
@@ -512,12 +513,11 @@ private void call()
512513
energyLimit = getTotalEnergyLimit(creator, caller, contract, feeLimit, callValue);
513514
}
514515

515-
long maxCpuTimeOfOneTx = rootRepository.getDynamicPropertiesStore()
516-
.getMaxCpuTimeOfOneTx() * VMConstant.ONE_THOUSAND;
517-
long thisTxCPULimitInUs =
518-
(long) (maxCpuTimeOfOneTx * getCpuLimitInUsRatio());
516+
long maxCpuTimeOfOneTxMs = rootRepository.getDynamicPropertiesStore().getMaxCpuTimeOfOneTx();
519517
long vmStartInUs = System.nanoTime() / VMConstant.ONE_THOUSAND;
520-
long vmShouldEndInUs = vmStartInUs + thisTxCPULimitInUs;
518+
long vmShouldEndInUs = VmTimeoutPolicy.computeVmShouldEndInUs(
519+
isConstantCall, vmStartInUs, maxCpuTimeOfOneTxMs, getCpuLimitInUsRatio(),
520+
CommonParameter.getInstance().getConstantCallTimeoutMs());
521521
ProgramInvoke programInvoke = ProgramInvokeFactory
522522
.createProgramInvoke(TrxType.TRX_CONTRACT_CALL_TYPE, executorType, trx,
523523
tokenValue, tokenId, blockCap.getInstance(), rootRepository, vmStartInUs,

common/src/main/java/org/tron/common/parameter/CommonParameter.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,23 @@ public class CommonParameter {
5858
@Getter
5959
@Setter
6060
public double maxTimeRatio = calcMaxTimeRatio();
61+
/**
62+
* Max TVM execution time (ms) for constant calls. 0 = use the same
63+
* deadline as block processing (current behaviour). Otherwise must be
64+
* within [CONSTANT_CALL_TIMEOUT_MIN_MS, CONSTANT_CALL_TIMEOUT_MAX_MS];
65+
* range is enforced at config-load in Args.
66+
*/
67+
@Getter
68+
@Setter
69+
public long constantCallTimeoutMs = 0L;
70+
/**
71+
* Max concurrent constant-call executions. Cap is engaged only when
72+
* constantCallTimeoutMs exceeds the baseline default; nodes leaving the
73+
* timeout at its default see no behaviour change.
74+
*/
75+
@Getter
76+
@Setter
77+
public int constantCallMaxConcurrency = 10;
6178
@Getter
6279
@Setter
6380
public boolean saveInternalTx;

common/src/main/java/org/tron/core/config/args/VmConfig.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import lombok.Getter;
66
import lombok.Setter;
77
import lombok.extern.slf4j.Slf4j;
8+
import org.tron.core.vm.VmTimeoutPolicy;
89

910
/**
1011
* VM configuration bean. Field names match config.conf keys under the "vm" section.
@@ -27,6 +28,8 @@ public class VmConfig {
2728
private boolean saveInternalTx = false;
2829
private boolean saveFeaturedInternalTx = false;
2930
private boolean saveCancelAllUnfreezeV2Details = false;
31+
private long constantCallTimeoutMs = 0L;
32+
private int constantCallMaxConcurrency = 10;
3033

3134
/**
3235
* Create VmConfig from the "vm" section of the application config.
@@ -60,5 +63,22 @@ private void postProcess() {
6063
logger.warn("Configuring [vm.saveCancelAllUnfreezeV2Details] won't work as "
6164
+ "vm.saveInternalTx or vm.saveFeaturedInternalTx is off.");
6265
}
66+
67+
// constantCallTimeoutMs: 0 (sentinel = use existing path) or
68+
// [MIN_MS, MAX_MS]. Issue #6681.
69+
if (constantCallTimeoutMs != VmTimeoutPolicy.CONSTANT_CALL_TIMEOUT_UNSET
70+
&& (constantCallTimeoutMs < VmTimeoutPolicy.CONSTANT_CALL_TIMEOUT_MIN_MS
71+
|| constantCallTimeoutMs > VmTimeoutPolicy.CONSTANT_CALL_TIMEOUT_MAX_MS)) {
72+
throw new IllegalArgumentException(
73+
"vm.constantCallTimeoutMs must be 0 or within ["
74+
+ VmTimeoutPolicy.CONSTANT_CALL_TIMEOUT_MIN_MS + ", "
75+
+ VmTimeoutPolicy.CONSTANT_CALL_TIMEOUT_MAX_MS + "], got "
76+
+ constantCallTimeoutMs);
77+
}
78+
if (constantCallMaxConcurrency <= 0) {
79+
throw new IllegalArgumentException(
80+
"vm.constantCallMaxConcurrency must be > 0, got "
81+
+ constantCallMaxConcurrency);
82+
}
6383
}
6484
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package org.tron.core.vm;
2+
3+
import org.tron.common.math.StrictMathWrapper;
4+
5+
/**
6+
* Constants and helpers governing the TVM execution-time deadline for constant
7+
* calls (triggerconstantcontract / eth_call). Lives in the {@code common}
8+
* module so that both {@code VmConfig} (config-time validation) and
9+
* {@code VMActuator} (runtime deadline computation) can reference the same
10+
* policy values without creating a circular dependency.
11+
*
12+
* <p>See issue #6681.
13+
*/
14+
public final class VmTimeoutPolicy {
15+
16+
private VmTimeoutPolicy() {
17+
}
18+
19+
/** Sentinel: 0 means "use existing maxCpuTimeOfOneTx * ratio path". */
20+
public static final long CONSTANT_CALL_TIMEOUT_UNSET = 0L;
21+
22+
/**
23+
* Sanity floor for {@code vm.constantCallTimeoutMs}, enforced at config-load
24+
* in {@code VmConfig.postProcess()}. Below this value the operator has
25+
* almost certainly mistyped — the actual cap-engagement comparison is
26+
* runtime against the network's {@code maxCpuTimeOfOneTx}.
27+
*/
28+
public static final long CONSTANT_CALL_TIMEOUT_MIN_MS = 80L;
29+
30+
/** Hard upper bound, enforced at config-load. */
31+
public static final long CONSTANT_CALL_TIMEOUT_MAX_MS = 500L;
32+
33+
/** Concurrency cap default; engages only when timeout is raised. */
34+
public static final int CONSTANT_CALL_MAX_CONCURRENCY_DEFAULT = 10;
35+
36+
/**
37+
* Compute the absolute deadline (in microseconds since system start) for
38+
* a TVM execution. Block processing always uses
39+
* {@code maxCpuTimeOfOneTxMs * 1000 * cpuLimitRatio}. Constant calls may
40+
* opt into a longer window via {@code vm.constantCallTimeoutMs}; when that
41+
* value is unset, constant calls fall back to the same path as block
42+
* processing.
43+
*
44+
* <p>Centralised here so the block-processing and constant-call branches
45+
* cannot drift; see issue #6681.
46+
*/
47+
public static long computeVmShouldEndInUs(boolean isConstantCall,
48+
long vmStartInUs,
49+
long maxCpuTimeOfOneTxMs,
50+
double cpuLimitRatio,
51+
long configuredConstantCallTimeoutMs) {
52+
long budgetUs;
53+
if (isConstantCall
54+
&& configuredConstantCallTimeoutMs != CONSTANT_CALL_TIMEOUT_UNSET) {
55+
// Operators can only EXTEND the constant-call window, never shrink it
56+
// below the network's current maxCpuTimeOfOneTx. Issue #6681.
57+
long effectiveMs =
58+
StrictMathWrapper.max(configuredConstantCallTimeoutMs, maxCpuTimeOfOneTxMs);
59+
budgetUs = StrictMathWrapper.multiplyExact(effectiveMs, 1_000L);
60+
} else {
61+
long unscaledUs = StrictMathWrapper.multiplyExact(maxCpuTimeOfOneTxMs, 1_000L);
62+
budgetUs = (long) (unscaledUs * cpuLimitRatio);
63+
}
64+
return StrictMathWrapper.addExact(vmStartInUs, budgetUs);
65+
}
66+
67+
/**
68+
* Whether the constant-call concurrency cap should engage. The cap engages
69+
* strictly when the operator's configured timeout exceeds the network's
70+
* current {@code maxCpuTimeOfOneTx}; nodes leaving the timeout at the
71+
* sentinel see no throttling. Issue #6681.
72+
*/
73+
public static boolean isCapEngaged(long configuredConstantCallTimeoutMs,
74+
long networkMaxCpuTimeOfOneTxMs) {
75+
return configuredConstantCallTimeoutMs > networkMaxCpuTimeOfOneTxMs;
76+
}
77+
}

common/src/main/resources/reference.conf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,17 @@ vm = {
709709

710710
# Max retry time for executing transaction in estimating energy
711711
estimateEnergyMaxRetry = 3
712+
713+
# Max TVM execution time (ms) for constant calls (triggerconstantcontract /
714+
# eth_call). 0 = use the same deadline as block processing. When set, must be
715+
# in [80, 500]. See issue #6681. Migration note: if previously running --debug
716+
# to extend constant calls, switch to this option (--debug also extends
717+
# block-processing, which is unsafe; see issue #6266).
718+
constantCallTimeoutMs = 0
719+
720+
# Max concurrent constant-call executions. Engages only when
721+
# constantCallTimeoutMs is raised above 80.
722+
constantCallMaxConcurrency = 10
712723
}
713724

714725
# Governance proposal toggle parameters. All default to 0 (disabled).

framework/src/main/java/org/tron/core/Wallet.java

Lines changed: 80 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import java.util.Map.Entry;
6262
import java.util.Objects;
6363
import java.util.Optional;
64+
import java.util.concurrent.Semaphore;
6465
import java.util.stream.Collectors;
6566
import lombok.Getter;
6667
import lombok.extern.slf4j.Slf4j;
@@ -210,6 +211,7 @@
210211
import org.tron.core.store.VotesStore;
211212
import org.tron.core.store.WitnessStore;
212213
import org.tron.core.utils.TransactionUtil;
214+
import org.tron.core.vm.VmTimeoutPolicy;
213215
import org.tron.core.vm.program.Program;
214216
import org.tron.core.zen.ShieldedTRC20ParametersBuilder;
215217
import org.tron.core.zen.ShieldedTRC20ParametersBuilder.ShieldedTRC20ParametersType;
@@ -279,6 +281,23 @@ public class Wallet {
279281
.fromString("TokenBurn(address,uint256,bytes32[3])"));
280282
private static final String BROADCAST_TRANS_FAILED = "Broadcast transaction {} failed, {}.";
281283

284+
private static volatile Semaphore constantCallSemaphore;
285+
286+
private static Semaphore getConstantCallSemaphore() {
287+
Semaphore s = constantCallSemaphore;
288+
if (s == null) {
289+
synchronized (Wallet.class) {
290+
s = constantCallSemaphore;
291+
if (s == null) {
292+
s = new Semaphore(
293+
CommonParameter.getInstance().getConstantCallMaxConcurrency(), true);
294+
constantCallSemaphore = s;
295+
}
296+
}
297+
}
298+
return s;
299+
}
300+
282301
@Getter
283302
private final SignInterface cryptoEngine;
284303
@Autowired
@@ -3158,53 +3177,69 @@ public Transaction callConstantContract(TransactionCapsule trxCap,
31583177
throw new ContractValidateException("this node does not support constant");
31593178
}
31603179

3161-
Block headBlock;
3162-
List<BlockCapsule> blockCapsuleList = chainBaseManager.getBlockStore()
3163-
.getBlockByLatestNum(1);
3164-
if (CollectionUtils.isEmpty(blockCapsuleList)) {
3165-
throw new HeaderNotFound("latest block not found");
3166-
} else {
3167-
headBlock = blockCapsuleList.get(0).getInstance();
3168-
}
3169-
3170-
BlockCapsule headBlockCapsule = new BlockCapsule(headBlock);
3171-
TransactionContext context = new TransactionContext(headBlockCapsule, trxCap,
3172-
StoreFactory.getInstance(), true, false);
3173-
VMActuator vmActuator = new VMActuator(true);
3174-
3175-
vmActuator.validate(context);
3176-
vmActuator.execute(context);
3177-
3178-
ProgramResult result = context.getProgramResult();
3179-
if (!isEstimating && result.getException() != null
3180-
|| result.getException() instanceof Program.OutOfTimeException) {
3181-
RuntimeException e = result.getException();
3182-
logger.warn("Constant call failed for reason: {}", e.getMessage());
3183-
throw e;
3184-
}
3185-
3186-
TransactionResultCapsule ret = new TransactionResultCapsule();
3187-
builder.setEnergyUsed(result.getEnergyUsed());
3188-
builder.setEnergyPenalty(result.getEnergyPenaltyTotal());
3189-
builder.addConstantResult(ByteString.copyFrom(result.getHReturn()));
3190-
result.getLogInfoList().forEach(logInfo ->
3191-
builder.addLogs(LogInfo.buildLog(logInfo)));
3192-
result.getInternalTransactions().forEach(it ->
3193-
builder.addInternalTransactions(buildInternalTransaction(it)));
3194-
ret.setStatus(0, code.SUCESS);
3195-
if (StringUtils.isNoneEmpty(result.getRuntimeError())) {
3196-
ret.setStatus(0, code.FAILED);
3197-
retBuilder
3198-
.setMessage(ByteString.copyFromUtf8(result.getRuntimeError()))
3199-
.build();
3180+
long networkMaxCpuTimeOfOneTxMs = chainBaseManager.getDynamicPropertiesStore()
3181+
.getMaxCpuTimeOfOneTx();
3182+
boolean throttled = VmTimeoutPolicy.isCapEngaged(
3183+
CommonParameter.getInstance().getConstantCallTimeoutMs(), networkMaxCpuTimeOfOneTxMs);
3184+
Semaphore sem = throttled ? getConstantCallSemaphore() : null;
3185+
if (sem != null && !sem.tryAcquire()) {
3186+
throw new ContractValidateException(
3187+
"too many concurrent constant calls; configured cap is "
3188+
+ CommonParameter.getInstance().getConstantCallMaxConcurrency());
32003189
}
3201-
if (result.isRevert()) {
3202-
ret.setStatus(0, code.FAILED);
3203-
retBuilder.setMessage(ByteString.copyFromUtf8("REVERT opcode executed"))
3204-
.build();
3190+
try {
3191+
Block headBlock;
3192+
List<BlockCapsule> blockCapsuleList = chainBaseManager.getBlockStore()
3193+
.getBlockByLatestNum(1);
3194+
if (CollectionUtils.isEmpty(blockCapsuleList)) {
3195+
throw new HeaderNotFound("latest block not found");
3196+
} else {
3197+
headBlock = blockCapsuleList.get(0).getInstance();
3198+
}
3199+
3200+
BlockCapsule headBlockCapsule = new BlockCapsule(headBlock);
3201+
TransactionContext context = new TransactionContext(headBlockCapsule, trxCap,
3202+
StoreFactory.getInstance(), true, false);
3203+
VMActuator vmActuator = new VMActuator(true);
3204+
3205+
vmActuator.validate(context);
3206+
vmActuator.execute(context);
3207+
3208+
ProgramResult result = context.getProgramResult();
3209+
if (!isEstimating && result.getException() != null
3210+
|| result.getException() instanceof Program.OutOfTimeException) {
3211+
RuntimeException e = result.getException();
3212+
logger.warn("Constant call failed for reason: {}", e.getMessage());
3213+
throw e;
3214+
}
3215+
3216+
TransactionResultCapsule ret = new TransactionResultCapsule();
3217+
builder.setEnergyUsed(result.getEnergyUsed());
3218+
builder.setEnergyPenalty(result.getEnergyPenaltyTotal());
3219+
builder.addConstantResult(ByteString.copyFrom(result.getHReturn()));
3220+
result.getLogInfoList().forEach(logInfo ->
3221+
builder.addLogs(LogInfo.buildLog(logInfo)));
3222+
result.getInternalTransactions().forEach(it ->
3223+
builder.addInternalTransactions(buildInternalTransaction(it)));
3224+
ret.setStatus(0, code.SUCESS);
3225+
if (StringUtils.isNoneEmpty(result.getRuntimeError())) {
3226+
ret.setStatus(0, code.FAILED);
3227+
retBuilder
3228+
.setMessage(ByteString.copyFromUtf8(result.getRuntimeError()))
3229+
.build();
3230+
}
3231+
if (result.isRevert()) {
3232+
ret.setStatus(0, code.FAILED);
3233+
retBuilder.setMessage(ByteString.copyFromUtf8("REVERT opcode executed"))
3234+
.build();
3235+
}
3236+
trxCap.setResult(ret);
3237+
return trxCap.getInstance();
3238+
} finally {
3239+
if (sem != null) {
3240+
sem.release();
3241+
}
32053242
}
3206-
trxCap.setResult(ret);
3207-
return trxCap.getInstance();
32083243
}
32093244

32103245
public SmartContract getContract(GrpcAPI.BytesMessage bytesMessage) {

framework/src/main/java/org/tron/core/config/args/Args.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
import org.tron.core.config.Parameter.NodeConstant;
7474
import org.tron.core.exception.TronError;
7575
import org.tron.core.store.AccountStore;
76+
import org.tron.core.vm.VmTimeoutPolicy;
7677
import org.tron.p2p.P2pConfig;
7778
import org.tron.p2p.dns.update.DnsType;
7879
import org.tron.p2p.dns.update.PublishConfig;
@@ -212,6 +213,22 @@ private static void applyVmConfig(VmConfig vm) {
212213
PARAMETER.saveInternalTx = vm.isSaveInternalTx();
213214
PARAMETER.saveFeaturedInternalTx = vm.isSaveFeaturedInternalTx();
214215
PARAMETER.saveCancelAllUnfreezeV2Details = vm.isSaveCancelAllUnfreezeV2Details();
216+
PARAMETER.constantCallTimeoutMs = vm.getConstantCallTimeoutMs();
217+
PARAMETER.constantCallMaxConcurrency = vm.getConstantCallMaxConcurrency();
218+
219+
if (PARAMETER.constantCallTimeoutMs != VmTimeoutPolicy.CONSTANT_CALL_TIMEOUT_UNSET) {
220+
logger.warn(
221+
"vm.constantCallTimeoutMs is configured to {} ms; the concurrency"
222+
+ " cap of {} will engage whenever this value exceeds the"
223+
+ " network's current maxCpuTimeOfOneTx (operators can only"
224+
+ " extend, not shrink, the constant-call window)."
225+
+ " Tune with vm.constantCallMaxConcurrency. See issue #6681."
226+
+ " If you previously ran with --debug solely for this purpose,"
227+
+ " switch to vm.constantCallTimeoutMs (--debug also extends"
228+
+ " block-processing, which is unsafe; see issue #6266).",
229+
PARAMETER.constantCallTimeoutMs,
230+
PARAMETER.constantCallMaxConcurrency);
231+
}
215232
}
216233

217234
// Old applyStorageConfig removed — merged into applyStorageConfig()

0 commit comments

Comments
 (0)