Skip to content

feat(vm): implement TIP-854 canonicalize sign-precompile calldata#6715

Open
yanghang8612 wants to merge 2 commits intotronprotocol:developfrom
yanghang8612:feat/precompile-canonical-input
Open

feat(vm): implement TIP-854 canonicalize sign-precompile calldata#6715
yanghang8612 wants to merge 2 commits intotronprotocol:developfrom
yanghang8612:feat/precompile-canonical-input

Conversation

@yanghang8612
Copy link
Copy Markdown
Collaborator

What does this PR do?

Implements TIP-854 (Canonicalize calldata for signature-verification precompiles) for java-tron, gated behind the existing ALLOW_TVM_OSAKA hardfork.

  • Adds a calldata-length guard at the top of ValidateMultiSign.execute and BatchValidateSign.doExecute. The guard rejects when, with W = 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) / I already bakes in: (5, 5) for validateMultiSign, (5, 6) for batchValidateSign.
  • On reject: execute returns (false, empty) without invoking the decoder and without performing any ecrecover. The invoking call frame — reachable through any of CALL / CALLTOKEN / STATICCALL / DELEGATECALL / CALLCODE — consumes its pre-allocated energy, the stack receives 0, memory receives no return data, and the outer transaction continues with its remaining budget intact.
  • For canonically-shaped calldata (length == H*W + I*W*N for some N >= 0), the new rule is a no-op. Pre-activation behaviour, including the per-call energy cost, is byte-for-byte unchanged.
  • No new proposal / committee.* key / CommonParameter / Args plumbing — 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 N equally-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 through Arrays.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:

  • Unit Tests
    • ValidateMultiSignContractTest (3 new cases): rejects malformed calldata across three buckets (non-32-aligned tail, fewer than H words, aligned-but-bad-tail) plus null; 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 each bytes element encodes in exactly four words.
    • OperationsTest.testTip854OuterFrameContainment: drives both precompiles through Program.callToPrecompiledAddress with malformed calldata under Op.CALL and asserts (a) no exception propagates to the outer frame, (b) the inner CALL pushes 0, (c) the outer frame keeps executing afterwards.
  • Manual Testing
    • ./gradlew :actuator:compileJava :framework:compileTestJava — OK.
    • ./gradlew :framework:test --tests "*ValidateMultiSignContractTest" --tests "*BatchValidateSignContractTest" --tests "*OperationsTest.testTip854*" — all pass.

Follow up

  • Validation of inner dynamic offsets and any further decoder hardening (abi.encode conformance) is intentionally out of scope per the TIP and can be addressed in a follow-up if desired.

…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.
@github-actions github-actions Bot requested a review from CodeNinjaEvan April 28, 2026 08:54
@halibobo1205 halibobo1205 added the topic:vm VM, smart contract label Apr 28, 2026
@halibobo1205 halibobo1205 added this to the GreatVoyage-v4.8.2 milestone Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

topic:vm VM, smart contract

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants