Repository navigation
[Feature] Standardize precompile execution with fine-grained timeout checks #6697
Description
Activity
@yanghang8612 Thanks for the detailed analysis. Aligning the timeout handling across parallel precompiles makes the codebase much more predictable for anyone building on or auditing the core logic. I especially like the idea of a 'shared helper' to standardize the execution pattern for future parallel precompile tasks. This kind of symmetry is key to a robust VM implementation.
Really appreciate that the test plan already covers both sides of the timeout path — "latch exhausted →
notEnoughTime" and "cancelled futures reported as cancelled". That second one is a nice touch in particular, since it pins down the actualcancel(true)behavior rather than just the exception being thrown. If useful, two low-priority scenarios that might complement those nicely:1. Queued tasks after cancel. Since
workersInNonConstantCallis a fixed 5-thread pool and shielded transactions can easily submit more tasks than that, some do end up queued in practice. Wondering if it'd be worth a quick probe (e.g., anAtomicIntegerincremented inside a test-only task subclass'scall()) to confirm queued-but-not-yet-started tasks really don't run once the timeout fires. Would turn the "releases worker slots" benefit from a structural claim into a verifiable fact. Totally optional — feel free to skip if it feels like overkill for this change.2. Mixed completion states at timeout. Could be nice to have a scenario or two where
await(...) == falsehappens while some futures have already returned — one with all-truepartials, one with mixedtrue/false. Mostly a safety net: guards against a future refactor accidentally letting control slip to thecheckResultfold orinsertLeaveswhen the latch already timed out. Not strictly needed to validate this change, more about locking in the eager-throw contract for later.Either way, really looking forward to seeing this one land. 🚀
@freddie-lele Thanks! Glad the symmetry framing lands. Given the point about future parallel-verification precompiles inheriting the same semantics, I'll fold both call-sites onto the shared helper in the same PR rather than leaving it as an optional follow-up.
@alan-eth Thanks — both are worth having and I'll fold them into the test plan.
For (1), I'll submit more subtasks than
workersInNonConstantCallhas threads and use a test-only subclass with anAtomicInteger.incrementAndGet()at the top ofcall(), then assert the counter stays ≤ pool size after the latch timeout fires.FutureTask.cancel(true)flips queued tasks to CANCELLED before a worker dequeues them, socall()shouldn't be entered for anything still queued — the counter makes that observable.For (2), I'll add both the all-
trueand mixedtrue/falsepartial cases withawait(...) == false, assertingProgram.Exception.notEnoughTimesurfaces and neither thecheckResultfold norinsertLeavesruns.Really appreciate the careful read.
@yanghang8612
One implementation detail looks worth pinning down before the helper lands.Today the observable difference between these two precompiles is not quite "BatchValidateSign already surfaces OOT while VerifyTransferProof does not".
BatchValidateSign.doExecute()does throw on latch timeout, butBatchValidateSign.execute()currently catchesThrowableand returns zero bytes, so theOutOfTimeExceptiondoes not escape the public precompile boundary either.VerifyTransferProof.execute()also swallowsThrowable. From the VM call-site, the real divergence today is thatVerifyTransferProofcan still block the block-processing thread onfuture.get()after the CPU budget is already exhausted, whileBatchValidateSignavoids that extra wait.That suggests a couple of things for the shared helper / PR shape:
- If the intended end-state is "the precompile itself raises OOT", both
execute()wrappers need to passthroughOutOfTimeExceptionexplicitly (similar toValidateMultiSign). Otherwise the helper can throw internally and still get swallowed at the outer boundary. - The cancel path should also cover
getCPUTimeLeftInNanoSecond()itself throwing beforeawait()begins. In that case the futures have already been submitted, so cancelling only onawait(...) == falsestill leaves submitted work running. - I would also cancel outstanding futures on
InterruptedExceptionfor the same resource-release reason.
One nuance on the proposed queued-task test: with the current input constraints, a single
VerifyTransferProofcall can submit at most 5 tasks (2 spend + 2 output + 1 binding), which exactly matches the fixed pool size of 5. So deterministic per-call queueing is not observable through the existing end-to-end input shape. If we want to verify queued futures becomingCANCELLED, a small package-private helper test with a dedicated tiny executor would be much more deterministic than an end-to-end proof test, and it avoids flakiness from the shared static pools.Also, for the PR description I’d phrase
cancel(true)as best-effort reclamation for running native verification calls, while treating “queued tasks do not start” as the stronger guarantee.- If the intended end-state is "the precompile itself raises OOT", both
Additional concern: solidity-node vs full-node state divergence from the precompile-internal timeout
Before landing the proposed change, I'd like to flag a pre-existing asymmetry that this PR would unintentionally extend to
VerifyTransferProof.TVM has two independent wall-clock timeout checks:
-
Opcode-level —
Program.checkCPUTimeLimit(Program.java:1241-1260) short-circuits on solidity nodes and debug mode:if (CommonParameter.getInstance().isDebug()) { return; } if (CommonParameter.getInstance().isSolidityNode()) { return; }
This is intentional: solidity nodes aren't on the consensus path and must reproduce mainnet state regardless of local replay latency.
-
Precompile-internal —
PrecompiledContracts.getCPUTimeLeftInNanoSecond(PrecompiledContracts.java:458-465) has no such short-circuit. It is used byBatchValidateSignat L1091 and byVerifyTransferProofat L1478 to bound aCountDownLatch.await(...).
Because
vmShouldEndInUsis still computed normally on solidity nodes (VMActuator.java:401-405), theawaitcan genuinely time out on a solidity node under GC / disk / scheduling pressure even when the same transaction succeeded on the producing SR.For
BatchValidateSign, the timeout path is:- L1091
awaitreturnswithNoTimeout=false - L1095 throws
notEnoughTime - L1041-1046 outer
catch (Throwable)swallows it and returnsPair.of(true, new byte[32])(all-zero result) - Control returns to the VM loop; the next opcode's
checkCPUTimeLimitshort-circuits on a solidity node, so execution continues with the zero-byte result - The contract branches on a value that differs from mainnet → divergent SSTORE / LOG / REVERT → persisted state diverges from the SR's block
On a full node the same timeout would also land in the outer catch, but the very next
checkCPUTimeLimitthrows OOT and the transaction reverts cleanly — so only the solidity node silently forks.VerifyTransferProofis currently not affected: it ignoreswithNoTimeoutand falls through tofuture.get(), so workers always finish and the real result is returned (solidity node is just slower). This PR's proposal to have it honorwithNoTimeoutand short-circuit on timeout would remove that safety property and introduce the same divergence surface thatBatchValidateSignalready has.Suggested fix
Gate the precompile-internal timeout on the same condition as
checkCPUTimeLimit, so the two checks stay in sync:protected long getCPUTimeLeftInNanoSecond() { CommonParameter p = CommonParameter.getInstance(); if (p.isDebug() || p.isSolidityNode()) { return Long.MAX_VALUE; } long left = getVmShouldEndInUs() * VMConstant.ONE_THOUSAND - System.nanoTime(); if (left <= 0) { throw Program.Exception.notEnoughTime("call"); } return left; }
Effect:
- Full nodes: behavior unchanged — both precompiles still time out as today / as proposed.
- Solidity / debug nodes: precompile workers are awaited to completion, matching the existing
checkCPUTimeLimitshort-circuit semantics. The cost is that a heavy replay transaction can take longer on a solidity node, but that is already the accepted trade-off for every other opcode on the replay path.
This one change covers both precompiles and closes the divergence for
BatchValidateSignat the same time as making theVerifyTransferProofrefactor safe to land.Happy to open a separate issue for the
BatchValidateSignpart if the maintainers prefer to keep this PR focused.-
@yanghang8612 Perfect — thanks for folding both in, and the
FutureTask.cancel(true)framing for queued tasks is exactly right. One small heads-up though: @waynercheung's point below is a useful guardrail on the pool-saturation scenario — a singleVerifyTransferProofcall caps at 5 submitted tasks (2 spend + 2 output + 1 binding), which exactly equalsworkersInNonConstantCall's pool size, so the queued-vs-dequeued distinction isn't really observable through an end-to-end proof-shaped input.@waynercheung Thanks — that tightens the framing more than the issue body does. You're right that "
BatchValidateSignsurfaces OOT,VerifyTransferProofdoes not" isn't the real-world divergence: both precompiles' publicexecute(...)catchThrowableand return zeros, so theOutOfTimeExceptionnever escapes the precompile boundary in either case. The observable divergence at the VM call-site is strictly thatVerifyTransferProofparks the block-processing thread onfuture.get()until the slowest worker returns, whileBatchValidateSigndoes not.I'll rewrite the issue to match:
- Current State. Frame it as "
BatchValidateSign.doExecuteshort-circuits the post-latch path when the CPU budget is exhausted;VerifyTransferProof.executedoes not" — drop thenotEnoughTime-escaping language, since the outerThrowablecatch makes that invisible either way. - Impact. Keep the consensus-neutral claim (still true — zero-bytes return plus opcode-level
checkCPUTimeLimiton the next iteration produce the same receipt either way), but narrow the concrete benefit to "block-thread no longer blocks onfuture.get()past the CPU budget" + "worker cancellation stops orphaned verification computations".
That way the issue's framing matches what the PR will actually assert in code.
- Current State. Frame it as "
@Federico2014 Very real concern, and the asymmetry you're pointing at is already present today on
BatchValidateSign.doExecute— uncritically mirroring the pattern intoVerifyTransferProofextends the same surface to shielded proof verification rather than fixing it.Two things to pin down on the PR:
1. Verify the asymmetry doesn't produce consensus-visible divergence today. The saving grace is that both precompiles' public
execute(...)catchThrowableand return zeros, so a precompile-internalOutOfTimeExceptionnever reachesVMActuator.executeand never writes anything to the receipt. For a full-node vs. solidity-node state disagreement to surface on a consensus-visible field, the precompile would have to mutate state before the latch timeout fires — neither shielded proof verification nor batch signature verification does. I'll add a fixture-level regression test for both precompiles: drivegetCPUTimeLeftInNanoSecondto zero, run on a solidity-node-flagged VM, and assertcontractResult/energyUsed/ state / logs match the full-node replay byte-for-byte.2. If (1) holds, keep this issue's scope narrow. Aligning the two precompiles stays consensus-neutral, and the pool-thread fix is the real behavior change on the full-node side. If (1) surfaces any divergence, the fix is bigger than this issue — it'd mean adding the
isSolidityNode()short-circuit inside the precompile latch as well, not just at the opcode check — and I'd raise that as a separate follow-up rather than fold it in.Will confirm (1) with the fixture before opening the PR. Good catch either way.
@alan-eth Good heads-up. Counting submitted tasks for a single shielded transaction: 2 × spend + 2 × output + 1 × binding = 5, exactly
workersInNonConstantCall. So through a real proof-shaped input, nothing queues and the "queued-but-not-dequeued" invariant isn't observable end-to-end.I'll still keep the
AtomicIntegersubclass test for the mechanical invariant, but drive it through a syntheticsubmit()loop that oversubscribes the pool beyond 5 rather than through a shapedShieldedTRC20input — same assertion, just not claiming it reflects a real-world scenario. ForBatchValidateSignthe queued case is observable with a well-shaped input (N can be large), so the real-input coverage lives there.Quick update: after reviewing the v4.8.2 (Pyrrho) release tracking, we've decided not to include this change in v4.8.2.
Reason: while the refactor itself is small and self-contained, v4.8.2's scope is already locked (the
release_v4.8.2branch has been cut fromdevelop). We'd prefer to bundle this with a broader CPU-budget / precompile-timeout review in the next release rather than slipping it in late. The issue stays open and the work can still proceed in parallel so it's ready when the v4.8.3 window opens.A quick update on this issue: this improvement is planned for the next java-tron release. Development and related validation are currently underway, and the final implementation may still be adjusted as the work progresses.
- changed the title
[-][Feature] Align timeout and task-cancellation handling across parallel verification precompiles[/-][+][Feature] Standardize precompile execution with fine-grained timeout checks[/+]on Sep 8, 2026 Following a source review of the 34 precompile implementations and their relevant helpers, I have refined the issue body to distinguish common execution standardization from implementation-specific checkpoint work. The current title still describes the scope well.
The inventory below was cross-checked against the relevant code on
developat57b7b04. These groups reflect execution structure and proposed engineering work; their ordering is not a measured performance ranking.A. Internal execution and lifecycle work — 9 precompiles
Precompile(s) Proposed work BatchValidateSign,VerifyTransferProofIntegrate the shared parallel-task lifecycle, remaining-budget waits, result collection, exception propagation, and cleanup. Also cover the serial execution path where applicable and subsequent processing stages. ValidateMultiSignAdd consistent checkpoints between signature-processing units while preserving existing validation and activation rules. BN128Pairing,Blake2FIntegrate checkpoints into the computation helpers at suitable work boundaries. Select the frequency through benchmarks rather than checking only the outer precompile wrapper. VerifyMintProof,VerifyBurnProofAdd checks between verification stages and preserve native-context cleanup. Apply the shared Merkle-processing policy where that stage is used. RewardBalanceCarry the execution policy into reward-calculation helpers and check between collection-processing units that include storage access. ModExpCover parsing, computation, and result-processing boundaries using the common policy. Any deeper computation changes need a separate assessment of library support, correctness, and cost. B. Further checkpoint-granularity evaluation — 4 precompiles
Precompile(s) Proposed assessment Sha256,Ripempd160,EthRipemd160Evaluate chunked digest processing under the shared deadline, retaining the exact existing algorithms, crypto-engine selection, and output semantics. BN128MultiplicationEvaluate whether additional checkpoints within the bounded arithmetic work are worthwhile beyond common entry/exit checks. C. Common-wrapper integration first — 21 precompiles
Precompile(s) Initial treatment Identity,ECRecover,BN128Addition,MerkleHash,P256VerifyUse the common execution boundary for direct-return or fixed-work operations. Preserve ordinary result/error behavior and consistent timeout propagation wherever checkpoints are introduced. VoteCount,UsedVoteCountKeep the small in-memory traversals simple; add internal checks only if their measured cost justifies them. AvailableUnfreezeV2Size,ExpireUnfreezeBalanceV2Include the associated collection helpers in the review, with checkpoint frequency appropriate to normal bounded state. TotalVoteCount,UnfreezableBalanceV2,DelegatableResource,ResourceV2,CheckUnDelegateResource,TotalResourceCover the common boundary and associated account/resource helpers; avoid adding clock reads to every small accessor without evidence of benefit. IsSrCandidate,ReceivedVoteCount,GetChainParameter,ResourceUsage,TotalDelegatedResource,TotalAcquiredResourceStart with common entry/exit checks for their fixed set of queries and calculations. Common-wrapper integration is part of the implementation scope for all three groups. Group C does not mean that a precompile is excluded from the change.
Shared integration work
- Establish one invocation-scoped deadline and the applicable VM execution policy, including the context passed to workers.
- Review shared parsing, signature-recovery, proof/Merkle, and cryptographic helpers for checkpoint placement and exception propagation. Preserve initialization behavior and existing protocol rules.
- Preserve the distinction between timeout, interruption, and ordinary verification failure; make task and resource cleanup consistent across exit paths.
- Keep checks cooperative and describe cancellation according to the underlying library's capabilities.
Validation should combine deterministic clock/executor tests, existing functional fixtures, supported execution-mode and replay checks, and benchmarks for checkpoint overhead. The proposed scope does not imply that compatibility has already been established: activation requirements and final checkpoint placement remain to be confirmed by that work. This qualification also supersedes the earlier assumptions in this discussion about consensus neutrality and immediate reclamation of running computations.
The broader inventory makes sense, but rollout granularity still looks important. Applying the common wrapper to all 34 precompiles while also adding internal checkpoints to the heavier implementations makes any timeout-behavior regression difficult to isolate. Would it be safer to split this into a wrapper-only phase with characterization tests for all precompiles, followed by checkpoint/lifecycle changes grouped by execution shape? Any phase that changes an observable timeout result could then be benchmarked and evaluated for fork activation independently.
@lxcmyf That is a reasonable approach. A phased rollout should make behavioral changes easier to review and validate.
We will treat common-wrapper integration and characterization tests as the first phase, then evaluate implementation-specific checkpoint and lifecycle changes separately by execution shape. Any phase that may change observable timeout behavior will require its own benchmarking, compatibility review, and activation assessment before inclusion. This issue can remain the overall tracker while the implementation work is divided into smaller, reviewable changes.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Summary
Standardize execution across all TVM precompiled contracts, using a shared CPU-deadline policy, consistent timeout propagation, and resource cleanup. Add fine-grained internal checkpoints where the work warrants them, including iterative computation, multi-stage verification, and parallel tasks.
The common execution pattern applies to every precompile. Internal checkpoint placement should reflect the cost and structure of each implementation, including its helper methods. Lightweight operations can use common entry/exit checks; more substantial work needs checks between meaningful units of execution.
Motivation
Precompiles cover several execution shapes: fixed-work operations, collection traversal, iterative cryptographic computation, staged library calls, and parallel verification. Their execution wrappers, exception handling, and resource management have evolved separately.
A shared pattern would make the VM's CPU-budget contract easier to implement, review, and test across these shapes.
BatchValidateSignandVerifyTransferProofremain the initial parallel-execution integration points, while the broader source review supports applying the same principles throughout the precompile set.Proposed Solution
1. Establish a common execution lifecycle
Introduce a shared wrapper, for example
execute(...)delegating to a precompile-specificdoExecute(...), with:OutOfTimeExceptionthrough the public execution boundary.Preserve each precompile's ABI, result encoding, and ordinary input-validation and error semantics. Treat elapsed-time enforcement separately from existing fork-gated validation rules.
2. Use one deadline throughout the invocation
Sequential stages, helper methods, parallel workers, and blocking waits should use the same execution deadline. Each wait consumes the remaining budget rather than starting a new one.
Align the clock, units, expiration boundary, and timeout-enable policy with the VM. Validate all supported execution modes, including transaction execution, constant calls, replay, and debug operation. Workers that need checkpoints should receive the invocation's execution context explicitly.
3. Match checkpoint granularity to the work
Use the source inventory in the follow-up analysis to guide implementation:
Review shared parsing, signature, proof, and computation helpers as well as the visible
execute(...)methods. Place checkpoints between meaningful work units and around library calls where supported, while preserving cryptographic and protocol behavior.This is cooperative deadline enforcement. Checkpoint placement and cancellation must respect the capabilities of the underlying computation libraries; immediate interruption of every operation is not an implementation guarantee.
4. Share parallel execution and cleanup
Provide common handling for submission, deadline-aware waiting, result collection, and cancellation. Apply the same deadline throughout this lifecycle and make cleanup cover unsuccessful exits, including interruption and partial submission.
Preserve the caller's interrupt status where applicable, maintain the distinction between timeout and ordinary task failure, and propagate worker exceptions according to the common execution policy. Cancellation of running work is best effort and should be tested separately from cancellation of queued work.
Validation
Impact and Compatibility
The intended outcome is a consistent precompile execution contract, appropriate internal deadline checks, and maintainable task/resource handling.
No precompile address, ABI, or energy-pricing change is intended. Normal successful and ordinary validation behavior should be preserved. Changes to timeout handling and checkpoint placement still require compatibility testing; consensus neutrality and the absence of a fork gate must not be assumed in advance.
The source inventory is an implementation-planning assessment. Final checkpoint placement and scope remain subject to implementation, regression testing, and performance measurements.
References
PrecompiledContracts.java: precompile implementations and common base.VM.javaandProgram.java: VM execution policy.VMActuator.java: execution result handling.Additional Notes