diff --git a/etherlink/bin_node/lib_dev/durable_storage_path.ml b/etherlink/bin_node/lib_dev/durable_storage_path.ml index 17ff4106fab9ca65d231751a5194f997e91998f9..d9466b81cc99b47d999b4a01198a57f036cbfc4e 100644 --- a/etherlink/bin_node/lib_dev/durable_storage_path.ml +++ b/etherlink/bin_node/lib_dev/durable_storage_path.ml @@ -51,29 +51,74 @@ let sequencer_key = EVM.make "/sequencer" let maximum_gas_per_transaction = EVM.make "/maximum_gas_per_transaction" module Accounts = struct - let accounts = World_state.make "/eth_accounts" + let accounts_path = World_state.make "/eth_accounts" - let balance = "/balance" + let balance_path = "/balance" - let nonce = "/nonce" + let nonce_path = "/nonce" - let code = "/code" + let code_path = "/code" let code_hash = "/code.hash" - let storage = "/storage" + let storage_path = "/storage" - let account (Address (Hex s)) = accounts ^ "/" ^ s + let account (Address (Hex s)) = accounts_path ^ "/" ^ s - let balance address = account address ^ balance + let balance address = account address ^ balance_path - let nonce address = account address ^ nonce + let nonce address = account address ^ nonce_path - let code address = account address ^ code + let code address = account address ^ code_path let code_hash address = account address ^ code_hash - let storage address index = account address ^ storage ^ "/" ^ index + let storage address index = account address ^ storage_path ^ "/" ^ index + + type error += Invalid_address of string | Invalid_key of string + + let () = + register_error_kind + `Permanent + ~id:"durable_storage_invalid_address" + ~title:"Invalid address" + ~description:"Tried to access the account storage of an invalid address." + ~pp:(fun ppf path -> + Format.fprintf ppf "No account storage for invalid address %s" path) + Data_encoding.(obj1 (req "durable_storage_invalid_address" string)) + (function Invalid_address path -> Some path | _ -> None) + (fun path -> Invalid_address path) ; + register_error_kind + `Permanent + ~id:"durable_storage_invalid_key" + ~title:"Invalid storage key" + ~description:"Tried to access an invalid key in an account storage." + ~pp:(fun ppf path -> + Format.fprintf ppf "%s is not a valid storage key" path) + Data_encoding.(obj1 (req "durable_storage_invalid_key" string)) + (function Invalid_key path -> Some path | _ -> None) + (fun path -> Invalid_key path) + + let account_e (Address (Hex s)) = + let open Result_syntax in + if String.length s = 40 then return (accounts_path ^ "/" ^ s) + else tzfail (Invalid_address s) + + let concat_e address path = + let open Result_syntax in + let* address_path = account_e address in + return (address_path ^ path) + + let balance_e address = concat_e address balance_path + + let nonce_e address = concat_e address nonce_path + + let code_e address = concat_e address code_path + + let storage_e address index = + if String.length index = 64 then + concat_e address (storage_path ^ "/" ^ index) + else Result_syntax.tzfail (Invalid_key index) end module Code = struct diff --git a/etherlink/bin_node/lib_dev/durable_storage_path.mli b/etherlink/bin_node/lib_dev/durable_storage_path.mli index a7d51da3da97537856da7861a7b916688673e2df..6cce82a852ed246e1ac503bc504c68407a72cae4 100644 --- a/etherlink/bin_node/lib_dev/durable_storage_path.mli +++ b/etherlink/bin_node/lib_dev/durable_storage_path.mli @@ -54,6 +54,21 @@ module Accounts : sig (** Path to the account's storage at a given index. *) val storage : address -> path -> path + + type error += Invalid_address of string | Invalid_key of string + + (** Path to the account's balance. Error if address is invalid. *) + val balance_e : address -> path tzresult + + (** Path to the account's nonce. Error if address is invalid. *) + val nonce_e : address -> path tzresult + + (** Path to the account's code. Error if address is invalid. *) + val code_e : address -> path tzresult + + (** Path to the account's storage at a given index. Error if address or + storage key is invalid. *) + val storage_e : address -> path -> path tzresult end module Code : sig diff --git a/etherlink/bin_node/lib_dev/encodings/ethereum_types.ml b/etherlink/bin_node/lib_dev/encodings/ethereum_types.ml index e38b1ff9a5ac7ec0725fe480d5781224f949680f..9714495d7e0146598b6bb38ac8f59aaaec9c0730 100644 --- a/etherlink/bin_node/lib_dev/encodings/ethereum_types.ml +++ b/etherlink/bin_node/lib_dev/encodings/ethereum_types.ml @@ -1059,10 +1059,23 @@ module From_rlp = struct Result_syntax.return @@ Z.to_int @@ Z.of_bits @@ Bytes.to_string b) end +module StorageKey = struct + type t = hex + + let compare (Hex a) (Hex b) = String.compare a b + + let of_string = hex_of_string + + let to_string = hex_to_string +end + +module StorageMap = MapMake (StorageKey) + type state_account_override = { balance : quantity option; nonce : quantity option; code : hex option; + state_diff : hex StorageMap.t; } type state_override = state_account_override AddressMap.t @@ -1070,12 +1083,18 @@ type state_override = state_account_override AddressMap.t let state_account_override_encoding = let open Data_encoding in conv - (fun {balance; nonce; code} -> (balance, nonce, code)) - (fun (balance, nonce, code) -> {balance; nonce; code}) - (obj3 + (fun {balance; nonce; code; state_diff} -> + (balance, nonce, code, state_diff)) + (fun (balance, nonce, code, state_diff) -> + {balance; nonce; code; state_diff}) + (obj4 (opt "balance" quantity_encoding) (opt "nonce" quantity_encoding) - (opt "code" hex_encoding)) + (opt "code" hex_encoding) + (dft + "state_diff" + (StorageMap.associative_array_encoding hex_encoding) + StorageMap.empty)) let state_override_empty = AddressMap.empty diff --git a/etherlink/bin_node/lib_dev/encodings/ethereum_types.mli b/etherlink/bin_node/lib_dev/encodings/ethereum_types.mli index e2eb6532cd97933f55c57b7b596a61748b916050..db0d09f40f3acd622b1d66ecdc2e9d6e319f4c45 100644 --- a/etherlink/bin_node/lib_dev/encodings/ethereum_types.mli +++ b/etherlink/bin_node/lib_dev/encodings/ethereum_types.mli @@ -232,10 +232,17 @@ type txpool = { val txpool_encoding : txpool Data_encoding.t +module StorageMap : sig + include Map.S with type key = hex + + val associative_array_encoding : 'a Data_encoding.t -> 'a t Data_encoding.t +end + type state_account_override = { balance : quantity option; nonce : quantity option; code : hex option; + state_diff : hex StorageMap.t; } type state_override = state_account_override AddressMap.t diff --git a/etherlink/bin_node/lib_dev/evm_ro_context.ml b/etherlink/bin_node/lib_dev/evm_ro_context.ml index 166e5eaaf7418bcca689bfa8211e71e73047cf1e..d438723263f56cc63e8800d0773b43874f77174d 100644 --- a/etherlink/bin_node/lib_dev/evm_ro_context.ml +++ b/etherlink/bin_node/lib_dev/evm_ro_context.ml @@ -205,7 +205,7 @@ struct ~destination:Ctxt.ctxt.smart_rollup_address () in - let*! simulate_state = + let* simulate_state = State_override.update_accounts state_override simulate_state in let* raw_insights = diff --git a/etherlink/bin_node/lib_dev/state_override.ml b/etherlink/bin_node/lib_dev/state_override.ml index 9d4a9bd7c73fb273a00b7341cfa5e23a7627aa91..e0b4baccb011cdf30a924e326935ebb6361ad822 100644 --- a/etherlink/bin_node/lib_dev/state_override.ml +++ b/etherlink/bin_node/lib_dev/state_override.ml @@ -11,22 +11,54 @@ let durable_nonce v = v |> Ethereum_types.encode_u64_le |> Bytes.to_string let durable_code = Ethereum_types.hex_to_bytes -let update_account address {Ethereum_types.balance; nonce; code} state = +type error += Invalid_storage_value of string + +let () = + register_error_kind + `Permanent + ~id:"durable_storage_invalid_value" + ~title:"Invalid storage value" + ~description:"Tried to set an invalid value in an account storage." + ~pp:(fun ppf path -> + Format.fprintf ppf "%s is not a valid storage value" path) + Data_encoding.(obj1 (req "durable_storage_invalid_value" string)) + (function Invalid_storage_value path -> Some path | _ -> None) + (fun path -> Invalid_storage_value path) + +let update_storage address state_diff state = + let open Ethereum_types in + let open Lwt_result_syntax in + let update key value state = + let (Hex key) = key in + let (Hex value) = value in + if String.length value = 64 then + let*? key = Durable_storage_path.Accounts.storage_e address key in + let*! state = Evm_state.modify ~key ~value state in + return state + else tzfail (Invalid_storage_value value) + in + StorageMap.fold_es update state_diff state + +let update_account address {Ethereum_types.balance; nonce; code; state_diff} + state = let open Durable_storage_path in - let open Lwt_syntax in + let open Lwt_result_syntax in let update v_opt key encode state = match v_opt with - | None -> Lwt_syntax.return state - | Some v -> Evm_state.modify ~key ~value:(encode v) state + | None -> return state + | Some v -> + let*! state = Evm_state.modify ~key ~value:(encode v) state in + return state in let* state = update balance (Accounts.balance address) durable_balance state in let* state = update nonce (Accounts.nonce address) durable_nonce state in let* state = update code (Accounts.code address) durable_code state in + let* state = update_storage address state_diff state in return state let update_accounts state_override state = match state_override with - | None -> Lwt_syntax.return state - | Some so -> Ethereum_types.AddressMap.fold_s update_account so state + | None -> Lwt_result_syntax.return state + | Some so -> Ethereum_types.AddressMap.fold_es update_account so state diff --git a/etherlink/bin_node/lib_dev/state_override.mli b/etherlink/bin_node/lib_dev/state_override.mli index 470131e9857196364a9b430febc77a8a4d374069..2c54a5acbccedf6fe1d18515b4d2abaf6b92dfc6 100644 --- a/etherlink/bin_node/lib_dev/state_override.mli +++ b/etherlink/bin_node/lib_dev/state_override.mli @@ -6,4 +6,6 @@ (*****************************************************************************) val update_accounts : - Ethereum_types.state_override option -> Evm_state.t -> Evm_state.t Lwt.t + Ethereum_types.state_override option -> + Evm_state.t -> + Evm_state.t tzresult Lwt.t diff --git a/etherlink/kernel_evm/solidity_examples/state_override_tester.sol b/etherlink/kernel_evm/solidity_examples/state_override_tester.sol index c31bc544624a4a3a8c86e41f379a159919e9583b..a91b3b8e612d6665e6028f342161a11811219e63 100644 --- a/etherlink/kernel_evm/solidity_examples/state_override_tester.sol +++ b/etherlink/kernel_evm/solidity_examples/state_override_tester.sol @@ -9,7 +9,10 @@ contract Child { contract StateOverrideTester { - int private count = 42; + uint32 private count = 42; // private, so requires an explicit accessor + uint32 public const2 = 0xffffffff; + uint256 public sep = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + uint32 public const3 = 0xffffffff; function getBalance() public view returns (uint256) { return msg.sender.balance; diff --git a/etherlink/kernel_evm/solidity_examples/state_override_tester_readable.sol b/etherlink/kernel_evm/solidity_examples/state_override_tester_readable.sol index 1eebcb578686e8d3a5b62e6c00e8dbc1227f7d01..527367d81dfce5ea88ebaf60608dc8157bff1d5e 100644 --- a/etherlink/kernel_evm/solidity_examples/state_override_tester_readable.sol +++ b/etherlink/kernel_evm/solidity_examples/state_override_tester_readable.sol @@ -2,13 +2,21 @@ pragma solidity ^0.8.4; contract StateOverrideTester { - int private count = 42; + // first two 32bit value should be in same memory slot + uint32 private count = 42; // private, so require an explicit accessor + uint32 public const2 = 0xffffffff; + + // define a 32byte value to open a new memory slot + uint256 public sep = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + + // should be a third slot + uint32 public const3 = 0xffffffff; function getBalance() public view returns (uint256) { return msg.sender.balance; } - function getCount() public view returns (int) { + function getCount() public view returns (uint32) { return count; } } diff --git a/etherlink/tezt/tests/eth_call.ml b/etherlink/tezt/tests/eth_call.ml index 0075b7a1b3cf7de5a1070b21c4b93ebf3e2878d3..719c42bac4972e19017e333f7474fb2b7f999cd3 100644 --- a/etherlink/tezt/tests/eth_call.ml +++ b/etherlink/tezt/tests/eth_call.ml @@ -49,7 +49,7 @@ let test_call_state_override_balance = @@ fun {sequencer; _} _protocol -> (* This test checks that the simulation allows balance override. - To do so we deploy a contract which returns the balance of the message + To do so we deploy a contract which returns the balance of the message sender, and call it with a non-sensical address. *) let* constant = Solidity_contracts.state_override_tester () in @@ -238,9 +238,112 @@ let test_call_state_override_nonce = unit +let test_call_state_override_state_diff = + register + ~kernels:[Latest] (* Not a kernel specific test. *) + ~tags:["evm"; "state_override"; "state_diff"; "eth_call"] + ~title:"Can override part of account storage in eth_call" + @@ fun {sequencer; _} _protocol -> + (* + This test checks that the simulation allows state diff override. + To do so we deploy a contract with a value in storage, and call it with an + alternative storage that changes that value. + *) + let* constant = Solidity_contracts.state_override_tester_readable () in + let* () = Eth_cli.add_abi ~label:constant.label ~abi:constant.abi () in + (* Deploy the contract. *) + let* contract, _tx_hash = + send_transaction_to_sequencer + (fun () -> + Eth_cli.deploy + ~source_private_key:Eth_account.bootstrap_accounts.(0).private_key + ~endpoint:(Evm_node.endpoint sequencer) + ~abi:constant.abi + ~bin:constant.bin) + sequencer + in + + (* helpers *) + let call_method m = + let* calldata = Cast.calldata m in + return (`O [("to", `String contract); ("data", `String calldata)]) + in + let make_call ?(override = []) m = + let* call = call_method m in + Evm_node.call_evm_rpc + sequencer + {method_ = "eth_call"; parameters = `A (call :: override)} + in + let check_value call_result expected = + Check.( + (Evm_node.extract_result call_result |> JSON.as_string = expected) string) + ~error_msg:"Expected result %R but got %L " + in + + (* Check the starting contract storage *) + let* call_result = make_call "getCount()" in + check_value + call_result + "0x000000000000000000000000000000000000000000000000000000000000002a" ; + let* call_result = make_call "const2()" in + check_value + call_result + "0x00000000000000000000000000000000000000000000000000000000ffffffff" ; + let* call_result = make_call "const3()" in + check_value + call_result + "0x00000000000000000000000000000000000000000000000000000000ffffffff" ; + + (* try again with an override *) + let state_diff = + `O + [ + ( "0x0000000000000000000000000000000000000000000000000000000000000000", + `String + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + ] + in + let override = [`O [(contract, `O [("state_diff", state_diff)])]] in + let* call_result = make_call ~override "getCount()" in + check_value + call_result + "0x0000000000000000000000000000000000000000000000000000000000000000" ; + (* const2 is stored in same memory slot so should change *) + let* call_result = make_call ~override "const2()" in + check_value + call_result + "0x0000000000000000000000000000000000000000000000000000000000000000" ; + (* const3 is stored in a distinct memory slot so should be unchanged *) + let* call_result = make_call ~override "const3()" in + check_value + call_result + "0x00000000000000000000000000000000000000000000000000000000ffffffff" ; + + (* try with an invalid override *) + let invalid = + `O + [ + ( "0x00", + `String + "0x0000000000000000000000000000000000000000000000000000000000000000" + ); + ] + in + let override = [`O [(contract, `O [("state_diff", invalid)])]] in + let* call_result = make_call ~override "getCount()" in + Check.( + (Evm_node.extract_error_message call_result + |> JSON.as_string = "Error:\n 00 is not a valid storage key\n") + string) + ~error_msg:"Expected error %R but got %L " ; + + unit + let protocols = Protocol.all let () = test_call_state_override_code protocols ; test_call_state_override_nonce protocols ; + test_call_state_override_state_diff protocols ; test_call_state_override_balance protocols