Skip to content

TIP-871: Canonicalize MODEXP output length when modulus is zero #871

@yanghang8612

Description

@yanghang8612
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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions