Skip to content

Commit

Permalink
Fix/delegate call trace (0xPolygonHermez#2115)
Browse files Browse the repository at this point in the history
* WIP: fix delegatecall when trace is requested via custom tracer

* changes to custom tracer for call, callcode, staticcall, delegatecall opcodes

* fix custom tracer staticcall opcode

* fix custom tracer subcalls with multi levels

* comment test debug code

* review error handling

* fix custom call tracer for SC calls with precompiled code

* fix get precompiled address and input for opcodes that have tx value

* increase debug tracer e2e tests timeout

* update prover image
  • Loading branch information
tclemos authored May 25, 2023
1 parent 5c8f0f9 commit ecc771b
Show file tree
Hide file tree
Showing 24 changed files with 2,259 additions and 636 deletions.
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ services:
zkevm-prover:
container_name: zkevm-prover
restart: unless-stopped
image: hermeznetwork/zkevm-prover:v1.1.2-RC1-fork.4
image: hermeznetwork/zkevm-prover:v1.1.3-RC2-fork.4
depends_on:
zkevm-state-db:
condition: service_healthy
Expand Down
12 changes: 6 additions & 6 deletions jsonrpc/endpoints_debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,27 +216,27 @@ func (d *DebugEndpoints) buildStructLogs(stateStructLogs []instrumentation.Struc
RefundCounter: structLog.RefundCounter,
}

stack := make([]types.ArgBig, 0, len(structLog.Stack))
if !cfg.DisableStack && len(structLog.Stack) > 0 {
if !cfg.DisableStack {
stack := make([]types.ArgBig, 0, len(structLog.Stack))
for _, stackItem := range structLog.Stack {
if stackItem != nil {
stack = append(stack, types.ArgBig(*stackItem))
}
}
structLogRes.Stack = &stack
}
structLogRes.Stack = &stack

const memoryChunkSize = 32
memory := make([]string, 0, len(structLog.Memory))
if cfg.EnableMemory {
const memoryChunkSize = 32
memory := make([]string, 0, len(structLog.Memory))
for i := 0; i < len(structLog.Memory); i = i + memoryChunkSize {
slice32Bytes := make([]byte, memoryChunkSize)
copy(slice32Bytes, structLog.Memory[i:i+memoryChunkSize])
memoryStringItem := hex.EncodeToString(slice32Bytes)
memory = append(memory, memoryStringItem)
}
structLogRes.Memory = &memory
}
structLogRes.Memory = &memory

if !cfg.DisableStorage && len(structLog.Storage) > 0 {
storage := make(map[string]string, len(structLog.Storage))
Expand Down
87 changes: 72 additions & 15 deletions state/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,9 +435,9 @@ func (s *State) DebugTransaction(ctx context.Context, transactionHash common.Has

// ParseTheTraceUsingTheTracer parses the given trace with the given tracer.
func (s *State) ParseTheTraceUsingTheTracer(evm *fakevm.FakeEVM, trace instrumentation.ExecutorTrace, tracer tracers.Tracer) (json.RawMessage, error) {
var previousDepth int
var previousStep instrumentation.Step
var previousOp, previousGas *big.Int
var previousOpcode string
var previousError error
var stateRoot []byte

contextGas, ok := new(big.Int).SetString(trace.Context.Gas, encoding.Base10)
Expand All @@ -452,7 +452,12 @@ func (s *State) ParseTheTraceUsingTheTracer(evm *fakevm.FakeEVM, trace instrumen
}

tracer.CaptureTxStart(contextGas.Uint64())
tracer.CaptureStart(evm, common.HexToAddress(trace.Context.From), common.HexToAddress(trace.Context.To), trace.Context.Type == "CREATE", common.Hex2Bytes(strings.TrimLeft(trace.Context.Input, "0x")), contextGas.Uint64(), value)
decodedInput, err := hex.DecodeHex(trace.Context.Input)
if err != nil {
log.Errorf("error while decoding context input from hex to bytes:, %v", err)
return nil, ErrParsingExecutorTrace
}
tracer.CaptureStart(evm, common.HexToAddress(trace.Context.From), common.HexToAddress(trace.Context.To), trace.Context.Type == "CREATE", decodedInput, contextGas.Uint64(), value)

bigStateRoot, ok := new(big.Int).SetString(trace.Context.OldStateRoot, 0)
if !ok {
Expand Down Expand Up @@ -526,7 +531,7 @@ func (s *State) ParseTheTraceUsingTheTracer(evm *fakevm.FakeEVM, trace instrumen
break
}

if previousOpcode == "CALL" && step.Pc != 0 {
if previousStep.OpCode == "CALL" && step.Pc != 0 {
tracer.CaptureExit(step.ReturnData, gasCost.Uint64(), stepError)
}

Expand All @@ -538,20 +543,43 @@ func (s *State) ParseTheTraceUsingTheTracer(evm *fakevm.FakeEVM, trace instrumen
}
}

if step.OpCode == "CALL" || step.OpCode == "CALLCODE" || step.OpCode == "DELEGATECALL" || step.OpCode == "STATICCALL" || step.OpCode == "SELFDESTRUCT" {
tracer.CaptureEnter(fakevm.OpCode(op.Uint64()), common.HexToAddress(step.Contract.Caller), common.HexToAddress(step.Contract.Address), []byte(step.Contract.Input), gas.Uint64(), value)
if step.OpCode == "SELFDESTRUCT" {
tracer.CaptureExit(step.ReturnData, gasCost.Uint64(), stepError)
previousOpCodeCanBeSubCall := previousStep.OpCode == "CREATE" ||
previousStep.OpCode == "CREATE2" ||
previousStep.OpCode == "DELEGATECALL" ||
previousStep.OpCode == "CALL" ||
previousStep.OpCode == "STATICCALL" ||
// deprecated ones
previousStep.OpCode == "CALLCODE" ||
previousStep.OpCode == "SELFDESTRUCT"

// when a sub call or create is detected, the next step contains the contract updated
if previousOpCodeCanBeSubCall {
// shadowing "value" to override its value without compromising the external code
value := value
// value is not carried over when the capture enter handles STATIC CALL
if previousStep.OpCode == "STATICCALL" {
value = nil
}
}

// when a create2 is detected, the next step contains the contract updated
if previousOpcode == "CREATE" || previousOpcode == "CREATE2" {
tracer.CaptureEnter(fakevm.OpCode(previousOp.Uint64()), common.HexToAddress(step.Contract.Caller), common.HexToAddress(step.Contract.Address), []byte(step.Contract.Input), previousGas.Uint64(), value)
// if the previous depth is the same as the current one, this means
// the sub call did not executed any other step and the
// context is back to the same level. This can happen with pre compiled executions.
if previousStep.Depth == step.Depth {
addr, input := s.getPreCompiledCallAddressAndInput(previousStep)
tracer.CaptureEnter(fakevm.OpCode(previousOp.Uint64()), common.HexToAddress(previousStep.Contract.Address), addr, input, previousGas.Uint64(), value)
previousStepGasCost, ok := new(big.Int).SetString(step.GasCost, encoding.Base10)
if !ok {
log.Debugf("error while parsing previous step gasCost")
return nil, ErrParsingExecutorTrace
}
tracer.CaptureExit(step.ReturnData, previousStepGasCost.Uint64(), previousError)
} else {
tracer.CaptureEnter(fakevm.OpCode(previousOp.Uint64()), common.HexToAddress(step.Contract.Caller), common.HexToAddress(step.Contract.Address), []byte(step.Contract.Input), previousGas.Uint64(), value)
}
}

// returning from a call or create
if previousDepth > step.Depth {
if previousStep.Depth > step.Depth {
tracer.CaptureExit(step.ReturnData, gasCost.Uint64(), stepError)
}

Expand All @@ -560,10 +588,10 @@ func (s *State) ParseTheTraceUsingTheTracer(evm *fakevm.FakeEVM, trace instrumen
evm.StateDB.SetStateRoot(stateRoot)

// set previous step values
previousDepth = step.Depth
previousStep = step
previousOp = op
previousGas = gas
previousOpcode = step.OpCode
previousError = stepError
}

gasUsed, ok := new(big.Int).SetString(trace.Context.GasUsed, encoding.Base10)
Expand All @@ -579,6 +607,35 @@ func (s *State) ParseTheTraceUsingTheTracer(evm *fakevm.FakeEVM, trace instrumen
return tracer.GetResult()
}

func (s *State) getPreCompiledCallAddressAndInput(step instrumentation.Step) (common.Address, []byte) {
if step.OpCode == "DELEGATECALL" || step.OpCode == "CALL" || step.OpCode == "STATICCALL" || step.OpCode == "CALLCODE" {
addrPos := len(step.Stack) - 1 - 1
argsOffsetPos := addrPos - 1
argsSizePos := argsOffsetPos - 1

// if the stack has the tx value, we skip it
stackHasValue := step.OpCode == "CALL" || step.OpCode == "CALLCODE"
if stackHasValue {
argsOffsetPos--
argsSizePos--
}

addrEncoded := step.Stack[addrPos]
addr := common.HexToAddress("0x" + addrEncoded)

argsOffsetEncoded := step.Stack[argsOffsetPos]
argsOffset := hex.DecodeUint64(argsOffsetEncoded)

argsSizeEncoded := step.Stack[argsSizePos]
argsSize := hex.DecodeUint64(argsSizeEncoded)
input := make([]byte, argsSize)
copy(input[0:argsSize], step.Memory[argsOffset:argsOffset+argsSize])
return addr, input
} else {
return common.HexToAddress(step.Contract.Address), []byte(step.Contract.Input)
}
}

// PreProcessTransaction processes the transaction in order to calculate its zkCounters before adding it to the pool
func (s *State) PreProcessTransaction(ctx context.Context, tx *types.Transaction, dbTx pgx.Tx) (*ProcessBatchResponse, error) {
sender, err := GetSender(*tx)
Expand Down
6 changes: 3 additions & 3 deletions test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ test-e2e-group-5: stop ## Runs group 5 e2e tests checking race conditions
$(RUNZKPROVER)
docker ps -a
docker logs $(DOCKERCOMPOSEZKPROVER)
trap '$(STOP)' EXIT; MallocNanoZone=0 go test -count=1 -race -v -p 1 -timeout 600s ../ci/e2e-group5/...
trap '$(STOP)' EXIT; MallocNanoZone=0 go test -count=1 -race -v -p 1 -timeout 1200s ../ci/e2e-group5/...

.PHONY: test-e2e-group-6
test-e2e-group-6: stop ## Runs group 6 e2e tests checking race conditions
Expand Down Expand Up @@ -181,11 +181,11 @@ test-e2e-group-8: stop ## Runs group 8 e2e tests checking race conditions
$(RUNZKPROVER)
docker ps -a
docker logs $(DOCKERCOMPOSEZKPROVER)
trap '$(STOP)' EXIT; MallocNanoZone=0 go test -count=1 -race -v -p 1 -timeout 600s ../ci/e2e-group8/...
trap '$(STOP)' EXIT; MallocNanoZone=0 go test -count=1 -race -v -p 1 -timeout 1200s ../ci/e2e-group8/...


.PHONY: test-e2e-group-9
test-e2e-group-9: stop ## Runs group 8 e2e tests checking race conditions
test-e2e-group-9: stop ## Runs group 9 e2e tests checking race conditions
$(RUNSTATEDB)
$(RUNPOOLDB)
$(RUNEVENTDB)
Expand Down
30 changes: 30 additions & 0 deletions test/contracts/auto/Called.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Called {
uint256 num;
address sender;
uint256 value;

function setVars(uint256 _num) public payable {
num = _num;
sender = msg.sender;
value = msg.value;
}

function setVarsViaCall(uint256 _num) public payable {
bool ok;
(ok, ) = address(this).call(
abi.encodeWithSignature("setVars(uint256)", _num)
);
require(ok, "failed to perform call");
}

function getVars() public view returns (uint256, address, uint256) {
return (num, sender, value);
}

function getVarsAndVariable(uint256 _num) public view returns (uint256, address, uint256, uint256) {
return (num, sender, value, _num);
}
}
74 changes: 74 additions & 0 deletions test/contracts/auto/Caller.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract Caller {
function call(address _contract, uint _num) public payable {
bool ok;
(ok, ) = _contract.call(
abi.encodeWithSignature("setVars(uint256)", _num)
);
require(ok, "failed to perform call");
}

function delegateCall(address _contract, uint _num) public payable {
bool ok;
(ok, ) = _contract.delegatecall(
abi.encodeWithSignature("setVars(uint256)", _num)
);
require(ok, "failed to perform delegate call");
}

function staticCall(address _contract) public payable {
bool ok;
bytes memory result;
(ok, result) = _contract.staticcall(
abi.encodeWithSignature("getVars()")
);
require(ok, "failed to perform static call");

uint256 num;
address sender;
uint256 value;

(num, sender, value) = abi.decode(result, (uint256, address, uint256));
}

function invalidStaticCallMoreParameters(address _contract) public {
bool ok;
(ok,) = _contract.staticcall(
abi.encodeWithSignature("getVarsAndVariable(uint256)", 1, 2)
);
require(!ok, "static call was supposed to fail with more parameters");
}

function invalidStaticCallLessParameters(address _contract) public {
bool ok;
(ok,) = _contract.staticcall(
abi.encodeWithSignature("getVarsAndVariable(uint256)")
);
require(!ok, "static call was supposed to fail with less parameters");
}

function invalidStaticCallWithInnerCall(address _contract) public {
bool ok;
(ok,) = _contract.staticcall(
abi.encodeWithSignature("getVarsAndVariable(uint256)")
);
require(!ok, "static call was supposed to fail with less parameters");
}

function multiCall(address _contract, uint _num) public payable {
call(_contract, _num);
delegateCall(_contract, _num);
staticCall(_contract);
}

function preEcrecover_0() public {
bytes32 messHash = 0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3;
uint8 v = 28;
bytes32 r = 0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608;
bytes32 s = 0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada;

ecrecover(messHash, v, r, s);
}
}
26 changes: 26 additions & 0 deletions test/contracts/auto/ChainCallLevel1.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract ChainCallLevel1 {
function exec(address level2Addr, address level3Addr, address level4Addr) public payable {
bool ok;
(ok, ) = level2Addr.call(
abi.encodeWithSignature("exec(address,address)", level3Addr, level4Addr)
);
require(ok, "failed to perform call to level 2");

(ok, ) = level2Addr.delegatecall(
abi.encodeWithSignature("exec(address,address)", level3Addr, level4Addr)
);
require(ok, "failed to perform delegate call to level 2");

bytes memory result;
(ok, result) = level2Addr.staticcall(
abi.encodeWithSignature("get(address,address)", level3Addr, level4Addr)
);
require(ok, "failed to perform static call to level 2");

string memory t;
(t) = abi.decode(result, (string));
}
}
27 changes: 27 additions & 0 deletions test/contracts/auto/ChainCallLevel2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract ChainCallLevel2 {
function exec(address level3Addr, address level4Addr) public payable {
bool ok;
(ok, ) = level3Addr.call(abi.encodeWithSignature("exec(address)", level4Addr));
require(ok, "failed to perform call to level 3");

(ok, ) = level3Addr.delegatecall(abi.encodeWithSignature("exec(address)", level4Addr));
require(ok, "failed to perform delegate call to level 3");
}

function get(address level3Addr, address level4Addr) public view returns (string memory t) {
bool ok;
bytes memory result;
(ok, result) = level3Addr.staticcall(abi.encodeWithSignature("get(address)", level4Addr));
require(ok, "failed to perform static call to level 3");

t = abi.decode(result, (string));

(ok, result) = level4Addr.staticcall(abi.encodeWithSignature("get()"));
require(ok, "failed to perform static call to level 4 from level 2");

t = abi.decode(result, (string));
}
}
23 changes: 23 additions & 0 deletions test/contracts/auto/ChainCallLevel3.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

contract ChainCallLevel3 {
function exec(address level4Addr) public payable {
bool ok;
(ok, ) = level4Addr.call(abi.encodeWithSignature("exec()"));
require(ok, "failed to perform call to level 4");

(ok, ) = level4Addr.delegatecall(abi.encodeWithSignature("exec()"));
require(ok, "failed to perform delegate call to level 4");
}

function get(address level4Addr) public view returns (string memory t) {
bool ok;
bytes memory result;

(ok, result) = level4Addr.staticcall(abi.encodeWithSignature("get()"));
require(ok, "failed to perform static call to level 4");

t = abi.decode(result, (string));
}
}
Loading

0 comments on commit ecc771b

Please sign in to comment.