Skip to content

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

Merged
CodeNinjaEvan merged 3 commits into
tronprotocol:developfrom
yanghang8612:feat/precompile-canonical-input
May 9, 2026
Merged

feat(vm): implement TIP-854 canonicalize sign-precompile calldata#6715
CodeNinjaEvan merged 3 commits into
tronprotocol:developfrom
yanghang8612:feat/precompile-canonical-input

Conversation

@yanghang8612
Copy link
Copy Markdown
Collaborator

@yanghang8612 yanghang8612 commented Apr 28, 2026

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. Header-only calldata, i.e. empty signature arrays, is intentionally rejected.
  • 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 calldata satisfying the positive-tail total-length predicate (length == H*W + I*W*N for some N >= 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.
  • 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 positive-tail total-length assumption — the pricing formula treats calldata as a static head followed by exactly N equally-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 through Arrays.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:

  • Unit Tests
    • ValidateMultiSignContractTest: rejects malformed calldata across four buckets (non-32-aligned tail, fewer than H words, header-only empty-array calldata, aligned-but-bad-tail) plus null; 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 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) 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.
  • 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, array lengths, per-element 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.

@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
@yanghang8612 yanghang8612 force-pushed the feat/precompile-canonical-input branch 4 times, most recently from 68dc311 to b4a4cf2 Compare May 6, 2026 08:36
…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.
@yanghang8612 yanghang8612 force-pushed the feat/precompile-canonical-input branch from b4a4cf2 to a5f1718 Compare May 6, 2026 08:39
@yanghang8612 yanghang8612 requested a review from aiden3885 May 8, 2026 07:54
Copy link
Copy Markdown
Collaborator

@CodeNinjaEvan CodeNinjaEvan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@CodeNinjaEvan CodeNinjaEvan merged commit e178733 into tronprotocol:develop May 9, 2026
18 checks passed
@github-project-automation github-project-automation Bot moved this to Done in java-tron May 9, 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

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants