feat(vm): implement TIP-854 canonicalize sign-precompile calldata#6715
Merged
CodeNinjaEvan merged 3 commits intoMay 9, 2026
Merged
Conversation
68dc311 to
b4a4cf2
Compare
…alidateSign under Osaka Reject calldata that doesn't fit the (words - H) / I shape (H=5, I=5/6) inside execute(); rejected inputs return Pair.of(false, EMPTY_BYTE_ARRAY). getEnergyForData unchanged.
… precompiles Add Osaka-gated rejection cases (mis-aligned, short head, bad tail, null) and a Program#callToPrecompiledAddress integration test pinning outer-frame containment.
b4a4cf2 to
a5f1718
Compare
aiden3885
approved these changes
May 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Implements TIP-854 (Canonicalize calldata for signature-verification precompiles) for java-tron, gated behind the existing
ALLOW_TVM_OSAKAhardfork.ValidateMultiSign.executeandBatchValidateSign.doExecute. The guard rejects when, withW = 32, any of the following holds:data == null,data.length % W != 0,data.length <= H*W, or(data.length - H*W) % (I*W) != 0.(H, I)are the same constants the per-call energy formula(words - H) / Ialready bakes in:(5, 5)forvalidateMultiSign,(5, 6)forbatchValidateSign. Header-only calldata, i.e. empty signature arrays, is intentionally rejected.executereturns(false, empty)without invoking the decoder and without performing anyecrecover. The invoking call frame — reachable through any ofCALL/CALLTOKEN/STATICCALL/DELEGATECALL/CALLCODE— consumes its pre-allocated energy, the stack receives0, memory receives no return data, and the outer transaction continues with its remaining budget intact.length == H*W + I*W*Nfor someN >= 1), the new rule is a no-op: execution proceeds into the existing decoder exactly as before. Pre-activation behaviour, including the per-call energy cost, is byte-for-byte unchanged.committee.*key /CommonParameter/Argsplumbing — reuses the already-wired Osaka gate.Why are these changes required?
The two precompiles charge energy under a fixed positive-tail total-length assumption — the pricing formula treats calldata as a static head followed by exactly
Nequally-sized tail slots — but the existing execute path does not enforce that same total-length predicate before decoding. The decoder follows whatever offsets calldata supplies and silently zero-pads any missing bytes throughArrays.copyOfRange. As a result, the set of byte lengths accepted by execution is a strict superset of the lengths pricing has been evaluated for: non-word-aligned trailing bytes are dropped, inputs shorter than or equal to the static head are zero-padded out or treated as empty arrays, and tails that don't decompose into an integer number of items still flow through. This makes the precompiles harder to reason about for wallets, SDKs, indexers, audits, and formal specifications. This PR closes that total-length gap by rejecting calldata whose byte length is outside the positive-tail predicate.This PR has been tested by:
ValidateMultiSignContractTest: rejects malformed calldata across four buckets (non-32-aligned tail, fewer thanHwords, header-only empty-array calldata, aligned-but-bad-tail) plusnull; positive-tail total-length input behaviour identical pre- vs post-activation; pre-activation does not take the new reject path.BatchValidateSignContractTest: same four shapes, parameterised for(H, I) = (5, 6); the positive-tail case uses real 65-byte signatures so eachbyteselement encodes in exactly four words.OperationsTest.testTip854OuterFrameContainment: drives both precompiles throughProgram.callToPrecompiledAddresswith malformed calldata underOp.CALLand asserts (a) no exception propagates to the outer frame, (b) the inner CALL pushes0, (c) no return data is exposed or copied to output memory, (d) the pre-allocated call energy is consumed, and (e) the outer frame keeps executing afterwards../gradlew :actuator:compileJava :framework:compileTestJava— OK../gradlew :framework:test --tests "*ValidateMultiSignContractTest" --tests "*BatchValidateSignContractTest" --tests "*OperationsTest.testTip854*"— all pass.Follow up
abi.encodeconformance) is intentionally out of scope per the TIP and can be addressed in a follow-up if desired.