From 9dda9069e52d4be272f9f905104c7edb4d6cdc95 Mon Sep 17 00:00:00 2001 From: Federico Borello <156438142+fborello-lambda@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:50:18 -0300 Subject: [PATCH] EVM implement CALL opcode (#21) * feat(call-opcode): Implement CALL More rigourous checks are needed * fix(call-opcode): fix Merge * feat(call-opcode): Add function _isEVM() * feat(call-opcode): Add function _pushEVMFrame() and _popEVMFrame() * feat(gas-related-function): Add gas related functions, needed for OP_CALL * chore: indent properly * feat(verbatims-functions): Add verbatims * feat(call-related-functions): Add _saveReturnDataAfterZkEVMCall() * feat(call-related-functions): Add _performCall() // Stack depth errors (WIP) * Fix stack too deep error * chore(remove dead code): Remove _performCall() * chore(copy code): Copy code inside the two objects * fix(call tests): Fix selector store inside _isEVM() * fix(call tests): Fix constants and Add prints (WIP) --------- Co-authored-by: Javier Chatruc Co-authored-by: Manuel Bilbao --- system-contracts/contracts/EvmInterpreter.yul | 564 +++++++++++++++++- 1 file changed, 558 insertions(+), 6 deletions(-) diff --git a/system-contracts/contracts/EvmInterpreter.yul b/system-contracts/contracts/EvmInterpreter.yul index beb79bbaf..330d9449b 100644 --- a/system-contracts/contracts/EvmInterpreter.yul +++ b/system-contracts/contracts/EvmInterpreter.yul @@ -62,6 +62,10 @@ object "EVMInterpreter" { verbatim_1i_0o("active_ptr_data_load", 0xFFFF) } + function loadReturndataIntoActivePtr() { + verbatim_0i_0o("return_data_ptr_to_active") + } + function getActivePtrDataSize() -> size { size := verbatim_0i_1o("active_ptr_data_size") } @@ -70,6 +74,14 @@ object "EVMInterpreter" { verbatim_3i_0o("active_ptr_data_copy", _dest, _source, _size) } + function ptrAddIntoActive(_dest) { + verbatim_1i_0o("active_ptr_add_assign", _dest) + } + + function ptrShrinkIntoActive(_dest) { + verbatim_1i_0o("active_ptr_shrink_assign", _dest) + } + function SYSTEM_CONTRACTS_OFFSET() -> offset { offset := 0x8000 } @@ -508,6 +520,74 @@ object "EVMInterpreter" { } } + function _isEVM(_addr) -> isEVM { + // bytes4 selector = ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.isAccountEVM.selector; + // function isAccountEVM(address _addr) external view returns (bool); + let selector := 0x8c040477 + // IAccountCodeStorage constant ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT = IAccountCodeStorage( + // address(SYSTEM_CONTRACTS_OFFSET + 0x02) + // ); + + mstore8(0, 0x8c) + mstore8(1, 0x04) + mstore8(2, 0x04) + mstore8(3, 0x77) + mstore(4, _addr) + let success := staticcall(gas(), ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT(), 0, 36, 0, 32) + + if iszero(success) { + // This error should never happen + revert(0, 0) + } + + isEVM := mload(0) + } + + function _pushEVMFrame(_passGas, _isStatic) { + // function pushEVMFrame(uint256 _passGas, bool _isStatic) external + let selector := 0xead77156 + + mstore8(0, 0xea) + mstore8(1, 0xd7) + mstore8(2, 0x71) + mstore8(3, 0x56) + mstore(4, _passGas) + mstore(36, _isStatic) + + let success := call(gas(), EVM_GAS_MANAGER_CONTRACT(), 0, 0, 68, 0, 0) + if iszero(success) { + // This error should never happen + revert(0, 0) + } + } + + function _popEVMFrame() { + // function popEVMFrame() external + // 0xe467d2f0 + let selector := 0xe467d2f0 + + mstore8(0, 0xe4) + mstore8(1, 0x67) + mstore8(2, 0xd2) + mstore8(3, 0xf0) + + let success := call(gas(), EVM_GAS_MANAGER_CONTRACT(), 0, 0, 4, 0, 0) + if iszero(success) { + // This error should never happen + revert(0, 0) + } + } + + // Each evm gas is 5 zkEVM one + // FIXME: change this variable to reflect real ergs : gas ratio + function GAS_DIVISOR() -> gas_div { gas_div := 5 } + function EVM_GAS_STIPEND() -> gas_stipend { gas_stipend := shl(30, 1) } // 1 << 30 + function OVERHEAD() -> overhead { overhead := 2000 } + + function _calcEVMGas(_zkevmGas) -> calczkevmGas { + calczkevmGas := div(_zkevmGas, GAS_DIVISOR()) + } + function genericCreate(addr, offset, size, sp) -> result { pop(warmAddress(addr)) @@ -552,15 +632,176 @@ object "EVMInterpreter" { } function getEVMGas() -> evmGas { - let GAS_DIVISOR, EVM_GAS_STIPEND, OVERHEAD := GAS_CONSTANTS() + let _gas := gas() + let requiredGas := add(EVM_GAS_STIPEND(), OVERHEAD()) + + if or(gt(requiredGas, _gas), eq(requiredGas, _gas)) { + evmGas := div(sub(_gas, requiredGas), GAS_DIVISOR()) + } + } + + function _getZkEVMGas(_evmGas) -> zkevmGas { + /* + TODO: refine the formula, especially with regard to decommitment costs + */ + zkevmGas := mul(_evmGas, GAS_DIVISOR()) + } + + function _saveReturndataAfterEVMCall(_outputOffset, _outputLen) -> _gasLeft{ + let lastRtSzOffset := LAST_RETURNDATA_SIZE_OFFSET() + let rtsz := returndatasize() + + loadReturndataIntoActivePtr() + + // if (rtsz > 31) + switch gt(rtsz, 31) + case 0 { + _gasLeft := 0 + _eraseReturndataPointer() + } + default { + returndatacopy(0, 0, 32) + _gasLeft := mload(0) + returndatacopy(_outputOffset, 32, _outputLen) + mstore(lastRtSzOffset, sub(rtsz, 32)) + + // Skip the returnData + ptrAddIntoActive(32) + } + } + + function _eraseReturndataPointer() { + let lastRtSzOffset := LAST_RETURNDATA_SIZE_OFFSET() + + let activePtrSize := getActivePtrDataSize() + ptrShrinkIntoActive(and(activePtrSize, 0xFFFFFFFF))// uint32(activePtrSize) + mstore(lastRtSzOffset, 0) + } + + function _saveReturndataAfterZkEVMCall() { + let lastRtSzOffset := LAST_RETURNDATA_SIZE_OFFSET() + + mstore(lastRtSzOffset, returndatasize()) + } + + function performCall(oldSp, evmGasLeft, isStatic) -> dynamicGas,sp { + let gasSend,addr,value,argsOffset,argsSize,retOffset,retSize + + gasSend, sp := popStackItem(oldSp) + addr, sp := popStackItem(sp) + value, sp := popStackItem(sp) + argsOffset, sp := popStackItem(sp) + argsSize, sp := popStackItem(sp) + retOffset, sp := popStackItem(sp) + retSize, sp := popStackItem(sp) - let gasLeft := gas() - let requiredGas := add(EVM_GAS_STIPEND, OVERHEAD) - evmGas := div(sub(gasLeft, requiredGas), GAS_DIVISOR) + // code_execution_cost is the cost of the called code execution (limited by the gas parameter). + // If address is warm, then address_access_cost is 100, otherwise it is 2600. See section access sets. + // If value is not 0, then positive_value_cost is 9000. In this case there is also a call stipend that is given to make sure that a basic fallback function can be called. 2300 is thus removed from the cost, and also added to the gas input. + // If value is not 0 and the address given points to an empty account, then value_to_empty_account_cost is 25000. An account is empty if its balance is 0, its nonce is 0 and it has no code. + dynamicGas := expandMemory(add(retOffset,retSize)) + switch warmAddress(addr) + case 0 { dynamicGas := add(dynamicGas,2600) } + default { dynamicGas := add(dynamicGas,100) } - if lt(gasLeft, requiredGas) { - evmGas := 0 + if not(iszero(value)) { + dynamicGas := add(dynamicGas,6700) + gasSend := add(gasSend,2300) + + if isAddrEmpty(addr) { + dynamicGas := add(dynamicGas,25000) + } + } + + if gt(gasSend,div(mul(evmGasLeft,63),64)) { + gasSend := div(mul(evmGasLeft,63),64) + } + argsOffset := add(argsOffset,MEM_OFFSET_INNER()) + retOffset := add(retOffset,MEM_OFFSET_INNER()) + // TODO: More Checks are needed + // Check gas + let success + + if isStatic { + if not(iszero(value)) { + revert(0, 0) + } + success, evmGasLeft := _performStaticCall( + _isEVM(addr), + gasSend, + addr, + argsOffset, + argsSize, + retOffset, + retSize + ) + } + + if _isEVM(addr) { + _pushEVMFrame(gasSend, isStatic) + success := call(gasSend, addr, value, argsOffset, argsSize, 0, 0) + + evmGasLeft := _saveReturndataAfterEVMCall(retOffset, retSize) + _popEVMFrame() + } + + // zkEVM native + if and(not(_isEVM(addr)), not(isStatic)) { + gasSend := _getZkEVMGas(gasSend) + let zkevmGasBefore := gas() + success := call(gasSend, addr, value, argsOffset, argsSize, retOffset, retSize) + + _saveReturndataAfterZkEVMCall() + + let gasUsed := _calcEVMGas(sub(zkevmGasBefore, gas())) + + evmGasLeft := 0 + if gt(gasSend, gasUsed) { + evmGasLeft := sub(gasSend, gasUsed) + } + } + + sp := pushStackItem(sp,success) + + // TODO: dynamicGas := add(dynamicGas,codeExecutionCost) how to do this? + // Check if the following is ok + dynamicGas := add(dynamicGas,gasSend) + } + + function _performStaticCall( + _calleeIsEVM, + _calleeGas, + _callee, + _inputOffset, + _inputLen, + _outputOffset, + _outputLen + ) -> success, _gasLeft { + if _calleeIsEVM { + _pushEVMFrame(_calleeGas, true) + // TODO Check the following comment from zkSync .sol. + // We can not just pass all gas here to prevert overflow of zkEVM gas counter + success := staticcall(_calleeGas, _callee, _inputOffset, _inputLen, 0, 0) + + _gasLeft := _saveReturndataAfterEVMCall(_outputOffset, _outputLen) + _popEVMFrame() + } + + // zkEVM native + if not(_calleeIsEVM) { + _calleeGas := _getZkEVMGas(_calleeGas) + let zkevmGasBefore := gas() + success := staticcall(_calleeGas, _callee, _inputOffset, _inputLen, _outputOffset, _outputLen) + + _saveReturndataAfterZkEVMCall() + + let gasUsed := _calcEVMGas(sub(zkevmGasBefore, gas())) + + _gasLeft := 0 + if gt(_calleeGas, gasUsed) { + _gasLeft := sub(_calleeGas, gasUsed) + } } } @@ -576,6 +817,17 @@ object "EVMInterpreter" { returnGas := chargeGas(gasToReturn, gasForCode) } + function isAddrEmpty(addr) -> isEmpty { + isEmpty := 0 + if and( and( + iszero(balance(addr)), + iszero(extcodesize(addr)) ), + iszero(getNonce(addr)) + ) { + isEmpty := 1 + } + } + function simulate( isCallerEVM, evmGasLeft, @@ -1488,6 +1740,13 @@ object "EVMInterpreter" { case 0 { sp := pushStackItem(sp, 0) } default { sp := pushStackItem(sp, addr) } } + case 0xF1 { // OP_CALL + let dynamicGas + // A function was implemented in order to avoid stack depth errors. + dynamicGas, sp := performCall(sp, evmGasLeft, isStatic) + + evmGasLeft := chargeGas(evmGasLeft,dynamicGas) + } // TODO: REST OF OPCODES default { // TODO: Revert properly here and report the unrecognized opcode @@ -1531,6 +1790,31 @@ object "EVMInterpreter" { } object "EVMInterpreter_deployed" { code { + + function loadCalldataIntoActivePtr() { + verbatim_1i_0o("active_ptr_data_load", 0xFFFF) + } + + function loadReturndataIntoActivePtr() { + verbatim_0i_0o("return_data_ptr_to_active") + } + + function getActivePtrDataSize() -> size { + size := verbatim_0i_1o("active_ptr_data_size") + } + + function copyActivePtrData(_dest, _source, _size) { + verbatim_3i_0o("active_ptr_data_copy", _dest, _source, _size) + } + + function ptrAddIntoActive(_dest) { + verbatim_1i_0o("active_ptr_add_assign", _dest) + } + + function ptrShrinkIntoActive(_dest) { + verbatim_1i_0o("active_ptr_shrink_assign", _dest) + } + function SYSTEM_CONTRACTS_OFFSET() -> offset { offset := 0x8000 } @@ -1963,6 +2247,267 @@ object "EVMInterpreter" { } } + function _isEVM(_addr) -> isEVM { + // bytes4 selector = ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.isAccountEVM.selector; + // function isAccountEVM(address _addr) external view returns (bool); + let selector := 0x8c040477 + // IAccountCodeStorage constant ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT = IAccountCodeStorage( + // address(SYSTEM_CONTRACTS_OFFSET + 0x02) + // ); + + mstore8(0, 0x8c) + mstore8(1, 0x04) + mstore8(2, 0x04) + mstore8(3, 0x77) + mstore(4, _addr) + + let success := staticcall(gas(), ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT(), 0, 36, 0, 32) + + if iszero(success) { + // This error should never happen + revert(0, 0) + } + + isEVM := mload(0) + } + + function _pushEVMFrame(_passGas, _isStatic) { + // function pushEVMFrame(uint256 _passGas, bool _isStatic) external + let selector := 0xead77156 + + mstore8(0, 0xea) + mstore8(1, 0xd7) + mstore8(2, 0x71) + mstore8(3, 0x56) + mstore(4, _passGas) + mstore(36, _isStatic) + + let success := call(gas(), EVM_GAS_MANAGER_CONTRACT(), 0, 0, 68, 0, 0) + if iszero(success) { + // This error should never happen + revert(0, 0) + } + } + + function _popEVMFrame() { + // function popEVMFrame() external + // 0xe467d2f0 + let selector := 0xe467d2f0 + + mstore8(0, 0xe4) + mstore8(1, 0x67) + mstore8(2, 0xd2) + mstore8(3, 0xf0) + + let success := call(gas(), EVM_GAS_MANAGER_CONTRACT(), 0, 0, 4, 0, 0) + if iszero(success) { + // This error should never happen + revert(0, 0) + } + } + + // Each evm gas is 5 zkEVM one + // FIXME: change this variable to reflect real ergs : gas ratio + function GAS_DIVISOR() -> gas_div { gas_div := 5 } + function EVM_GAS_STIPEND() -> gas_stipend { gas_stipend := shl(30, 1) } // 1 << 30 + function OVERHEAD() -> overhead { overhead := 2000 } + + function _calcEVMGas(_zkevmGas) -> calczkevmGas { + calczkevmGas := div(_zkevmGas, GAS_DIVISOR()) + } + + function getEVMGas() -> evmGas { + let _gas := gas() + let requiredGas := add(EVM_GAS_STIPEND(), OVERHEAD()) + + if or(gt(requiredGas, _gas), eq(requiredGas, _gas)) { + evmGas := div(sub(_gas, requiredGas), GAS_DIVISOR()) + } + } + + function _getZkEVMGas(_evmGas) -> zkevmGas { + /* + TODO: refine the formula, especially with regard to decommitment costs + */ + zkevmGas := mul(_evmGas, GAS_DIVISOR()) + } + + function _saveReturndataAfterEVMCall(_outputOffset, _outputLen) -> _gasLeft{ + let lastRtSzOffset := LAST_RETURNDATA_SIZE_OFFSET() + let rtsz := returndatasize() + + loadReturndataIntoActivePtr() + + // if (rtsz > 31) + printHex(returndatasize()) + switch gt(rtsz, 31) + case 0 { + // Unexpected return data. + printString("Unexpected return data") + _gasLeft := 0 + _eraseReturndataPointer() + } + default { + printString("SHOULD ENTER") + returndatacopy(0, 0, 32) + _gasLeft := mload(0) + returndatacopy(_outputOffset, 32, _outputLen) + mstore(lastRtSzOffset, sub(rtsz, 32)) + + // Skip the returnData + ptrAddIntoActive(32) + } + } + + function _eraseReturndataPointer() { + let lastRtSzOffset := LAST_RETURNDATA_SIZE_OFFSET() + + let activePtrSize := getActivePtrDataSize() + ptrShrinkIntoActive(and(activePtrSize, 0xFFFFFFFF))// uint32(activePtrSize) + mstore(lastRtSzOffset, 0) + } + + function _saveReturndataAfterZkEVMCall() { + let lastRtSzOffset := LAST_RETURNDATA_SIZE_OFFSET() + + mstore(lastRtSzOffset, returndatasize()) + } + + function performCall(oldSp, evmGasLeft, isStatic) -> dynamicGas,sp { + let gasSend,addr,value,argsOffset,argsSize,retOffset,retSize + + gasSend, sp := popStackItem(oldSp) + addr, sp := popStackItem(sp) + value, sp := popStackItem(sp) + argsOffset, sp := popStackItem(sp) + argsSize, sp := popStackItem(sp) + retOffset, sp := popStackItem(sp) + retSize, sp := popStackItem(sp) + + + // code_execution_cost is the cost of the called code execution (limited by the gas parameter). + // If address is warm, then address_access_cost is 100, otherwise it is 2600. See section access sets. + // If value is not 0, then positive_value_cost is 9000. In this case there is also a call stipend that is given to make sure that a basic fallback function can be called. 2300 is thus removed from the cost, and also added to the gas input. + // If value is not 0 and the address given points to an empty account, then value_to_empty_account_cost is 25000. An account is empty if its balance is 0, its nonce is 0 and it has no code. + dynamicGas := expandMemory(add(retOffset,retSize)) + switch warmAddress(addr) + case 0 { dynamicGas := add(dynamicGas,2600) } + default { dynamicGas := add(dynamicGas,100) } + + if not(iszero(value)) { + dynamicGas := add(dynamicGas,6700) + gasSend := add(gasSend,2300) + + if isAddrEmpty(addr) { + dynamicGas := add(dynamicGas,25000) + } + } + + if gt(gasSend,div(mul(evmGasLeft,63),64)) { + gasSend := div(mul(evmGasLeft,63),64) + } + argsOffset := add(argsOffset,MEM_OFFSET_INNER()) + retOffset := add(retOffset,MEM_OFFSET_INNER()) + // TODO: More Checks are needed + // Check gas + let success + + if isStatic { + printString("Static") + if not(iszero(value)) { + revert(0, 0) + } + success, evmGasLeft := _performStaticCall( + _isEVM(addr), + gasSend, + addr, + argsOffset, + argsSize, + retOffset, + retSize + ) + } + + if _isEVM(addr) { + printString("isEVM") + _pushEVMFrame(gasSend, isStatic) + success := call(gasSend, addr, value, argsOffset, argsSize, 0, 0) + printHex(success) + evmGasLeft := _saveReturndataAfterEVMCall(retOffset, retSize) + _popEVMFrame() + printString("EVM Done") + } + + // zkEVM native + if and(iszero(_isEVM(addr)), iszero(isStatic)) { + printString("is zkEVM") + gasSend := _getZkEVMGas(gasSend) + let zkevmGasBefore := gas() + success := call(gasSend, addr, value, argsOffset, argsSize, retOffset, retSize) + _saveReturndataAfterZkEVMCall() + let gasUsed := _calcEVMGas(sub(zkevmGasBefore, gas())) + + evmGasLeft := 0 + if gt(gasSend, gasUsed) { + evmGasLeft := sub(gasSend, gasUsed) + } + printString("zkEVM Done") + } + + sp := pushStackItem(sp,success) + + // TODO: dynamicGas := add(dynamicGas,codeExecutionCost) how to do this? + // Check if the following is ok + dynamicGas := add(dynamicGas,gasSend) + } + + function _performStaticCall( + _calleeIsEVM, + _calleeGas, + _callee, + _inputOffset, + _inputLen, + _outputOffset, + _outputLen + ) -> success, _gasLeft { + if _calleeIsEVM { + _pushEVMFrame(_calleeGas, true) + // TODO Check the following comment from zkSync .sol. + // We can not just pass all gas here to prevert overflow of zkEVM gas counter + success := staticcall(_calleeGas, _callee, _inputOffset, _inputLen, 0, 0) + + _gasLeft := _saveReturndataAfterEVMCall(_outputOffset, _outputLen) + _popEVMFrame() + } + + // zkEVM native + if not(_calleeIsEVM) { + _calleeGas := _getZkEVMGas(_calleeGas) + let zkevmGasBefore := gas() + success := staticcall(_calleeGas, _callee, _inputOffset, _inputLen, _outputOffset, _outputLen) + + _saveReturndataAfterZkEVMCall() + + let gasUsed := _calcEVMGas(sub(zkevmGasBefore, gas())) + + _gasLeft := 0 + if gt(_calleeGas, gasUsed) { + _gasLeft := sub(_calleeGas, gasUsed) + } + } + } + + function isAddrEmpty(addr) -> isEmpty { + isEmpty := 0 + if and( and( + iszero(balance(addr)), + iszero(extcodesize(addr)) ), + iszero(getNonce(addr)) + ) { + isEmpty := 1 + } + } + function genericCreate(addr, offset, size, sp) -> result { pop(warmAddress(addr)) @@ -2935,6 +3480,13 @@ object "EVMInterpreter" { case 0 { sp := pushStackItem(sp, 0) } default { sp := pushStackItem(sp, addr) } } + case 0xF1 { // OP_CALL + let dynamicGas + // A function was implemented in order to avoid stack depth errors. + dynamicGas, sp := performCall(sp, evmGasLeft, isStatic) + + evmGasLeft := chargeGas(evmGasLeft,dynamicGas) + } // TODO: REST OF OPCODES default { // TODO: Revert properly here and report the unrecognized opcode