Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

905 rpc debug tracetransaction is missing error and revert reason for reverted txs #945

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 53 additions & 9 deletions accounts/abi/abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"errors"
"fmt"
"io"
"math/big"

libcommon "github.com/gateway-fm/cdk-erigon-lib/common"

Expand Down Expand Up @@ -263,8 +264,31 @@ func (abi *ABI) HasReceive() bool {
return abi.Receive.Type == Receive
}

// revertSelector is a special function selector for revert reason unpacking.
var revertSelector = crypto.Keccak256([]byte("Error(string)"))[:4]
var (
// revertSelector is a special function selector for revert reason unpacking.
revertSelector = crypto.Keccak256([]byte("Error(string)"))[:4]

// panicSelector is a special function selector for panic reason unpacking.
panicSelector = crypto.Keccak256([]byte("Panic(uint256)"))[:4]

// panicReasons map is for readable panic codes
// see this linkage for the details
// https://docs.soliditylang.org/en/v0.8.21/control-structures.html#panic-via-assert-and-error-via-require
// the reason string list is copied from ether.js
// https://github.com/ethers-io/ethers.js/blob/fa3a883ff7c88611ce766f58bdd4b8ac90814470/src.ts/abi/interface.ts#L207-L218
panicReasons = map[uint64]string{
0x00: "generic panic",
0x01: "assert(false)",
0x11: "arithmetic underflow or overflow",
0x12: "division or modulo by zero",
0x21: "enum overflow",
0x22: "invalid encoded storage byte array accessed",
0x31: "out-of-bounds array access; popping on an empty array",
0x32: "out-of-bounds access of an array or bytesN",
0x41: "out of memory",
0x51: "uninitialized function",
}
)

