From db1459fd9a2ac14e174f52e0b50604ae863c2d66 Mon Sep 17 00:00:00 2001 From: Thomas Letan Date: Fri, 11 Jul 2025 15:46:55 +0200 Subject: [PATCH] EVM Node: Output EIP-55-compatible addresses * What This change updates the `address_to_string` function to return addresses with a mixed-case checksum, following the EIP-55 standard. * Why EIP-55 has been introduced to prevent errors from mistyped addresses. It provides a checksum mechanism that helps verify the integrity of an address, reducing the risk of sending funds to an incorrect recipient due to a typo. * How The `address_to_string` function was reimplemented. It now calculates the Keccak-256 hash of the address. Based on this hash, it selectively capitalizes the alphabetic characters of the address string to create the checksum, and prefixes the result with "0x". --- etherlink/CHANGES_NODE.md | 3 ++ .../lib_dev/encodings/ethereum_types.ml | 34 ++++++++++++---- .../lib_dev/encodings/ethereum_types.mli | 2 +- etherlink/bin_node/lib_dev/evm_store.ml | 2 +- etherlink/tezt/tests/evm_rollup.ml | 40 +++++++++---------- etherlink/tezt/tests/evm_sequencer.ml | 34 +++++----------- 6 files changed, 60 insertions(+), 55 deletions(-) diff --git a/etherlink/CHANGES_NODE.md b/etherlink/CHANGES_NODE.md index 179b3adc12c1..3f02d96cf0fb 100644 --- a/etherlink/CHANGES_NODE.md +++ b/etherlink/CHANGES_NODE.md @@ -10,6 +10,9 @@ - `GET /health_check` will now fail if the database connections used by the EVM node are stalled. (!18667) +- RPC responses now output [EIP-55 compliant addresses][eip-55]. (!18676) + +[eip-55]: https://eips.ethereum.org/EIPS/eip-55 ### Metrics changes diff --git a/etherlink/bin_node/lib_dev/encodings/ethereum_types.ml b/etherlink/bin_node/lib_dev/encodings/ethereum_types.ml index e569330e97da..95442b79fd17 100644 --- a/etherlink/bin_node/lib_dev/encodings/ethereum_types.ml +++ b/etherlink/bin_node/lib_dev/encodings/ethereum_types.ml @@ -70,7 +70,27 @@ type address = Address of hex [@@ocaml.unboxed] let address_of_string s = Address (hex_of_string (String.lowercase_ascii s)) -let address_to_string (Address a) = hex_to_string a +let address_to_string (Address (Hex address)) = + (* Implementation compliant with EIP-55 + See https://eips.ethereum.org/EIPS/eip-55 *) + let hexchar_to_int c = + if '0' <= c && c <= '9' then Char.code c - Char.code '0' + else if 'A' <= c && c <= 'F' then Char.code c - Char.code 'A' + 10 + else if 'a' <= c && c <= 'f' then Char.code c - Char.code 'a' + 10 + else raise (Invalid_argument "address_to_string: not a valid address") + in + + let hash = Digestif.KECCAK_256.(to_hex @@ digest_string address) in + let address = Bytes.of_string address in + + Bytes.iteri + (fun i c -> + if 'a' <= c && c <= 'f' then + let hash_nibble = hexchar_to_int (String.get hash i) in + if hash_nibble >= 8 then Bytes.set address i (Char.uppercase_ascii c)) + address ; + + "0x" ^ Bytes.to_string address let address_encoding = Data_encoding.(conv address_to_string address_of_string string) @@ -544,7 +564,7 @@ type 'transaction_object block = { transactionRoot : hash; stateRoot : hash; receiptRoot : hash; - miner : hex; + miner : address; difficulty : quantity; totalDifficulty : quantity; extraData : hex; @@ -606,8 +626,8 @@ let block_from_rlp_v0 bytes = have that, potentially this could be the sequencer. *) let miner = decode_option - ~default:(Hex "0000000000000000000000000000000000000000") - decode_hex + ~default:(Address (Hex "0000000000000000000000000000000000000000")) + decode_address miner in let transactionRoot = @@ -706,8 +726,8 @@ let block_from_rlp_v1 bytes = have that, potentially this could be the sequencer. *) let miner = decode_option - ~default:(Hex "0000000000000000000000000000000000000000") - decode_hex + ~default:(Address (Hex "0000000000000000000000000000000000000000")) + decode_address miner in let transactionRoot = @@ -928,7 +948,7 @@ let block_encoding transaction_object_encoding = (req "transactionsRoot" hash_encoding) (req "stateRoot" hash_encoding) (req "receiptsRoot" hash_encoding) - (req "miner" hex_encoding)) + (req "miner" address_encoding)) (obj10 (req "difficulty" quantity_encoding) (req "totalDifficulty" quantity_encoding) diff --git a/etherlink/bin_node/lib_dev/encodings/ethereum_types.mli b/etherlink/bin_node/lib_dev/encodings/ethereum_types.mli index 099bb54e4c10..9f855b92a66a 100644 --- a/etherlink/bin_node/lib_dev/encodings/ethereum_types.mli +++ b/etherlink/bin_node/lib_dev/encodings/ethereum_types.mli @@ -184,7 +184,7 @@ type 'transaction_object block = { transactionRoot : hash; stateRoot : hash; receiptRoot : hash; - miner : hex; + miner : address; difficulty : quantity; totalDifficulty : quantity; extraData : hex; diff --git a/etherlink/bin_node/lib_dev/evm_store.ml b/etherlink/bin_node/lib_dev/evm_store.ml index 4a1d9f80409a..6310893a3520 100644 --- a/etherlink/bin_node/lib_dev/evm_store.ml +++ b/etherlink/bin_node/lib_dev/evm_store.ml @@ -125,7 +125,7 @@ module Legacy_encodings = struct (req "transactionsRoot" hash_encoding) (req "stateRoot" hash_encoding) (req "receiptsRoot" hash_encoding) - (req "miner" hex_encoding)) + (req "miner" address_encoding)) (obj10 (req "difficulty" quantity_encoding) (req "totalDifficulty" quantity_encoding) diff --git a/etherlink/tezt/tests/evm_rollup.ml b/etherlink/tezt/tests/evm_rollup.ml index e3b3d5daeaca..aca0083086d4 100644 --- a/etherlink/tezt/tests/evm_rollup.ml +++ b/etherlink/tezt/tests/evm_rollup.ml @@ -770,9 +770,8 @@ let deploy_with_base_checks {contract; expected_address} full_evm_setup = let endpoint = Evm_node.endpoint evm_node in let sender = Eth_account.bootstrap_accounts.(0) in let* contract_address, tx = deploy ~contract ~sender full_evm_setup in - let address = String.lowercase_ascii contract_address in Check.( - (address = expected_address) + (contract_address = expected_address) string ~error_msg:"Expected address to be %R but was %L.") ; let* code_in_kernel = @@ -1236,7 +1235,7 @@ let test_l2_deploy_simple_storage = deploy_with_base_checks { contract = simple_storage_resolved; - expected_address = "0xd77420f73b4612a7a99dba8c2afd30a1886b0344"; + expected_address = "0xd77420F73B4612a7A99DBA8c2AFd30a1886b0344"; } evm_setup @@ -1370,8 +1369,7 @@ let test_l2_deploy_erc20 = (* deploy the contract *) let* address, tx = deploy ~contract:erc20_resolved ~sender evm_setup in Check.( - (String.lowercase_ascii address - = "0xd77420f73b4612a7a99dba8c2afd30a1886b0344") + (address = "0xd77420F73B4612a7A99DBA8c2AFd30a1886b0344") string ~error_msg:"Expected address to be %R but was %L.") ; @@ -1490,7 +1488,7 @@ let test_deploy_contract_with_push0 = deploy_with_base_checks { contract = shanghai_storage_resolved; - expected_address = "0xd77420f73b4612a7a99dba8c2afd30a1886b0344"; + expected_address = "0xd77420F73B4612a7A99DBA8c2AFd30a1886b0344"; } evm_setup @@ -1539,7 +1537,7 @@ let test_log_index = ~gas_price:1_000_000_000 ~gas:27_638 ~value:Wei.zero - ~address:"0xd77420f73b4612a7a99dba8c2afd30a1886b0344" + ~address:"0xd77420F73B4612a7A99DBA8c2AFd30a1886b0344" ~signature:"emitBoth(uint256)" ~arguments:["100"] () @@ -1552,7 +1550,7 @@ let test_log_index = ~gas_price:1_000_000_000 ~gas:25_664 ~value:Wei.zero - ~address:"0xd77420f73b4612a7a99dba8c2afd30a1886b0344" + ~address:"0xd77420F73B4612a7A99DBA8c2AFd30a1886b0344" ~signature:"emitA(uint256)" ~arguments:["10"] () @@ -1917,7 +1915,7 @@ let test_rpc_txpool_content = ~gas_price:100_000 ~gas:23_300 ~value:(Wei.of_string "100") - ~address:"0x11d3c9168db9d12a3c591061d555870969b43dc9" + ~address:"0x11D3C9168db9d12a3C591061D555870969b43dC9" () in let* tx2 = @@ -1928,7 +1926,7 @@ let test_rpc_txpool_content = ~gas_price:100_000 ~gas:23_300 ~value:(Wei.of_string "100") - ~address:"0x11d3c9168db9d12a3c591061d555870969b43dc9" + ~address:"0x11D3C9168db9d12a3C591061D555870969b43dC9" () in let*@ tx_hash1 = Rpc.send_raw_transaction ~raw_tx:tx1 evm_node in @@ -1961,12 +1959,12 @@ let test_rpc_txpool_content = let transaction_addr_queued = List.nth txpool_queued 0 in Check.( (transaction_addr_pending.address - = "0x6ce4d79d4e77402e1ef3417fdda433aa744c6e1c") + = "0x6ce4d79d4E77402e1ef3417Fdda433aA744C6e1c") string) ~error_msg:"Expected caller of transaction_1 to be %R, got %L." ; Check.( (transaction_addr_queued.address - = "0x6ce4d79d4e77402e1ef3417fdda433aa744c6e1c") + = "0x6ce4d79d4E77402e1ef3417Fdda433aA744C6e1c") string) ~error_msg:"Expected caller of transaction_2 to be %R, got %L." ; let num_pending_transaction_addr_1 = @@ -1996,13 +1994,13 @@ let test_rpc_txpool_content = ~transaction_content:pending_transaction_addr_1_content ~blockHash:"null" ~blockNumber:"null" - ~from:"0x6ce4d79d4e77402e1ef3417fdda433aa744c6e1c" + ~from:"0x6ce4d79d4E77402e1ef3417Fdda433aA744C6e1c" ~gas:"0x5b04" ~gasPrice:"0x186a0" ~hash:"0xed148f664807dfcb3c7095de22a6c63e72ae4f9d503549c525f5014327b51693" ~input:"0x" ~nonce:"0x0" - ~to_:"0x11d3c9168db9d12a3c591061d555870969b43dc9" + ~to_:"0x11D3C9168db9d12a3C591061D555870969b43dC9" ~transactionIndex:"null" ~value:"0x64" (* TODO: https://gitlab.com/tezos/tezos/-/issues/7194 @@ -2018,13 +2016,13 @@ let test_rpc_txpool_content = ~transaction_content:queued_transaction_addr_1_content ~blockHash:"null" ~blockNumber:"null" - ~from:"0x6ce4d79d4e77402e1ef3417fdda433aa744c6e1c" + ~from:"0x6ce4d79d4E77402e1ef3417Fdda433aA744C6e1c" ~gas:"0x5b04" ~gasPrice:"0x186a0" ~hash:"0x38b2831803a0f9a82bcc68f79bf167311b0adfe9e3d111a7f9579cdfcbae0f0f" ~input:"0x" ~nonce:"0x1" - ~to_:"0x11d3c9168db9d12a3c591061d555870969b43dc9" + ~to_:"0x11D3C9168db9d12a3C591061D555870969b43dC9" ~transactionIndex:"null" ~value:"0x64" (* TODO: https://gitlab.com/tezos/tezos/-/issues/7194 @@ -2371,7 +2369,7 @@ let test_estimate_gas_additionnal_field = let eth_call = [ ( "from", - Ezjsonm.encode_string @@ "0x6ce4d79d4e77402e1ef3417fdda433aa744c6e1c" + Ezjsonm.encode_string @@ "0x6ce4d79d4E77402e1ef3417Fdda433aA744C6e1c" ); ("data", Ezjsonm.encode_string @@ "0x" ^ data); ("value", Ezjsonm.encode_string @@ "0x0"); @@ -2400,8 +2398,7 @@ let test_eth_call_storage_contract = in let* () = check_tx_succeeded ~endpoint ~tx in Check.( - (String.lowercase_ascii address - = "0xd77420f73b4612a7a99dba8c2afd30a1886b0344") + (address = "0xd77420F73B4612a7A99DBA8c2AFd30a1886b0344") string ~error_msg:"Expected address to be %R but was %L.") ; @@ -4373,9 +4370,8 @@ let test_rpc_getLogs = let* erc20_resolved = erc20 evm_version in (* deploy the contract *) let* address, _tx = deploy ~contract:erc20_resolved ~sender evm_setup in - let address = String.lowercase_ascii address in Check.( - (address = "0xd77420f73b4612a7a99dba8c2afd30a1886b0344") + (address = "0xd77420F73B4612a7A99DBA8c2AFd30a1886b0344") string ~error_msg:"Expected address to be %R but was %L.") ; (* minting / burning *) @@ -4619,7 +4615,7 @@ let test_l2_revert_returns_unused_gas = ~gas_price:65_536 ~gas:100_000 ~value:Wei.zero - ~address:"0xd77420f73b4612a7a99dba8c2afd30a1886b0344" + ~address:"0xd77420F73B4612a7A99DBA8c2AFd30a1886b0344" ~signature:"run()" () in diff --git a/etherlink/tezt/tests/evm_sequencer.ml b/etherlink/tezt/tests/evm_sequencer.ml index 505d6e616299..1bd4bfea75be 100644 --- a/etherlink/tezt/tests/evm_sequencer.ml +++ b/etherlink/tezt/tests/evm_sequencer.ml @@ -7938,8 +7938,7 @@ let test_trace_delegate_call = sequencer in Check.( - JSON.( - top_call |-> "to" |> as_string = String.lowercase_ascii delegator_address) + JSON.(top_call |-> "to" |> as_string = delegator_address) string ~error_msg:"Top call should have been traced as coming from %R but was %L") ; Check.( @@ -7948,15 +7947,11 @@ let test_trace_delegate_call = ~error_msg:"Top call should have some subcalls") ; let delegatecall = JSON.(top_call |-> "calls" |> as_list |> List.hd) in Check.( - JSON.( - delegatecall |-> "from" |> as_string - = String.lowercase_ascii delegator_address) + JSON.(delegatecall |-> "from" |> as_string = delegator_address) string ~error_msg:"Delegate call should be traced as coming from %R but was %L") ; Check.( - JSON.( - delegatecall |-> "to" |> as_string - = String.lowercase_ascii delegated_address) + JSON.(delegatecall |-> "to" |> as_string = delegated_address) string ~error_msg: "Delegate call should be traced as going to delegated contract %R but \ @@ -8635,9 +8630,7 @@ let test_trace_transaction_calltracer_deposit = unit let test_miner = - let sequencer_pool_address = - String.lowercase_ascii "0x8aaD6553Cf769Aa7b89174bE824ED0e53768ed70" - in + let sequencer_pool_address = "0x8aaD6553Cf769Aa7b89174bE824ED0e53768ed70" in register_all ~__FILE__ ~tags:["evm"; "miner"; "coinbase"] @@ -8645,7 +8638,7 @@ let test_miner = ~sequencer_pool_address @@ fun {sequencer; evm_version; _} _protocol -> let*@ block = Rpc.get_block_by_number ~block:"latest" sequencer in - Check.((String.lowercase_ascii block.miner = sequencer_pool_address) string) + Check.((block.miner = sequencer_pool_address) string) ~error_msg: "Block miner should be the sequencer pool address, expected %R got %L" ; (* We deploy a contract that stores the block coinbase in its storage, and @@ -8671,10 +8664,7 @@ let test_miner = ~method_call:"getStorageCoinbase()" () in - Check.( - (String.lowercase_ascii @@ String.trim storage_coinbase - = sequencer_pool_address) - string) + Check.((String.trim storage_coinbase = sequencer_pool_address) string) ~error_msg: "Stored coinbase should be the sequencer pool address, expected %R got %L" ; let* view_coinbase = @@ -8685,10 +8675,7 @@ let test_miner = ~method_call:"getStorageCoinbase()" () in - Check.( - (String.lowercase_ascii @@ String.trim view_coinbase - = sequencer_pool_address) - string) + Check.((String.trim view_coinbase = sequencer_pool_address) string) ~error_msg: "Viewed coinbase should be the sequencer pool address, expected %R got %L" ; @@ -10328,11 +10315,11 @@ let test_tx_pool_replacing_transactions_on_limit () = in let txpool_content () = let*@ pending, queued = Rpc.txpool_content sequencer in - let sender = String.lowercase_ascii sender.address in + let sender = sender.address in let pending = List.find_map (fun Rpc.{address; transactions} -> - if String.lowercase_ascii address = sender then + if address = sender then Some (List.map fst transactions |> List.sort compare) else None) pending @@ -10340,7 +10327,7 @@ let test_tx_pool_replacing_transactions_on_limit () = let queued = List.find_map (fun Rpc.{address; transactions} -> - if String.lowercase_ascii address = sender then + if address = sender then Some (List.map fst transactions |> List.sort compare) else None) queued @@ -11113,7 +11100,6 @@ let test_websocket_logs_event = ~bin:erc20.bin) sequencer in - let address = String.lowercase_ascii address in let transfer_event_topic = let h = Tezos_crypto.Hacl.Hash.Keccak_256.digest -- GitLab