Skip to content

COINBASE opcode unit test asserts wrong value (getLast20Bytes vs actual 21-byte push) #6831

@FinchAlex

Description

@FinchAlex

Bug Description

The unit test for the COINBASE (0x41) opcode in OperationsTest.java asserts against getLast20Bytes(), but the actual coinBaseAction implementation pushes the full 21-byte TRON address (including the 0x41 prefix) without calling getLast20Bytes().

This creates a misleading discrepancy: the test says COINBASE returns 20 bytes (no prefix), but production code returns 21 bytes (with prefix). The test only passes by accident because the mock coinbase value has zeros in the prefix position.

The asymmetry is intentional in production code -- coinBaseAction deliberately differs from addressAction/callerAction/originAction which all call getLast20Bytes(). But the test doesn't reflect this.

Production code (OperationActions.java)

// ADDRESS, CALLER, ORIGIN -- strip 0x41 prefix:
public static void addressAction(Program program) {
    DataWord address = program.getContractAddress();
    if (VMConfig.allowMultiSign()) {
        address = new DataWord(address.getLast20Bytes()); // strips prefix
    }
    program.stackPush(address);
}

// COINBASE -- does NOT strip prefix:
public static void coinBaseAction(Program program) {
    DataWord coinbase = program.getCoinbase();
    program.stackPush(coinbase);  // pushes full 21-byte DataWord, 0x41 at byte[11]
}

Test code (OperationsTest.java)

// COINBASE test -- asserts getLast20Bytes() (doesn't match production):
Assert.assertEquals(new DataWord(invoke.getCoinbase().getLast20Bytes()),
    program.getStack().pop());

Environment

Network: N/A (unit test issue)

Software Versions

OS: N/A
JVM: N/A
Git Commit: master (also present in 4.7.6+, likely all versions)
Version: N/A
Code: N/A

Expected Behavior

The COINBASE test should assert the actual value pushed by coinBaseAction:

Assert.assertEquals(invoke.getCoinbase(), program.getStack().pop());

Or at minimum, the test should use a non-zero coinbase address where getLast20Bytes() produces a different result from the full DataWord, so the assertion would catch the discrepancy.

Actual Behavior

The test asserts getLast20Bytes() which strips the 0x41 prefix. This passes only because the mock coinbase is all-zeros (or has zeros at the prefix byte position), making getLast20Bytes() and the full DataWord equivalent.

Frequency

  • Always (100%)

Steps to Reproduce

  1. Open framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java
  2. Find the COINBASE (0x41) test section
  3. Compare the assertion with coinBaseAction in OperationActions.java
  4. Observe that the test uses getLast20Bytes() but the production code does not

To demonstrate concretely: change the test's mock invoke to use a coinbase with a non-zero first byte (e.g., 0x41...). The assertion will fail because getLast20Bytes() drops byte[11] (0x41) while the actual opcode preserves it.

Additional Context

Impact

This misleading test caused a bug in a third-party TVM implementation. The implementor followed the test's implication that COINBASE strips the 0x41 prefix (like ADDRESS/CALLER/ORIGIN), when in fact it preserves it. The bug was only caught during mainnet block replay at block 3,270,871 where a contract uses COINBASE as a randomness seed -- the different prefix handling changed the SHA3 hash, causing execution to diverge.

Suggested fix

// Option 1: Fix assertion to match production behavior
Assert.assertEquals(invoke.getCoinbase(), program.getStack().pop());

// Option 2: Use non-zero coinbase in mock so getLast20Bytes() != full DataWord
// This would have caught the discrepancy immediately

A comment explaining the intentional asymmetry between COINBASE and other address opcodes would also help future implementors.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions