(Specifications) A bug in specifications with no direct impact on client implementations
Description
Brief/Intro
The opcode PUSH0 is introduced in EIP-3855 (https://eips.ethereum.org/EIPS/eip-3855 ) that pushes the constant value 0 onto the stack.
In Erigon (https://github.com/erigontech/erigon ), the function IsPush() is used to check if an opcode is a push opcode, which does not include the PUSH0 opcode. In the current implementation, this function IsPush() is mostly utilized in the execution tracer and external tools, such asm and goja. Missing the opcode PUSH0 would mess up the trace generated in the upstream applications.
// IsPush specifies if an opcode is a PUSH opcode.
func (op OpCode) IsPush() bool {
switch op {
case PUSH1, PUSH2, PUSH3, PUSH4, PUSH5, PUSH6, PUSH7, PUSH8, PUSH9, PUSH10, PUSH11, PUSH12, PUSH13, PUSH14, PUSH15, PUSH16, PUSH17, PUSH18, PUSH19, PUSH20, PUSH21, PUSH22, PUSH23, PUSH24, PUSH25, PUSH26, PUSH27, PUSH28, PUSH29, PUSH30, PUSH31, PUSH32:
return true
}
return false
}
However, it misses the PUSH0 opcode introduced in the EIP-3855 (https://eips.ethereum.org/EIPS/eip-3855 ). This function IsPush() is currently utilized in the upstream execution tracer and external application. For example, the CaptureState() in
func (ot *opcodeTracer) CaptureState(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, rData []byte, opDepth int, err error) {
//CaptureState sees the system as it is before the opcode is run. It seems to never get an error.
contract := scope.Contract
//sanity check
if pc > uint64(MaxUint16) {
panic(fmt.Sprintf("PC is bigger than uint16! pc=%d=0x%x", pc, pc))
}
pc16 := uint16(pc)
currentTxHash := ot.env.TxHash
currentTxDepth := opDepth - 1
ls := len(ot.stack)
currentEntry := ot.stack[ls-1]
//sanity check
if currentEntry.Depth != currentTxDepth {
panic(fmt.Sprintf("Depth should be the same but isn't: current tx's %d, current entry's %d", currentTxDepth, currentEntry.Depth))
}
// is the Tx entry still not fully initialized?
if currentEntry.TxHash == nil {
// CaptureStart creates the entry for a new Tx, but doesn't have access to EVM data, like the Tx Hash
// here we ASSUME that the tx entry was recently created by CaptureStart
// AND that this is the first CaptureState that has happened since then
// AND that both Captures are for the same transaction
// AND that we can't go into another depth without executing at least 1 opcode
// Note that the only connection between CaptureStart and CaptureState that we can notice is that the current op's depth should be lastTxEntry.Depth+1
// fill in the missing data in the entry
currentEntry.TxHash = new(libcommon.Hash)
currentEntry.TxHash.SetBytes(currentTxHash.Bytes())
currentEntry.CodeHash = new(libcommon.Hash)
currentEntry.CodeHash.SetBytes(contract.CodeHash.Bytes())
currentEntry.CodeSize = len(contract.Code)
if ot.saveOpcodes {
currentEntry.Opcodes = make([]opcode, 0, 200)
}
//fmt.Fprintf(ot.w, "%sFilled in TxHash\n", strings.Repeat("\t",depth))
if ot.saveBblocks {
currentEntry.Bblocks = make(sliceBblocks, 0, 10)
}
}
// prepare the opcode's stack for saving
//stackTop := &stack.Stack{Data: make([]uint256.Int, 0, 7)}//stack.New()
// the most stack positions consumed by any opcode is 7
//for i:= min(7, st.Len()-1); i>=0; i-- {
// stackTop.Push(st.Back(i))
//}
//THIS VERSION SHOULD BE FASTER BUT IS UNTESTED
//stackTop := make([]uint256.Int, 7, 7)
//sl := st.Len()
//minl := min(7, sl)
//startcopy := sl-minl
//stackTop := &stack.Stack{Data: make([]uint256.Int, minl, minl)}//stack.New()
//copy(stackTop.Data, st.Data[startcopy:sl])
//sanity check
if currentEntry.OpcodeFault != "" {
panic(fmt.Sprintf("Running opcodes but fault is already set. txFault=%s, opFault=%v, op=%s",
currentEntry.OpcodeFault, err, op.String()))
}
// if it is a Fault, check whether we already have a record of the opcode. If so, just add the flag to it
errstr := ""
if err != nil {
errstr = err.Error()
currentEntry.OpcodeFault = errstr
}
faultAndRepeated := false
if pc16 == currentEntry.lastPc16 && op == currentEntry.lastOp {
//it's a repeated opcode. We assume this only happens when it's a Fault.
if err == nil {
panic(fmt.Sprintf("Duplicate opcode with no fault. bn=%d txaddr=%s pc=%x op=%s",
ot.blockNumber, currentEntry.TxAddr, pc, op.String()))
}
faultAndRepeated = true
//ot.fsumWriter.WriteString("Fault for EXISTING opcode\n")
//ot.fsumWriter.Flush()
if ot.saveOpcodes {
lo := len(currentEntry.Opcodes)
currentEntry.Opcodes[lo-1].Fault = errstr
}
} else {
// it's a new opcode
if ot.saveOpcodes {
newOpcode := opcode{pc16, op, errstr}
currentEntry.Opcodes = append(currentEntry.Opcodes, newOpcode)
}
}
// detect and store bblocks
if ot.saveBblocks {
// PC discontinuities can only happen because of a PUSH (which is followed by the data to be pushed) or a JUMP (which lands into a JUMPDEST)
// Therefore, after a PC discontinuity we either have op==JUMPDEST or lastOp==PUSH
// Only the JUMPDEST case is a real control flow discontinuity and therefore starts a new bblock
lseg := len(currentEntry.Bblocks)
isFirstBblock := lseg == 0
isContinuous := pc16 == currentEntry.lastPc16+1 || currentEntry.lastOp.IsPush()
...
Though it does not affect the state transition of the Ethereum, the execution trace and other external applications dependent on it would be incorrect.
Impact Details
The function IsPush() is only utilized in transaction execution trace and other external applications and does not impact the state transition of Ethereum at this moment, it would mess up the trace and produce incorrect execution trace for the upstream application.
References
https://github.com/erigontech/erigon/tree/v2.61.0
https://eips.ethereum.org/EIPS/eip-3855
Proof of Concept
Proof of Concept
For simplicity, we create the following simple test cases:
Check if PUSH0, PUSH1 and PUSH32 are push opcodes:
package vm
import (
"fmt"
"testing"
)
func TestIsPush(t *testing.T) {
fmt.Printf("Is PUSH0 a push opcode: %t\n", PUSH0.IsPush())
fmt.Printf("Is PUSH1 a push opcode: %t\n", PUSH1.IsPush())
fmt.Printf("Is PUSH32 a push opcode: %t\n", PUSH32.IsPush())
}
The test result shows that the PUSH0 is not push opcode:
=== RUN TestIsPush
Is PUSH0 a push opcode: false
Is PUSH1 a push opcode: true
Is PUSH32 a push opcode: true
--- PASS: TestIsPush (0.00s)
PASS
Process finished with the exit code 0