tip: 871
title: Canonicalize MODEXP output length when modulus is zero
author: yanghang8612@163.com
discussions-to: https://github.com/tronprotocol/tips/issues/871
status: Draft
type: Standards Track
category: VM
created: 2026-05-14
Simple Summary
Make the modExp precompile (0x...05) return a zero-filled byte array of length modLen when the modulus value is zero, instead of an empty byte array. This aligns the precompile's output length with EIP-198 and with every major Ethereum client.
Motivation
EIP-198 defines the MODEXP output as base^exp mod modulus, left-padded to exactly the byte length of the modulus (modLen, the third length header in the calldata). The output is a fixed-length value: callers — and the RETURNDATASIZE / RETURNDATACOPY opcodes — rely on it being modLen bytes regardless of the operand values.
The current java-tron implementation has a special case for a zero modulus value that returns an empty byte array:
// check if modulus is zero
if (isZero(mod)) {
return Pair.of(true, EMPTY_BYTE_ARRAY);
}
When the calldata declares modLen > 0 but the modulus bytes are all zero, java-tron produces 0 bytes of output while Ethereum produces modLen zero bytes. ethereumj carried the identical bug and fixed it in commit d28ba5c ("Fixed: modexp returns incorrect length with 0 modulus"); go-ethereum returns LeftPadBytes([]byte{}, modLen) for the same case.
The effect is a consensus-relevant divergence from Ethereum: a contract that calls MODEXP with a zero modulus observes a different RETURNDATASIZE, and a CALL / STATICCALL to 0x...05 copies a different number of bytes into the caller's memory than it would on Ethereum. This breaks the fixed-length contract that EIP-198 consumers — common in on-chain cryptography libraries — depend on, and makes the precompile harder to reason about for SDKs, audits, and cross-chain tooling.
Specification
Let modLen be the third length header parsed from the precompile calldata, as today.
This change is gated by the ALLOW_TVM_OSAKA hardfork flag (proposal id 96) and ships as part of the Osaka upgrade, alongside the other Osaka VM changes (TIP-7883, TIP-854, TIP-7951, TIP-7939, EIP-7823). No new proposal id or config switch is introduced.
After activation, in modExp's execute, when the parsed modulus value is zero:
- If
VMConfig.allowTvmOsaka() is true, execute returns true with an output of new byte[modLen] — a modLen-length, all-zero byte array.
- Otherwise (pre-Osaka),
execute returns true with the empty byte array, as today.
All other behaviour is unchanged:
- The energy cost (
getEnergyForData) is not modified.
- The non-zero-modulus path, including the existing left-pad-to-
modLen adjustment of the modPow result, is unchanged.
modLen is bounded by the same calldata parsing that already governs the non-zero-modulus left-pad allocation, so no new allocation bound is introduced.
Rationale
A zero modulus makes base^exp mod modulus mathematically undefined, so the returned value is a convention rather than a computed result. EIP-198 nonetheless fixes the output length at modLen unconditionally, and the established cross-client convention is to return modLen zero bytes. Adopting that convention is the minimal change that removes the divergence; choosing any other value or length would itself be a fresh incompatibility.
The fix is deliberately scoped to the single isZero(mod) branch. The pricing formula and the non-zero path already satisfy EIP-198, so no other change is required.
Compatibility
This feature is gated behind the ALLOW_TVM_OSAKA hardfork flag and activates with the Osaka upgrade; it constitutes a hard fork. Pre-activation behaviour is byte-for-byte unchanged.
The only inputs whose observable behaviour changes are MODEXP calls with modLen > 0 and an all-zero modulus value: post-activation they return modLen zero bytes instead of empty output. Calls with modLen == 0 are unaffected (new byte[0] and the empty byte array are equivalent). Any contract that previously relied on the empty-output behaviour for a zero modulus would observe the new, EIP-198-conformant length after activation.
Simple Summary
Make the
modExpprecompile (0x...05) return a zero-filled byte array of lengthmodLenwhen the modulus value is zero, instead of an empty byte array. This aligns the precompile's output length with EIP-198 and with every major Ethereum client.Motivation
EIP-198 defines the MODEXP output as
base^exp mod modulus, left-padded to exactly the byte length of the modulus (modLen, the third length header in the calldata). The output is a fixed-length value: callers — and theRETURNDATASIZE/RETURNDATACOPYopcodes — rely on it beingmodLenbytes regardless of the operand values.The current java-tron implementation has a special case for a zero modulus value that returns an empty byte array:
When the calldata declares
modLen > 0but the modulus bytes are all zero, java-tron produces0bytes of output while Ethereum producesmodLenzero bytes. ethereumj carried the identical bug and fixed it in commitd28ba5c("Fixed: modexp returns incorrect length with 0 modulus"); go-ethereum returnsLeftPadBytes([]byte{}, modLen)for the same case.The effect is a consensus-relevant divergence from Ethereum: a contract that calls MODEXP with a zero modulus observes a different
RETURNDATASIZE, and aCALL/STATICCALLto0x...05copies a different number of bytes into the caller's memory than it would on Ethereum. This breaks the fixed-length contract that EIP-198 consumers — common in on-chain cryptography libraries — depend on, and makes the precompile harder to reason about for SDKs, audits, and cross-chain tooling.Specification
Let
modLenbe the third length header parsed from the precompile calldata, as today.This change is gated by the
ALLOW_TVM_OSAKAhardfork flag (proposal id96) and ships as part of the Osaka upgrade, alongside the other Osaka VM changes (TIP-7883, TIP-854, TIP-7951, TIP-7939, EIP-7823). No new proposal id or config switch is introduced.After activation, in
modExp'sexecute, when the parsed modulus value is zero:VMConfig.allowTvmOsaka()is true,executereturnstruewith an output ofnew byte[modLen]— amodLen-length, all-zero byte array.executereturnstruewith the empty byte array, as today.All other behaviour is unchanged:
getEnergyForData) is not modified.modLenadjustment of themodPowresult, is unchanged.modLenis bounded by the same calldata parsing that already governs the non-zero-modulus left-pad allocation, so no new allocation bound is introduced.Rationale
A zero modulus makes
base^exp mod modulusmathematically undefined, so the returned value is a convention rather than a computed result. EIP-198 nonetheless fixes the output length atmodLenunconditionally, and the established cross-client convention is to returnmodLenzero bytes. Adopting that convention is the minimal change that removes the divergence; choosing any other value or length would itself be a fresh incompatibility.The fix is deliberately scoped to the single
isZero(mod)branch. The pricing formula and the non-zero path already satisfy EIP-198, so no other change is required.Compatibility
This feature is gated behind the
ALLOW_TVM_OSAKAhardfork flag and activates with the Osaka upgrade; it constitutes a hard fork. Pre-activation behaviour is byte-for-byte unchanged.The only inputs whose observable behaviour changes are MODEXP calls with
modLen > 0and an all-zero modulus value: post-activation they returnmodLenzero bytes instead of empty output. Calls withmodLen == 0are unaffected (new byte[0]and the empty byte array are equivalent). Any contract that previously relied on the empty-output behaviour for a zero modulus would observe the new, EIP-198-conformant length after activation.