feat(vm): implement TIP-854 canonicalize sign-precompile calldata#6715
Open
yanghang8612 wants to merge 2 commits intotronprotocol:developfrom
Open
feat(vm): implement TIP-854 canonicalize sign-precompile calldata#6715yanghang8612 wants to merge 2 commits intotronprotocol:developfrom
yanghang8612 wants to merge 2 commits intotronprotocol:developfrom
Conversation
…alidateSign under Osaka Under allowTvmOsaka, both precompiles now reject calldata that does not match the shape implied by the existing getEnergyForData formula (words - H) / I: the byte length must be a multiple of 32, at least as long as the H-word static head, and the remaining tail must be divisible by the per-item width (H=5, I=5 for validateMultiSign; H=5, I=6 for batchValidateSign). The check lives only in execute(...). Rejected calldata returns Pair.of(false, EMPTY_BYTE_ARRAY), so the enclosing CALL is taken through the normal precompile-failure path in Program#callToPrecompiledAddress: refundEnergy(0) leaves the CALL's pre-allocated energy consumed, the stack receives 0 and memory gets no return data, just like any other unsuccessful inner call. That is strictly better than the pre-Osaka behaviour where malformed calldata surfaced as an AIOOBE, escaped through the RuntimeException handler in VM#play, and triggered spendAllEnergy - draining the whole enclosing transaction rather than only this CALL frame. Returning false also short-circuits the success-branch refund, so the legacy formula's negative result on too-short calldata can never be observed as an energy credit. getEnergyForData and the energy formula are unchanged.
… precompiles Add regression tests for the Osaka-gated length guard on validateMultiSign and batchValidateSign. Each precompile's existing unit test file gets three new @test methods covering the four buckets from the TIP discussion: - non-32-aligned length (r = 1, 31) - fewer than H = 5 static-head words (lengths 0, 32, 64, 96, 128) - 32-aligned but tail not a multiple of I (I = 5 / 6) - null calldata - canonicalized well-formed input unchanged pre- vs post-activation - pre-activation behaviour (legacy decoder path / outer catch) preserved OperationsTest gets one integration test that drives the reject through Program#callToPrecompiledAddress and asserts the outer-frame containment invariant: inner CALL pushes 0, the outer frame sees no propagated exception, and subsequent stack operations still succeed. Every test resets allowTvmOsaka to 0 in finally so state does not leak to neighbouring tests.
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.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 >= 0), the new rule is a no-op. 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 shape assumption — the pricing formula treats calldata as a static head followed by exactly
Nequally-sized tail items — but the existing execute path does not enforce the same shape 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 strings accepted is a strict superset of the shapes pricing has ever been evaluated for: non-word-aligned trailing bytes are dropped, inputs shorter than the static head are zero-padded out, 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 the gap by collapsing the accepted set to exactly the shapes pricing already assumes.This PR has been tested by:
ValidateMultiSignContractTest(3 new cases): rejects malformed calldata across three buckets (non-32-aligned tail, fewer thanHwords, aligned-but-bad-tail) plusnull; canonical-input behaviour identical pre- vs post-activation; pre-activation does not take the new reject path.BatchValidateSignContractTest(3 new cases): same three shapes, parameterised for(H, I) = (5, 6); the canonical-input 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) 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.