Add function for validating address hashes (#537)

pull/557/head
Alex Garibay 6 years ago committed by GitHub
parent 9b8b66241d
commit 0f61ea45cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 119
      apps/explorer/lib/explorer/chain/hash/address.ex
  2. 31
      apps/explorer/test/explorer/chain/hash/address_test.exs

@ -148,4 +148,123 @@ defmodule Explorer.Chain.Hash.Address do
@impl Hash
def byte_count, do: @byte_count
@doc """
Validates a hexadecimal encoded string to see if it conforms to an address.
## Error Descriptions
* `:invalid_characters` - String used non-hexidecimal characters
* `:invalid_checksum` - Mixed-case string didn't pass [EIP-55 checksum](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md)
* `:invalid_length` - Addresses are expected to be 40 hex characters long
## Example
iex> Explorer.Chain.Hash.Address.validate("0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d")
{:ok, "0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d"}
iex> Explorer.Chain.Hash.Address.validate("0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232H")
{:error, :invalid_characters}
"""
@spec validate(String.t()) :: {:ok, String.t()} | {:error, :invalid_length | :invalid_characters, :invalid_checksum}
def validate("0x" <> hash) do
with {:length, true} <- {:length, String.length(hash) == 40},
{:hex, true} <- {:hex, is_hex?(hash)},
{:mixed_case, true} <- {:mixed_case, is_mixed_case?(hash)},
{:checksummed, true} <- {:checksummed, is_checksummed?(hash)} do
{:ok, "0x" <> hash}
else
{:length, false} ->
{:error, :invalid_length}
{:hex, false} ->
{:error, :invalid_characters}
{:mixed_case, false} ->
{:ok, "0x" <> hash}
{:checksummed, false} ->
{:error, :invalid_checksum}
end
end
@spec is_hex?(String.t()) :: boolean()
defp is_hex?(hash) do
case Regex.run(~r|[0-9a-f]{40}|i, hash) do
nil -> false
[_] -> true
end
end
@spec is_mixed_case?(String.t()) :: boolean()
defp is_mixed_case?(hash) do
upper_check = ~r|[0-9A-F]{40}|
lower_check = ~r|[0-9a-f]{40}|
with nil <- Regex.run(upper_check, hash),
nil <- Regex.run(lower_check, hash) do
true
else
_ -> false
end
end
@spec is_checksummed?(String.t()) :: boolean()
defp is_checksummed?(original_hash) do
lowercase_hash = String.downcase(original_hash)
sha3_hash = :keccakf1600.hash(:sha3_256, lowercase_hash)
do_checksum_check(sha3_hash, original_hash)
end
@spec do_checksum_check(binary(), String.t()) :: boolean()
defp do_checksum_check(_, ""), do: true
defp do_checksum_check(sha3_hash, address_hash) do
<<checksum_digit::integer-size(4), remaining_sha3_hash::bits>> = sha3_hash
<<current_char::binary-size(1), remaining_address_hash::binary>> = address_hash
if is_proper_case?(checksum_digit, current_char) do
do_checksum_check(remaining_sha3_hash, remaining_address_hash)
else
false
end
end
@spec is_proper_case?(integer, String.t()) :: boolean()
defp is_proper_case?(checksum_digit, character) do
case_map = %{
"0" => :both,
"1" => :both,
"2" => :both,
"3" => :both,
"4" => :both,
"5" => :both,
"6" => :both,
"7" => :both,
"8" => :both,
"9" => :both,
"a" => :lower,
"b" => :lower,
"c" => :lower,
"d" => :lower,
"e" => :lower,
"f" => :lower,
"A" => :upper,
"B" => :upper,
"C" => :upper,
"D" => :upper,
"E" => :upper,
"F" => :upper
}
character_case = Map.get(case_map, character)
# Digits with checksum digit greater than 7 should be uppercase
if checksum_digit > 7 do
character_case in ~w(both upper)a
else
character_case in ~w(both lower)a
end
end
end

@ -2,4 +2,35 @@ defmodule Explorer.Chain.Hash.AddressTest do
use ExUnit.Case, async: true
doctest Explorer.Chain.Hash.Address
alias Explorer.Chain.Hash.Address
describe "validate/1" do
test "with valid uppercase hash" do
assert Address.validate("0xC1912FEE45D61C87CC5EA59DAE31190FFFFF232D") ==
{:ok, "0xC1912FEE45D61C87CC5EA59DAE31190FFFFF232D"}
end
test "with valid lowercase hash" do
assert Address.validate("0xc1912fee45d61c87cc5ea59dae31190fffff232d") ==
{:ok, "0xc1912fee45d61c87cc5ea59dae31190fffff232d"}
end
test "with valid checksummed hash" do
assert Address.validate("0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d") ==
{:ok, "0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232d"}
end
test "with invalid checksum hash" do
assert Address.validate("0xC1912fEE45d61C87Cc5EA59DaE31190FFFFf232d") == {:error, :invalid_checksum}
end
test "with non-hex string" do
assert Address.validate("0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232H") == {:error, :invalid_characters}
end
test "with invalid length string" do
assert Address.validate("0xc1912fEE45d61C87Cc5EA59DaE31190FFFFf232") == {:error, :invalid_length}
end
end
end

Loading…
Cancel
Save