// UnpackRevert resolves the abi-encoded revert reason. According to the solidity
// spec https://solidity.readthedocs.io/en/latest/control-structures.html#revert,
Expand All @@ -274,13 +298,33 @@ func UnpackRevert(data []byte) (string, error) {
if len(data) < 4 {
return "", errors.New("invalid data for unpacking")
}
if !bytes.Equal(data[:4], revertSelector) {
switch {
case bytes.Equal(data[:4], revertSelector):
typ, _ := NewType("string", "", nil)
unpacked, err := (Arguments{{Type: typ}}).Unpack(data[4:])
if err != nil {
return "", err
}
return unpacked[0].(string), nil
case bytes.Equal(data[:4], panicSelector):
typ, err := NewType("uint256", "", nil)
if err != nil {
return "", err
}
unpacked, err := (Arguments{{Type: typ}}).Unpack(data[4:])
if err != nil {
return "", err
}
pCode := unpacked[0].(*big.Int)
// uint64 safety check for future
// but the code is not bigger than MAX(uint64) now
if pCode.IsUint64() {
if reason, ok := panicReasons[pCode.Uint64()]; ok {
return reason, nil
}
}
return fmt.Sprintf("unknown panic code: %#x", pCode), nil
default:
return "", errors.New("invalid data for unpacking")
}
typ, _ := NewType("string", "", nil)
unpacked, err := (Arguments{{Type: typ}}).Unpack(data[4:])
if err != nil {
return "", err
}
return unpacked[0].(string), nil
}
25 changes: 20 additions & 5 deletions eth/tracers/logger/json_stream_zkevm.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type JsonStreamLogger_ZkEvm struct {
env vm.VMInterface

counterCollector *vm.CounterCollector
stateClosed bool
}

// NewStructLogger returns a new logger
Expand All @@ -42,6 +43,7 @@ func NewJsonStreamLogger_ZkEvm(cfg *LogConfig, ctx context.Context, stream *json
storage: make(map[libcommon.Address]Storage),
firstCapture: true,
counterCollector: counterCollector,
stateClosed: true,
}
if cfg != nil {
logger.cfg = *cfg
Expand Down Expand Up @@ -82,6 +84,12 @@ func (l *JsonStreamLogger_ZkEvm) CaptureState(pc uint64, op vm.OpCode, gas, cost
if l.cfg.Limit != 0 && l.cfg.Limit <= len(l.logs) {
return
}

if !l.stateClosed {
l.stream.WriteObjectEnd()
_ = l.stream.Flush()
l.stateClosed = true
}
if !l.firstCapture {
l.stream.WriteMore()
} else {
Expand All @@ -90,6 +98,8 @@ func (l *JsonStreamLogger_ZkEvm) CaptureState(pc uint64, op vm.OpCode, gas, cost

outputStorage := l.prepareStorage(scope, scope.Contract, op)

l.stream.WriteObjectStart()
l.stateClosed = false
l.writeOpSnapshot(pc, op, gas, cost, depth, err)

l.writeError(err)
Expand All @@ -106,8 +116,8 @@ func (l *JsonStreamLogger_ZkEvm) CaptureState(pc uint64, op vm.OpCode, gas, cost

l.writeCounters()

l.stream.WriteObjectEnd()
_ = l.stream.Flush()
// l.stream.WriteObjectEnd()
// _ = l.stream.Flush()
}

func (l *JsonStreamLogger_ZkEvm) prepareStorage(scope *vm.ScopeContext, contract *vm.Contract, op vm.OpCode) bool {
Expand Down Expand Up @@ -145,7 +155,6 @@ func (l *JsonStreamLogger_ZkEvm) prepareStorage(scope *vm.ScopeContext, contract

func (l *JsonStreamLogger_ZkEvm) writeOpSnapshot(pc uint64, op vm.OpCode, gas, cost uint64, depth int, err error) {
// create a new snapshot of the EVM.
l.stream.WriteObjectStart()
l.stream.WriteObjectField("pc")
l.stream.WriteUint64(pc)
l.stream.WriteMore()
Expand Down Expand Up @@ -278,13 +287,19 @@ func (l *JsonStreamLogger_ZkEvm) writeCounters() {
}
l.stream.WriteObjectEnd()
}
// not needed for this tracer
}

// CaptureFault implements the Tracer interface to trace an execution fault
// while running an opcode.
func (l *JsonStreamLogger_ZkEvm) CaptureFault(pc uint64, op vm.OpCode, gas, cost uint64, scope *vm.ScopeContext, depth int, err error) {
// not needed for this tracer
if !l.stateClosed {
l.stream.WriteMore()
l.stream.WriteObjectField("error")
l.stream.WriteString(err.Error())
l.stream.WriteObjectEnd()
_ = l.stream.Flush()
l.stateClosed = true
}
}

// CaptureEnd is called after the call finishes to finalize the tracing.
Expand Down
58 changes: 58 additions & 0 deletions zk/debug_tools/test-contracts/contracts/MyToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.7.0 <0.9.0;

interface IERC20 {
function totalSupply() external view returns (uint);
function balanceOf(address account) external view returns (uint);
function transfer(address recipient, uint amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint);
function approve(address spender, uint amount) external returns (bool);
function transferFrom(address sender, address recipient, uint amount) external returns (bool);

event Transfer(address indexed from, address indexed to, uint value);
event Approval(address indexed owner, address indexed spender, uint value);
}

contract MyToken is IERC20 {
uint override public totalSupply;
mapping(address => uint) override public balanceOf;
mapping(address => mapping(address => uint)) override public allowance;
uint8 public decimals = 18;

constructor() {
}

function transfer(address recipient, uint amount) override external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(msg.sender, recipient, amount);
return true;
}

function approve(address spender, uint amount) override external returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}

function transferFrom(address sender, address recipient, uint amount) override external returns (bool) {
allowance[sender][msg.sender] -= amount;
balanceOf[sender] -= amount;
balanceOf[recipient] += amount;
emit Transfer(sender, recipient, amount);
return true;
}

function mint(uint amount) external {
balanceOf[msg.sender] += amount;
totalSupply += amount;
emit Transfer(address(0), msg.sender, amount);
}

function burn(uint amount) external {
balanceOf[msg.sender] -= amount;
totalSupply -= amount;
emit Transfer(msg.sender, address(0), amount);
}
}
7 changes: 7 additions & 0 deletions zk/debug_tools/test-contracts/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion zk/debug_tools/test-contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"emitlog:mainnet": "npx hardhat compile && npx hardhat run scripts/emitlog.js --network mainnet",
"spam:local": "npx hardhat compile && npx hardhat run scripts/spam-transactions.js --network local",
"delegateCall:local": "npx hardhat compile && npx hardhat run scripts/delegate-call.js --network local",
"erc20Revert:local": "npx hardhat compile && npx hardhat run scripts/ERC20-revert.js --network local",
"erc20Revert:sepolia": "npx hardhat compile && npx hardhat run scripts/ERC20-revert.js --network sepolia",
"chainCall:local": "npx hardhat compile && npx hardhat run scripts/chain-call.js --network local",
"chainCall:sepolia": "npx hardhat compile && npx hardhat run scripts/chain-call.js --network sepolia"
},
Expand All @@ -23,6 +25,7 @@
"license": "ISC",
"devDependencies": {
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"hardhat": "^2.22.4"
"hardhat": "^2.22.4",
"@openzeppelin/contracts": "^5.0.2"
}
}
27 changes: 27 additions & 0 deletions zk/debug_tools/test-contracts/scripts/ERC20-revert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// deploys contracts and calls a method to produce delegate call
async function main() {
try {
const ERC20 = await hre.ethers.getContractFactory("MyToken");

// Deploy the contracts
const erc20 = await ERC20.deploy();

// Wait for the deployment transactions to be mined
await erc20.waitForDeployment();

console.log(`erc20 deployed to: ${await erc20.getAddress()}`);

const transferFromResult = await erc20.transferFrom("0x5294ef2a2519BedC457D75b8FE5b132CCE32036a", "0xb218d64f29e5768B5Cc9b791B5514a2C611eE547", 1, {gasLimit: 1000000});
console.log('transferFromResult method call transaction: ', transferFromResult.hash);
} catch (error) {
console.error(error);
process.exit(1);
}
}

main()
.then(() => process.exit(0))
.catch(error => {
console.error(error);
process.exit(1);
});
Loading