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
Steps to Reproduce
- Open
framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.java
- Find the COINBASE (0x41) test section
- Compare the assertion with
coinBaseAction in OperationActions.java
- 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.
Bug Description
The unit test for the
COINBASE(0x41) opcode inOperationsTest.javaasserts againstgetLast20Bytes(), but the actualcoinBaseActionimplementation pushes the full 21-byte TRON address (including the 0x41 prefix) without callinggetLast20Bytes().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
coinbasevalue has zeros in the prefix position.The asymmetry is intentional in production code --
coinBaseActiondeliberately differs fromaddressAction/callerAction/originActionwhich all callgetLast20Bytes(). But the test doesn't reflect this.Production code (
OperationActions.java)Test code (
OperationsTest.java)Environment
Network: N/A (unit test issue)
Software Versions
Expected Behavior
The COINBASE test should assert the actual value pushed by
coinBaseAction: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), makinggetLast20Bytes()and the full DataWord equivalent.Frequency
Steps to Reproduce
framework/src/test/java/org/tron/common/runtime/vm/OperationsTest.javacoinBaseActioninOperationActions.javagetLast20Bytes()but the production code does notTo 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 becausegetLast20Bytes()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
A comment explaining the intentional asymmetry between COINBASE and other address opcodes would also help future implementors.