diff --git a/etherlink/CHANGES_NODE.md b/etherlink/CHANGES_NODE.md index 498f4910b379db22cc3cac6ba3565b07e3c158db..48b7c860328c39574a59bdd2bc0839b16c3595ee 100644 --- a/etherlink/CHANGES_NODE.md +++ b/etherlink/CHANGES_NODE.md @@ -30,6 +30,8 @@ - The observer mode has the `devmode` flag, similar to the sequencer mode. (!13163) - Adds a `--read-only` flag to the proxy mode, if the flag is set, the transaction pool is not supported. (!13162) +- Support `debug_traceTransaction` with its default logger `structLogs`. + (!13268, !13321, !13350, !13378) ### Experimental diff --git a/etherlink/bin_node/lib_dev/durable_storage_path.ml b/etherlink/bin_node/lib_dev/durable_storage_path.ml index c2e80401f3b0310d1049fbddf0f5e4c8fd3e4664..a94dcf199e9e14ad41bb7a342fd1c5486f1f9d4f 100644 --- a/etherlink/bin_node/lib_dev/durable_storage_path.ml +++ b/etherlink/bin_node/lib_dev/durable_storage_path.ml @@ -126,4 +126,10 @@ module Trace_transaction = struct let output_failed = root ^ "/failed" let output_return_value = root ^ "/return_value" + + let opcodes_root = root ^ "/struct_logs" + + let logs_length = opcodes_root ^ "/length" + + let opcode i = opcodes_root ^ "/" ^ string_of_int i end diff --git a/etherlink/bin_node/lib_dev/durable_storage_path.mli b/etherlink/bin_node/lib_dev/durable_storage_path.mli index 66eb5f15e09cbeccf51448dce34e36024362c632..58d79d06ede8c76da504005dffac301226e40bce 100644 --- a/etherlink/bin_node/lib_dev/durable_storage_path.mli +++ b/etherlink/bin_node/lib_dev/durable_storage_path.mli @@ -104,4 +104,8 @@ module Trace_transaction : sig (** Path where is stored the value returned by the transaction's execution. *) val output_return_value : path + + val logs_length : path + + val opcode : int -> path end diff --git a/etherlink/bin_node/lib_dev/encodings/tracer_types.ml b/etherlink/bin_node/lib_dev/encodings/tracer_types.ml index bfdf9dfb79dfa5ee2b5d50eccdf47d1290613b61..53a3e91c254741e22682c27f4f8ff5e47b060ba7 100644 --- a/etherlink/bin_node/lib_dev/encodings/tracer_types.ml +++ b/etherlink/bin_node/lib_dev/encodings/tracer_types.ml @@ -115,12 +115,531 @@ let input_rlp_encoder hash config = let storage = bool_encoding config.tracer_config.disable_storage in List [hash; return_data; memory; stack; storage] |> encode |> Bytes.to_string -(* This is a temporary type, it should be filled in a follow up patch. Int and - not unit, so that the encoding doesn't fail with: "Cannot insert potentially - zero-sized element in a list." Making it a list ensures it is encoded as an - (empty) array and compatible with the specification until the correct - type. *) -type opcode_log = int +let hex_encoding = + let open Data_encoding in + conv Hex.to_bytes_exn Hex.of_bytes bytes + +module Opcode = struct + (* These two pattern matchings are generated + https://ethereum.org/en/developers/docs/evm/opcodes/, with a combination of + macros. *) + + type t = Char.t + + let opcode_to_string = function + | '\x00' -> "STOP" + | '\x01' -> "ADD" + | '\x02' -> "MUL" + | '\x03' -> "SUB" + | '\x04' -> "DIV" + | '\x05' -> "SDIV" + | '\x06' -> "MOD" + | '\x07' -> "SMOD" + | '\x08' -> "ADDMOD" + | '\x09' -> "MULMOD" + | '\x0A' -> "EXP" + | '\x0B' -> "SIGNEXTEND" + | '\x0C' .. '\x0F' -> "invalid" + | '\x10' -> "LT" + | '\x11' -> "GT" + | '\x12' -> "SLT" + | '\x13' -> "SGT" + | '\x14' -> "EQ" + | '\x15' -> "ISZERO" + | '\x16' -> "AND" + | '\x17' -> "OR" + | '\x18' -> "XOR" + | '\x19' -> "NOT" + | '\x1A' -> "BYTE" + | '\x1B' -> "SHL" + | '\x1C' -> "SHR" + | '\x1D' -> "SAR" + | '\x1E' .. '\x1F' -> "invalid" + | '\x20' -> "KECCAK256" + | '\x21' .. '\x2F' -> "invalid" + | '\x30' -> "ADDRESS" + | '\x31' -> "BALANCE" + | '\x32' -> "ORIGIN" + | '\x33' -> "CALLER" + | '\x34' -> "CALLVALUE" + | '\x35' -> "CALLDATALOAD" + | '\x36' -> "CALLDATASIZE" + | '\x37' -> "CALLDATACOPY" + | '\x38' -> "CODESIZE" + | '\x39' -> "CODECOPY" + | '\x3A' -> "GASPRICE" + | '\x3B' -> "EXTCODESIZE" + | '\x3C' -> "EXTCODECOPY" + | '\x3D' -> "RETURNDATASIZE" + | '\x3E' -> "RETURNDATACOPY" + | '\x3F' -> "EXTCODEHASH" + | '\x40' -> "BLOCKHASH" + | '\x41' -> "COINBASE" + | '\x42' -> "TIMESTAMP" + | '\x43' -> "NUMBER" + | '\x44' -> "PREVRANDAO" + | '\x45' -> "GASLIMIT" + | '\x46' -> "CHAINID" + | '\x47' -> "SELFBALANCE" + | '\x48' -> "BASEFEE" + | '\x49' -> "BLOBHASH" + | '\x4A' -> "BLOBBASEFEE" + | '\x4B' .. '\x4F' -> "invalid" + | '\x50' -> "POP" + | '\x51' -> "MLOAD" + | '\x52' -> "MSTORE" + | '\x53' -> "MSTORE8" + | '\x54' -> "SLOAD" + | '\x55' -> "SSTORE" + | '\x56' -> "JUMP" + | '\x57' -> "JUMPI" + | '\x58' -> "PC" + | '\x59' -> "MSIZE" + | '\x5A' -> "GAS" + | '\x5B' -> "JUMPDEST" + | '\x5C' -> "TLOAD" + | '\x5D' -> "TSTORE" + | '\x5E' -> "MCOPY" + | '\x5F' -> "PUSH0" + | '\x60' -> "PUSH1" + | '\x61' -> "PUSH2" + | '\x62' -> "PUSH3" + | '\x63' -> "PUSH4" + | '\x64' -> "PUSH5" + | '\x65' -> "PUSH6" + | '\x66' -> "PUSH7" + | '\x67' -> "PUSH8" + | '\x68' -> "PUSH9" + | '\x69' -> "PUSH10" + | '\x6A' -> "PUSH11" + | '\x6B' -> "PUSH12" + | '\x6C' -> "PUSH13" + | '\x6D' -> "PUSH14" + | '\x6E' -> "PUSH15" + | '\x6F' -> "PUSH16" + | '\x70' -> "PUSH17" + | '\x71' -> "PUSH18" + | '\x72' -> "PUSH19" + | '\x73' -> "PUSH20" + | '\x74' -> "PUSH21" + | '\x75' -> "PUSH22" + | '\x76' -> "PUSH23" + | '\x77' -> "PUSH24" + | '\x78' -> "PUSH25" + | '\x79' -> "PUSH26" + | '\x7A' -> "PUSH27" + | '\x7B' -> "PUSH28" + | '\x7C' -> "PUSH29" + | '\x7D' -> "PUSH30" + | '\x7E' -> "PUSH31" + | '\x7F' -> "PUSH32" + | '\x80' -> "DUP1" + | '\x81' -> "DUP2" + | '\x82' -> "DUP3" + | '\x83' -> "DUP4" + | '\x84' -> "DUP5" + | '\x85' -> "DUP6" + | '\x86' -> "DUP7" + | '\x87' -> "DUP8" + | '\x88' -> "DUP9" + | '\x89' -> "DUP10" + | '\x8A' -> "DUP11" + | '\x8B' -> "DUP12" + | '\x8C' -> "DUP13" + | '\x8D' -> "DUP14" + | '\x8E' -> "DUP15" + | '\x8F' -> "DUP16" + | '\x90' -> "SWAP1" + | '\x91' -> "SWAP2" + | '\x92' -> "SWAP3" + | '\x93' -> "SWAP4" + | '\x94' -> "SWAP5" + | '\x95' -> "SWAP6" + | '\x96' -> "SWAP7" + | '\x97' -> "SWAP8" + | '\x98' -> "SWAP9" + | '\x99' -> "SWAP10" + | '\x9A' -> "SWAP11" + | '\x9B' -> "SWAP12" + | '\x9C' -> "SWAP13" + | '\x9D' -> "SWAP14" + | '\x9E' -> "SWAP15" + | '\x9F' -> "SWAP16" + | '\xA0' -> "LOG0" + | '\xA1' -> "LOG1" + | '\xA2' -> "LOG2" + | '\xA3' -> "LOG3" + | '\xA4' -> "LOG4" + | '\xA5' .. '\xEF' -> "invalid" + | '\xF0' -> "CREATE" + | '\xF1' -> "CALL" + | '\xF2' -> "CALLCODE" + | '\xF3' -> "RETURN" + | '\xF4' -> "DELEGATECALL" + | '\xF5' -> "CREATE2" + | '\xF6' .. '\xF9' -> "invalid" + | '\xFA' -> "STATICCALL" + | '\xFB' .. '\xFC' -> "invalid" + | '\xFD' -> "REVERT" + | '\xFE' -> + "INVALID" + (* This is the "official" INVALID opcode, contrary to + the others that actually doesn't exist. *) + | '\xFF' -> "SELFDESTRUCT" + + let string_to_opcode = function + | "STOP" -> '\x00' + | "ADD" -> '\x01' + | "MUL" -> '\x02' + | "SUB" -> '\x03' + | "DIV" -> '\x04' + | "SDIV" -> '\x05' + | "MOD" -> '\x06' + | "SMOD" -> '\x07' + | "ADDMOD" -> '\x08' + | "MULMOD" -> '\x09' + | "EXP" -> '\x0A' + | "SIGNEXTEND" -> '\x0B' + | "LT" -> '\x10' + | "GT" -> '\x11' + | "SLT" -> '\x12' + | "SGT" -> '\x13' + | "EQ" -> '\x14' + | "ISZERO" -> '\x15' + | "AND" -> '\x16' + | "OR" -> '\x17' + | "XOR" -> '\x18' + | "NOT" -> '\x19' + | "BYTE" -> '\x1A' + | "SHL" -> '\x1B' + | "SHR" -> '\x1C' + | "SAR" -> '\x1D' + | "KECCAK256" -> '\x20' + | "ADDRESS" -> '\x30' + | "BALANCE" -> '\x31' + | "ORIGIN" -> '\x32' + | "CALLER" -> '\x33' + | "CALLVALUE" -> '\x34' + | "CALLDATALOAD" -> '\x35' + | "CALLDATASIZE" -> '\x36' + | "CALLDATACOPY" -> '\x37' + | "CODESIZE" -> '\x38' + | "CODECOPY" -> '\x39' + | "GASPRICE" -> '\x3A' + | "EXTCODESIZE" -> '\x3B' + | "EXTCODECOPY" -> '\x3C' + | "RETURNDATASIZE" -> '\x3D' + | "RETURNDATACOPY" -> '\x3E' + | "EXTCODEHASH" -> '\x3F' + | "BLOCKHASH" -> '\x40' + | "COINBASE" -> '\x41' + | "TIMESTAMP" -> '\x42' + | "NUMBER" -> '\x43' + | "PREVRANDAO" -> '\x44' + | "GASLIMIT" -> '\x45' + | "CHAINID" -> '\x46' + | "SELFBALANCE" -> '\x47' + | "BASEFEE" -> '\x48' + | "BLOBHASH" -> '\x49' + | "BLOBBASEFEE" -> '\x4A' + | "POP" -> '\x50' + | "MLOAD" -> '\x51' + | "MSTORE" -> '\x52' + | "MSTORE8" -> '\x53' + | "SLOAD" -> '\x54' + | "SSTORE" -> '\x55' + | "JUMP" -> '\x56' + | "JUMPI" -> '\x57' + | "PC" -> '\x58' + | "MSIZE" -> '\x59' + | "GAS" -> '\x5A' + | "JUMPDEST" -> '\x5B' + | "TLOAD" -> '\x5C' + | "TSTORE" -> '\x5D' + | "MCOPY" -> '\x5E' + | "PUSH0" -> '\x5F' + | "PUSH1" -> '\x60' + | "PUSH2" -> '\x61' + | "PUSH3" -> '\x62' + | "PUSH4" -> '\x63' + | "PUSH5" -> '\x64' + | "PUSH6" -> '\x65' + | "PUSH7" -> '\x66' + | "PUSH8" -> '\x67' + | "PUSH9" -> '\x68' + | "PUSH10" -> '\x69' + | "PUSH11" -> '\x6A' + | "PUSH12" -> '\x6B' + | "PUSH13" -> '\x6C' + | "PUSH14" -> '\x6D' + | "PUSH15" -> '\x6E' + | "PUSH16" -> '\x6F' + | "PUSH17" -> '\x70' + | "PUSH18" -> '\x71' + | "PUSH19" -> '\x72' + | "PUSH20" -> '\x73' + | "PUSH21" -> '\x74' + | "PUSH22" -> '\x75' + | "PUSH23" -> '\x76' + | "PUSH24" -> '\x77' + | "PUSH25" -> '\x78' + | "PUSH26" -> '\x79' + | "PUSH27" -> '\x7A' + | "PUSH28" -> '\x7B' + | "PUSH29" -> '\x7C' + | "PUSH30" -> '\x7D' + | "PUSH31" -> '\x7E' + | "PUSH32" -> '\x7F' + | "DUP1" -> '\x80' + | "DUP2" -> '\x81' + | "DUP3" -> '\x82' + | "DUP4" -> '\x83' + | "DUP5" -> '\x84' + | "DUP6" -> '\x85' + | "DUP7" -> '\x86' + | "DUP8" -> '\x87' + | "DUP9" -> '\x88' + | "DUP10" -> '\x89' + | "DUP11" -> '\x8A' + | "DUP12" -> '\x8B' + | "DUP13" -> '\x8C' + | "DUP14" -> '\x8D' + | "DUP15" -> '\x8E' + | "DUP16" -> '\x8F' + | "SWAP1" -> '\x90' + | "SWAP2" -> '\x91' + | "SWAP3" -> '\x92' + | "SWAP4" -> '\x93' + | "SWAP5" -> '\x94' + | "SWAP6" -> '\x95' + | "SWAP7" -> '\x96' + | "SWAP8" -> '\x97' + | "SWAP9" -> '\x98' + | "SWAP10" -> '\x99' + | "SWAP11" -> '\x9A' + | "SWAP12" -> '\x9B' + | "SWAP13" -> '\x9C' + | "SWAP14" -> '\x9D' + | "SWAP15" -> '\x9E' + | "SWAP16" -> '\x9F' + | "LOG0" -> '\xA0' + | "LOG1" -> '\xA1' + | "LOG2" -> '\xA2' + | "LOG3" -> '\xA3' + | "LOG4" -> '\xA4' + | "CREATE" -> '\xF0' + | "CALL" -> '\xF1' + | "CALLCODE" -> '\xF2' + | "RETURN" -> '\xF3' + | "DELEGATECALL" -> '\xF4' + | "CREATE2" -> '\xF5' + | "STATICCALL" -> '\xFA' + | "REVERT" -> '\xFD' + | "INVALID" -> '\xFE' + | "SELFDESTRUCT" -> '\xFF' + | opcode -> Stdlib.failwith (Format.sprintf "Invalid opcode %s" opcode) + + let encoding = + Data_encoding.conv opcode_to_string string_to_opcode Data_encoding.string +end + +(* Serves only for encoding numeric values in JSON that are up to 2^53. *) +type uint53 = Z.t + +let uint53_encoding = + let open Data_encoding in + let uint53_to_json i = + (* See {!Json_data_encoding.int53} *) + if i < Z.shift_left Z.one 53 then `Float (Z.to_float i) + else Stdlib.failwith "JSON cannot accept integers more than 2^53" + in + let json_to_uint53 = function + | `Float i -> Z.of_float i + | _ -> Stdlib.failwith "Invalid representation for uint53" + in + let json_encoding = conv uint53_to_json json_to_uint53 json in + splitted ~json:json_encoding ~binary:z + +type opcode_log = { + pc : uint53; + op : Opcode.t; + gas : uint53; + gas_cost : uint53; + memory : Hex.t list option; + mem_size : int32 option; + stack : Hex.t list option; + return_data : Hex.t option; + storage : (Hex.t * Hex.t) list option; + depth : uint53; + refund : uint53; + error : string option; +} + +let decode_value decode = + let open Result_syntax in + function + | Rlp.Value b -> decode b + | _ -> tzfail (error_of_fmt "Invalid RLP encoding for an expected value") + +let decode_list decode = + let open Result_syntax in + function + | Rlp.List l -> return (decode l) + | _ -> tzfail (error_of_fmt "Invalid RLP encoding for an expected list") + +let opcode_rlp_decoder bytes = + let open Result_syntax in + let* rlp = Rlp.decode bytes in + match rlp with + | Rlp.List + [ + Value pc; + Value op; + Value gas; + Value gas_cost; + Value depth; + error; + stack; + return_data; + raw_memory; + storage; + ] -> + let pc = Bytes.to_string pc |> Z.of_bits in + let* op = + if Bytes.length op > 1 then + tzfail + (error_of_fmt + "Invalid opcode encoding: %a" + Hex.pp + (Hex.of_bytes op)) + else if Bytes.length op = 0 then return '\x00' + else return (Bytes.get op 0) + in + let gas = Bytes.to_string gas |> Z.of_bits in + let gas_cost = Bytes.to_string gas_cost |> Z.of_bits in + let depth = Bytes.to_string depth |> Z.of_bits in + let* error = + Rlp.decode_option + (decode_value (fun e -> return @@ Bytes.to_string e)) + error + in + let* return_data = + Rlp.decode_option + (decode_value (fun d -> return @@ Hex.of_bytes d)) + return_data + in + let* stack = + Rlp.decode_option + (decode_list + @@ List.filter_map (function + | Rlp.List _ -> None + | Rlp.Value v -> Some (Hex.of_bytes v))) + stack + in + let* raw_memory = Rlp.decode_option (decode_value return) raw_memory in + let mem_size = + Option.map (fun m -> Bytes.length m |> Int32.of_int) raw_memory + in + let* memory = + Option.map_e + (fun memory -> + let* chunks = TzString.chunk_bytes 32 memory in + return @@ List.map Hex.of_string chunks) + raw_memory + in + let* storage = + let parse_storage_index = function + | Rlp.List [Value _; Value index; Value value] -> + Some (Hex.of_bytes index, Hex.of_bytes value) + | _ -> None + in + Rlp.decode_option + (decode_list (List.filter_map parse_storage_index)) + storage + in + return + { + pc; + op; + gas; + gas_cost; + memory; + mem_size; + stack; + return_data; + storage; + depth; + refund = Z.zero; + error; + } + | _ -> tzfail (error_of_fmt "Invalid rlp encoding for opcode: %a" Rlp.pp rlp) + +let opcode_encoding = + let open Data_encoding in + conv + (fun { + pc; + op; + gas; + gas_cost; + memory; + mem_size; + stack; + return_data; + storage; + depth; + refund; + error; + } -> + ( ( pc, + op, + gas, + gas_cost, + memory, + mem_size, + stack, + return_data, + storage, + depth ), + (refund, error) )) + (fun ( ( pc, + op, + gas, + gas_cost, + memory, + mem_size, + stack, + return_data, + storage, + depth ), + (refund, error) ) -> + { + pc; + op; + gas; + gas_cost; + memory; + mem_size; + stack; + return_data; + storage; + depth; + refund; + error; + }) + (merge_objs + (obj10 + (req "pc" uint53_encoding) + (req "op" Opcode.encoding) + (req "gas" uint53_encoding) + (req "gasCost" uint53_encoding) + (req "memory" (option (list hex_encoding))) + (req "memSize" (option int32)) + (req "stack" (option (list hex_encoding))) + (req "returnData" (option hex_encoding)) + (req "storage" (option (list (tup2 hex_encoding hex_encoding)))) + (req "depth" uint53_encoding)) + (obj2 (req "refund" uint53_encoding) (req "error" (option string)))) type output = { gas : int64; @@ -140,9 +659,10 @@ let output_encoding = (req "gas" int64) (req "failed" bool) (req "returnValue" Ethereum_types.hash_encoding) - (req "structLogs" (list int31))) + (req "structLogs" (list opcode_encoding))) -let output_binary_decoder ~gas ~failed ~return_value = +let output_binary_decoder ~gas ~failed ~return_value ~struct_logs = + let open Result_syntax in let gas = Ethereum_types.decode_number gas |> fun (Ethereum_types.Qty z) -> Z.to_int64 z @@ -154,4 +674,5 @@ let output_binary_decoder ~gas ~failed ~return_value = let (`Hex hex_value) = Hex.of_bytes return_value in Ethereum_types.hash_of_string hex_value in - {gas; failed; return_value; struct_logs = []} + let* struct_logs = List.map_e opcode_rlp_decoder struct_logs in + return {gas; failed; return_value; struct_logs} diff --git a/etherlink/bin_node/lib_dev/encodings/tracer_types.mli b/etherlink/bin_node/lib_dev/encodings/tracer_types.mli index a27f1a767b08a7a016995286f6ef76b61910d075..b21dd2d99446029fa86ae9ed15cae65aa6aaff51 100644 --- a/etherlink/bin_node/lib_dev/encodings/tracer_types.mli +++ b/etherlink/bin_node/lib_dev/encodings/tracer_types.mli @@ -43,8 +43,32 @@ val input_encoding : (Ethereum_types.hash * config) Data_encoding.t val input_rlp_encoder : Ethereum_types.hash -> config -> string -(* This is a temporary type, it should be filled in a follow up patch. *) -type opcode_log +module Opcode : sig + type t = Char.t + + val opcode_to_string : t -> string + + val encoding : t Data_encoding.t +end + +type uint53 = Z.t + +val uint53_encoding : uint53 Data_encoding.t + +type opcode_log = { + pc : uint53; + op : Opcode.t; + gas : uint53; + gas_cost : uint53; + memory : Hex.t list option; + mem_size : int32 option; + stack : Hex.t list option; + return_data : Hex.t option; + storage : (Hex.t * Hex.t) list option; + depth : uint53; + refund : uint53; + error : string option; +} type output = { gas : int64; @@ -56,4 +80,8 @@ type output = { val output_encoding : output Data_encoding.t val output_binary_decoder : - gas:bytes -> failed:bytes -> return_value:bytes -> output + gas:bytes -> + failed:bytes -> + return_value:bytes -> + struct_logs:bytes list -> + output tzresult diff --git a/etherlink/bin_node/lib_dev/tracer.ml b/etherlink/bin_node/lib_dev/tracer.ml index 497731d00113db04a579faeaf645f53c4d82ac73..830118b5416421f4c070d0584b0285f1231fa820 100644 --- a/etherlink/bin_node/lib_dev/tracer.ml +++ b/etherlink/bin_node/lib_dev/tracer.ml @@ -15,6 +15,30 @@ let read_value ?default state path = | Some d -> return d | None -> tzfail Tracer_types.Trace_not_found) +let read_logs_length state = + let open Lwt_result_syntax in + let* value = + read_value + ~default:(Bytes.of_string "\000") + state + Durable_storage_path.Trace_transaction.logs_length + in + return (Bytes.to_string value |> Z.of_bits |> Z.to_int) + +let read_opcode state opcode_index = + read_value state (Durable_storage_path.Trace_transaction.opcode opcode_index) + +let read_logs state = + let open Lwt_result_syntax in + let* length = read_logs_length state in + let*? opcodes = + List.init + ~when_negative_length:(TzTrace.make (error_of_fmt "Invalid length")) + length + Fun.id + in + List.map_es (read_opcode state) opcodes + let read_output state = let open Lwt_result_syntax in let* gas = @@ -30,7 +54,9 @@ let read_output state = state Durable_storage_path.Trace_transaction.output_return_value in - return @@ Tracer_types.output_binary_decoder ~gas ~failed ~return_value + let* struct_logs = read_logs state in + Lwt.return + @@ Tracer_types.output_binary_decoder ~gas ~failed ~return_value ~struct_logs let trace_transaction ~block_number ~transaction_hash ~config = let open Lwt_result_syntax in diff --git a/etherlink/tezt/lib/helpers.ml b/etherlink/tezt/lib/helpers.ml index ff1e57b7891632b07bf31ddc322f76741f0a4d44..9781dce16b606c0a3006970a1c497a78d03446ab 100644 --- a/etherlink/tezt/lib/helpers.ml +++ b/etherlink/tezt/lib/helpers.ml @@ -47,6 +47,8 @@ let mapping_position index map_position = let hex_string_to_int x = `Hex x |> Hex.to_string |> Z.of_bits |> Z.to_int +let hex_256_of_int n = Printf.sprintf "%064x" n + let next_rollup_node_level ~sc_rollup_node ~client = let* () = Client.bake_for_and_wait ~keys:[] client in Sc_rollup_node.wait_sync ~timeout:30. sc_rollup_node diff --git a/etherlink/tezt/lib/helpers.mli b/etherlink/tezt/lib/helpers.mli index d26bd390e7de4aed1f789942774ee96cef439a84..373928079b7cac69afac3f8c903ac0452f6663a7 100644 --- a/etherlink/tezt/lib/helpers.mli +++ b/etherlink/tezt/lib/helpers.mli @@ -45,6 +45,9 @@ val mapping_position : string -> int -> string (** Transform an hexadecimal string to an integer using {!Z.of_bits}. *) val hex_string_to_int : string -> int +(** [hex_256_of_int n] returns the H256 of [n]. *) +val hex_256_of_int : int -> string + (** [next_rollup_node_level ~sc_rollup_node ~client] moves [sc_rollup_node] to the next level l1. *) val next_rollup_node_level : diff --git a/etherlink/tezt/lib/rpc.ml b/etherlink/tezt/lib/rpc.ml index 491630057dd25fa64731ae477887cb9619bc1f0b..e810737d5cb0f5a0ba36e78f23529da0db86aa1f 100644 --- a/etherlink/tezt/lib/rpc.ml +++ b/etherlink/tezt/lib/rpc.ml @@ -383,6 +383,4 @@ let trace_transaction ~transaction_hash ?tracer ?tracer_config evm_node = (Request.trace_transaction ~transaction_hash ?tracer ?tracer_config ()) in return - @@ decode_or_error - (fun response -> Evm_node.extract_result response |> ignore) - response + @@ decode_or_error (fun response -> Evm_node.extract_result response) response diff --git a/etherlink/tezt/lib/rpc.mli b/etherlink/tezt/lib/rpc.mli index e092d78f96b9ae94afee7f2703dc07dc72ba1a9f..0760dc9ba9a8d7c783eeea51c36487c440bb6fb5 100644 --- a/etherlink/tezt/lib/rpc.mli +++ b/etherlink/tezt/lib/rpc.mli @@ -169,4 +169,4 @@ val trace_transaction : ?tracer:string -> ?tracer_config:(string * JSON.u) list -> Evm_node.t -> - (unit, error) result Lwt.t + (JSON.t, error) result Lwt.t diff --git a/etherlink/tezt/tests/evm_rollup.ml b/etherlink/tezt/tests/evm_rollup.ml index 51ae8b3ced5b38ce7198c7f874d59329067bf104..a8796285a3331f0ae90586c012bbcd0e8ec7a0d9 100644 --- a/etherlink/tezt/tests/evm_rollup.ml +++ b/etherlink/tezt/tests/evm_rollup.ml @@ -48,8 +48,6 @@ type full_evm_setup = { kernel_root_hash : string; } -let hex_256_of n = Printf.sprintf "%064x" n - let hex_256_of_address acc = let s = acc.Eth_account.address in (* strip 0x and convert to lowercase *) @@ -152,7 +150,11 @@ let get_value_in_storage sc_rollup_node address nth = @@ Sc_rollup_rpc.get_global_block_durable_state_value ~pvm_kind ~operation:Sc_rollup_rpc.Value - ~key:(Durable_storage_path.storage address ~key:(hex_256_of nth) ()) + ~key: + (Durable_storage_path.storage + address + ~key:(Helpers.hex_256_of_int nth) + ()) () let check_str_in_storage ~evm_setup ~address ~nth ~expected = @@ -162,7 +164,11 @@ let check_str_in_storage ~evm_setup ~address ~nth ~expected = unit let check_nb_in_storage ~evm_setup ~address ~nth ~expected = - check_str_in_storage ~evm_setup ~address ~nth ~expected:(hex_256_of expected) + check_str_in_storage + ~evm_setup + ~address + ~nth + ~expected:(Helpers.hex_256_of_int expected) let get_storage_size sc_rollup_node ~address = let* storage = @@ -1135,14 +1141,14 @@ let test_l2_deploy_erc20 = [ ( address, [transfer_event_topic; zero_address; hex_256_of_address sender], - "0x" ^ hex_256_of amount ); + "0x" ^ Helpers.hex_256_of_int amount ); ] in let burn_logs sender amount = [ ( address, [transfer_event_topic; hex_256_of_address sender; zero_address], - "0x" ^ hex_256_of amount ); + "0x" ^ Helpers.hex_256_of_int amount ); ] in (* sender mints 42 *) @@ -3676,12 +3682,16 @@ let test_rpc_getStorageAt = let* () = check_tx_succeeded ~endpoint ~tx in let*@ hex_value = Rpc.get_storage_at ~address ~pos:"0x0" evm_node in Check.( - (Durable_storage_path.no_0x hex_value = hex_256_of expected_value0) string) + (Durable_storage_path.no_0x hex_value + = Helpers.hex_256_of_int expected_value0) + string) ~error_msg:"Expected %R, but got %L" ; let pos = Helpers.mapping_position sender.address 1 in let*@ hex_value = Rpc.get_storage_at ~address ~pos evm_node in Check.( - (Durable_storage_path.no_0x hex_value = hex_256_of expected_value1) string) + (Durable_storage_path.no_0x hex_value + = Helpers.hex_256_of_int expected_value1) + string) ~error_msg:"Expected %R, but got %L" ; unit @@ -3916,7 +3926,7 @@ let test_rpc_getLogs = [ ( address, [transfer_event_topic; hex_256_of_address sender; zero_address], - "0x" ^ hex_256_of amount ); + "0x" ^ Helpers.hex_256_of_int amount ); ] in (* sender mints 42 *) diff --git a/etherlink/tezt/tests/evm_sequencer.ml b/etherlink/tezt/tests/evm_sequencer.ml index 21a0a16bbaceb0cd393d3d33c7377636ed7fb8a2..e8f188de24c5c93dd4d3a9fe1ba51b14b70590ff 100644 --- a/etherlink/tezt/tests/evm_sequencer.ml +++ b/etherlink/tezt/tests/evm_sequencer.ml @@ -3518,6 +3518,178 @@ let test_trace_transaction_on_invalid_transaction = ~error_msg:"traceTransaction failed with the wrong error")) ; unit +let check_trace expect_null expected_returned_value receipt trace = + (* Checks that each opcode log are either all empty or non empty, considering + the configuration. *) + let check_struct_logs expect_null log = + let check_field field = + if expect_null then + Check.( + JSON.(log |-> field |> JSON.unannotate = `Null) + json_u + ~error_msg: + (Format.sprintf + "Field %s was expected to be null, but got %%L instead" + field)) + else + Check.( + JSON.(log |-> field |> JSON.unannotate <> `Null) + json_u + ~error_msg: + (Format.sprintf "Field %s wasn't expected to be null" field)) + in + check_field "memory" ; + check_field "storage" ; + check_field "memSize" ; + check_field "stack" ; + check_field "returnData" + in + let failed = JSON.(trace |-> "failed" |> as_bool) in + let gas_used = JSON.(trace |-> "gas" |> as_int64) in + let returned_value = + JSON.(trace |-> "returnValue" |> as_string |> Durable_storage_path.no_0x) + in + let logs = JSON.(trace |-> "structLogs" |> as_list) in + Check.( + (failed <> receipt.Transaction.status) + bool + ~error_msg:"The trace has a different status than in the receipt") ; + Check.( + (gas_used = receipt.gasUsed) + int64 + ~error_msg:"Trace reported %L gas used, but the trace reported %R") ; + + (* Whether we don't expect a value and we get "0x", or we expect a value and + it is encoded into a H256. *) + (match expected_returned_value with + | Some value -> + let expected_value = Helpers.hex_256_of_int value in + Check.( + (returned_value = expected_value) + string + ~error_msg: + "The transaction returned the value %L, but %R was expected") + | None -> + Check.( + (returned_value = "") + string + ~error_msg:"The transaction shouldn't return a value, but returned %L")) ; + + (* Checks the logs are consistent with the configuration (its an all in or + all out). *) + Check.((logs <> []) (list json) ~error_msg:"Logs shouldn't be empty") ; + List.iter + (check_struct_logs expect_null) + JSON.(trace |-> "structLogs" |> as_list) ; + unit + +let test_trace_transaction_call = + register_both + ~tags:["evm"; "rpc"; "trace"; "call"] + ~title:"Sequencer can run debug_traceTransaction and return a valid log" + ~da_fee:Wei.zero + @@ fun {sc_rollup_node; sequencer; client; proxy; _} _protocol -> + (* Transfer funds to a random address. *) + let endpoint = Evm_node.endpoint sequencer in + let sender = Eth_account.bootstrap_accounts.(0) in + + (* deploy contract *) + let* () = + Eth_cli.add_abi + ~label:Solidity_contracts.simple_storage.label + ~abi:Solidity_contracts.simple_storage.abi + () + in + let* contract_address, _tx_deployment = + send_transaction + (fun () -> + Eth_cli.deploy + ~source_private_key:sender.Eth_account.private_key + ~endpoint + ~abi:Solidity_contracts.simple_storage.label + ~bin:Solidity_contracts.simple_storage.bin) + sequencer + in + (* Block few levels to ensure we are replaying on an old block. *) + let* () = + repeat 2 (fun () -> + next_evm_level ~evm_node:sequencer ~sc_rollup_node ~client) + in + let* () = bake_until_sync ~sequencer ~sc_rollup_node ~proxy ~client () in + + (* We will first trace with every options enabled, and check that we have the + logs as complete as possible. We call the function `set` from the contract, + which isn't expected to fail and doesn't return any result. *) + let value_in_storage = 10 in + let* transaction_hash = + send_transaction + (Eth_cli.contract_send + ~source_private_key:sender.private_key + ~endpoint + ~abi_label:Solidity_contracts.simple_storage.label + ~address:contract_address + ~method_call:(Format.sprintf "set(%d)" value_in_storage)) + sequencer + in + let* () = + repeat 2 (fun () -> + next_evm_level ~evm_node:sequencer ~sc_rollup_node ~client) + in + let* () = bake_until_sync ~sequencer ~sc_rollup_node ~proxy ~client () in + (* We will use the receipt to check that the results from the trace are + consistent with the result from the transaction once applied in the + block. *) + let*@ transaction_receipt = + Rpc.get_transaction_receipt ~tx_hash:transaction_hash sequencer + in + let transaction_receipt = Option.get transaction_receipt in + let* trace_result = + Rpc.trace_transaction + ~transaction_hash + ~tracer_config: + [("enableMemory", `Bool true); ("enableReturnData", `Bool true)] + sequencer + in + let* () = + match trace_result with + | Ok trace -> check_trace false None transaction_receipt trace + | Error _ -> Test.fail "Trace transaction shouldn't have failed" + in + + (* The second test will disable every tracing options and call `get`, thats + returns a value, so that we can check that it returns the correct value in + the end. *) + let* transaction_hash = + send_transaction + (Eth_cli.contract_send + ~source_private_key:sender.private_key + ~endpoint + ~abi_label:Solidity_contracts.simple_storage.label + ~address:contract_address + ~method_call:"get()") + sequencer + in + let* () = + repeat 2 (fun () -> + next_evm_level ~evm_node:sequencer ~sc_rollup_node ~client) + in + let* () = bake_until_sync ~sequencer ~sc_rollup_node ~proxy ~client () in + let*@ transaction_receipt = + Rpc.get_transaction_receipt ~tx_hash:transaction_hash sequencer + in + let transaction_receipt = Option.get transaction_receipt in + let* trace_result = + Rpc.trace_transaction + ~transaction_hash + ~tracer_config: + [("disableStack", `Bool true); ("disableStorage", `Bool true)] + sequencer + in + match trace_result with + | Ok trace -> + check_trace true (Some value_in_storage) transaction_receipt trace + | Error _ -> Test.fail "Trace transaction shouldn't have failed" + let protocols = Protocol.all let () = @@ -3568,4 +3740,5 @@ let () = test_replay_rpc protocols ; test_txpool_content_empty_with_legacy_encoding protocols ; test_trace_transaction protocols ; - test_trace_transaction_on_invalid_transaction protocols + test_trace_transaction_on_invalid_transaction protocols ; + test_trace_transaction_call protocols