Skip to content

hb_keccak.erl - Keccak & SHA3 Hashing with Ethereum Support

Overview

Purpose: Keccak-256, SHA3-256 hashing and Ethereum address generation
Module: hb_keccak
Implementation: Native C NIF (Native Implemented Function)
Use Cases: Ethereum compatibility, cryptographic hashing

This module provides Keccak-256 and SHA3-256 hashing functions through a C NIF, enabling high-performance cryptographic operations. It includes utilities for converting ECDSA secp256k1 public keys to checksummed Ethereum addresses, essential for cross-chain compatibility.

Keccak vs SHA3

Important Distinction:
  • Keccak-256: Original Keccak algorithm used by Ethereum
  • SHA3-256: NIST-standardized version with different padding

These produce different hashes for the same input. Ethereum uses Keccak-256, not SHA3-256.

Dependencies

  • NIF: C implementation in priv/hb_keccak.so
  • Erlang/OTP: crypto (for binary operations)
  • HyperBEAM: hb_util
  • Testing: eunit

NIF Initialization

-on_load(init/0).
 
init() ->
    SoName = filename:join([code:priv_dir(hb), "hb_keccak"]),
    erlang:load_nif(SoName, 0).
Load Sequence:
  1. Module loads
  2. init/0 called automatically (-on_load)
  3. Finds hb_keccak.so in priv directory
  4. Loads NIF functions

Public Functions Overview

%% Hashing
-spec keccak_256(Binary) -> Hash.
-spec sha3_256(Binary) -> Hash.
 
%% Ethereum Address Generation
-spec key_to_ethereum_address(PublicKey) -> ChecksummedAddress.

Public Functions

1. keccak_256/1

-spec keccak_256(Binary) -> Hash
    when
        Binary :: binary(),
        Hash :: binary().

Description: Compute Keccak-256 hash of input binary. This is the hash function used by Ethereum, not the NIST SHA3-256 standard.

Implementation: Native C NIF

Hash Output: 32 bytes (256 bits)

Test Code:
-module(hb_keccak_256_test).
-include_lib("eunit/include/eunit.hrl").
 
keccak_256_test() ->
    Input = <<"testing">>,
    %% hb_util:to_hex returns lowercase hex
    Expected = <<"5f16f4c7f149ac4f9510d9cf8cf384038ad348b3bcdc01915f95de12df9d1b02">>,
    Hash = hb_keccak:keccak_256(Input),
    HexHash = hb_util:to_hex(Hash),
    ?assertEqual(Expected, HexHash).
 
keccak_256_empty_test() ->
    Input = <<>>,
    Hash = hb_keccak:keccak_256(Input),
    ?assertEqual(32, byte_size(Hash)).
 
keccak_256_long_input_test() ->
    Input = binary:copy(<<"test">>, 1000),
    Hash = hb_keccak:keccak_256(Input),
    ?assertEqual(32, byte_size(Hash)).
 
keccak_256_deterministic_test() ->
    Input = <<"hello world">>,
    Hash1 = hb_keccak:keccak_256(Input),
    Hash2 = hb_keccak:keccak_256(Input),
    ?assertEqual(Hash1, Hash2).

2. sha3_256/1

-spec sha3_256(Binary) -> Hash
    when
        Binary :: binary(),
        Hash :: binary().

Description: Compute SHA3-256 hash (NIST standard) of input binary. This is different from Keccak-256 due to different padding schemes.

Implementation: Native C NIF

Hash Output: 32 bytes (256 bits)

Test Code:
-module(hb_sha3_256_test).
-include_lib("eunit/include/eunit.hrl").
 
sha3_256_test() ->
    Input = <<"testing">>,
    %% hb_util:to_hex returns lowercase hex
    Expected = <<"7f5979fb78f082e8b1c676635db8795c4ac6faba03525fb708cb5fd68fd40c5e">>,
    Hash = hb_keccak:sha3_256(Input),
    HexHash = hb_util:to_hex(Hash),
    ?assertEqual(Expected, HexHash).
 
sha3_vs_keccak_test() ->
    Input = <<"test">>,
    SHA3Hash = hb_keccak:sha3_256(Input),
    KeccakHash = hb_keccak:keccak_256(Input),
    ?assertNotEqual(SHA3Hash, KeccakHash).
 
sha3_256_nist_vector_test() ->
    % NIST test vector: "abc"
    Input = <<"abc">>,
    Expected = <<"3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532">>,
    Hash = hb_keccak:sha3_256(Input),
    HexHash = hb_util:to_hex(Hash),
    ?assertEqual(Expected, HexHash).

3. key_to_ethereum_address/1

-spec key_to_ethereum_address(PublicKey) -> ChecksummedAddress
    when
        PublicKey :: binary(),
        ChecksummedAddress :: binary().

Description: Convert ECDSA secp256k1 public key (65 bytes uncompressed) to checksummed Ethereum address (EIP-55 format).

Algorithm:
1. Remove first byte (0x04 uncompressed marker)
2. Keccak-256 hash of remaining 64 bytes
3. Take last 20 bytes (40 hex chars) of hash
4. Apply EIP-55 checksum
5. Prepend "0x"
EIP-55 Checksum:
  • Hash the lowercase hex address
  • If hash nibble ≥ 8, capitalize corresponding address character
Implementation:
key_to_ethereum_address(Key) when is_binary(Key) ->
    <<_Prefix:1/binary, NoCompressionByte/binary>> = Key,
    Prefix = hb_util:to_hex(hb_keccak:keccak_256(NoCompressionByte)),
    Last40 = binary:part(Prefix, byte_size(Prefix) - 40, 40),
    Hash = hb_keccak:keccak_256(Last40),
    HashHex = hb_util:to_hex(Hash),
    ChecksumAddress = hash_to_checksum_address(Last40, HashHex),
    ChecksumAddress.
Test Code:
-module(hb_ethereum_address_test).
-include_lib("eunit/include/eunit.hrl").
 
keccak_256_key_to_address_test() ->
    % Base64url encoded public key
    Input = <<"BAoixXds4JhW42pzlLb83B3-I21lX78j3Q7cPaoFiCjMgjYwYLDj-xL132J147ifZFwRBmzmEMC8eYAXzbRNWuA">>,
    PublicKey = hb_util:decode(Input),
    ChecksumAddress = hb_keccak:key_to_ethereum_address(PublicKey),
    ?assertEqual(<<"0xb7B4360F7F6298dE2e7a11009270F35F189Bd77E">>, ChecksumAddress).
 
address_has_checksum_test() ->
    Input = <<"BAoixXds4JhW42pzlLb83B3-I21lX78j3Q7cPaoFiCjMgjYwYLDj-xL132J147ifZFwRBmzmEMC8eYAXzbRNWuA">>,
    Address = hb_keccak:key_to_ethereum_address(hb_util:decode(Input)),
    % Check that address has mixed case (checksum present)
    Lower = binary:bin_to_list(Address),
    Upper = string:to_upper(Lower),
    ?assertNotEqual(Lower, Upper).  % Has mixed case
 
ethereum_address_format_test() ->
    Input = <<"BAoixXds4JhW42pzlLb83B3-I21lX78j3Q7cPaoFiCjMgjYwYLDj-xL132J147ifZFwRBmzmEMC8eYAXzbRNWuA">>,
    Address = hb_keccak:key_to_ethereum_address(hb_util:decode(Input)),
    % Check format: 0x + 40 hex chars
    ?assertEqual(<<"0x">>, binary:part(Address, 0, 2)),
    ?assertEqual(42, byte_size(Address)).

Helper Functions

to_hex/1 (Private)

-spec to_hex(Binary) -> HexBinary
    when
        Binary :: binary(),
        HexBinary :: binary().

Description: Convert binary to uppercase hexadecimal representation.

Implementation:
to_hex(Bin) when is_binary(Bin) ->
    binary:encode_hex(Bin).

hash_to_checksum_address/2 (Private)

-spec hash_to_checksum_address(Address, Hash) -> ChecksummedAddress
    when
        Address :: binary(),
        Hash :: binary(),
        ChecksummedAddress :: binary().

Description: Apply EIP-55 checksum to Ethereum address (40 hex characters).

Algorithm:
For each character in address:
    If corresponding hash character'8':
        Uppercase address character
    Else:
        Keep lowercase
Prepend "0x"
Implementation:
hash_to_checksum_address(Last40, Hash) when
    is_binary(Last40),
    is_binary(Hash),
    byte_size(Last40) =:= 40 ->
    
    Checksummed = lists:zip(
        binary:bin_to_list(Last40),
        binary:bin_to_list(binary:part(Hash, 0, 40))
    ),
    Formatted = lists:map(
        fun({Char, H}) ->
            case H >= $8 of
                true -> string:to_upper([Char]);
                false -> [Char]
            end
        end,
        Checksummed
    ),
    <<"0x", (list_to_binary(lists:append(Formatted)))/binary>>.

Note: This is a private helper function. Test coverage is provided through key_to_ethereum_address/1 tests.


Ethereum Address Generation Details

Input Format

ECDSA secp256k1 Uncompressed Public Key:
  • Size: 65 bytes
  • Format: 0x04 || X || Y
  • 0x04: Uncompressed point marker
  • X: 32-byte x-coordinate
  • Y: 32-byte y-coordinate

Step-by-Step Process

% 1. Input: 65-byte public key
PublicKey = <<4, X:32/binary, Y:32/binary>>
 
% 2. Remove compression marker
<<_Prefix:1/binary, Coordinates/binary>> = PublicKey
% Coordinates = X || Y (64 bytes)
 
% 3. Hash coordinates
HashFull = keccak_256(Coordinates)
HashHex = to_hex(HashFull)
% HashHex = 64 hex characters
 
% 4. Take last 20 bytes (40 hex chars)
Address = binary:part(HashHex, byte_size(HashHex) - 40, 40)
% Address = "b7b4360f7f6298de2e7a11009270f35f189bd77e"
 
% 5. Apply EIP-55 checksum
AddressHash = keccak_256(Address)
AddressHashHex = to_hex(AddressHash)
 
Checksummed = apply_checksum(Address, AddressHashHex)
% Checksummed = "0xb7B4360F7F6298dE2e7a11009270F35F189Bd77E"

EIP-55 Checksum Algorithm

Specification

EIP-55: Mixed-case checksum encoding for Ethereum addresses

Purpose: Detect typos in addresses without requiring additional data

Algorithm:
  1. Take lowercase hex address (40 chars, no 0x prefix)
  2. Compute Keccak-256 hash of lowercase address
  3. For each hex digit in address:
    • If corresponding hash nibble ≥ 8: uppercase
    • Else: lowercase
  4. Prepend "0x"

Example

% Input address
Address = "b7b4360f7f6298de2e7a11009270f35f189bd77e"
 
% Hash of address
Hash = keccak_256("b7b4360f7f6298de2e7a11009270f35f189bd77e")
HashHex = "a1b2c3d4e5f6..." (64 chars)
 
% Character-by-character comparison
Address[0] = 'b', Hash[0] = 'a' (< '8') → lowercase 'b'
Address[1] = '7', Hash[1] = '1' (< '8') → lowercase '7'
Address[2] = 'b', Hash[2] = 'b' (≥ '8') → uppercase 'B'
Address[3] = '4', Hash[3] = '2' (< '8') → lowercase '4'
...
 
% Result
"0xb7B4360F7F6298dE2e7a11009270F35F189Bd77E"

Common Patterns

%% Hash data with Keccak-256 (Ethereum-compatible)
Data = <<"transaction data">>,
Hash = hb_keccak:keccak_256(Data),
HexHash = hb_util:to_hex(Hash).
 
%% Hash with SHA3-256 (NIST standard)
Data = <<"sensitive data">>,
Hash = hb_keccak:sha3_256(Data),
HexHash = hb_util:to_hex(Hash).
 
%% Generate Ethereum address from public key
% Public key from ECDSA secp256k1 keypair
{_PrivKey, PubKey} = crypto:generate_key(ecdh, secp256k1),
% PubKey is 65 bytes: <<4, X:32/binary, Y:32/binary>>
EthAddress = hb_keccak:key_to_ethereum_address(PubKey).
 
%% Verify address checksum
Address = <<"0xb7B4360F7F6298dE2e7a11009270F35F189Bd77E">>,
% Remove 0x prefix and convert to lowercase
<<"0x", AddrHex/binary>> = Address,
Lower = string:to_lower(binary_to_list(AddrHex)),
% Recompute checksum
Hash = hb_keccak:keccak_256(list_to_binary(Lower)),
HashHex = hb_util:to_hex(Hash),
Recomputed = hash_to_checksum_address(list_to_binary(Lower), HashHex),
Valid = (Address =:= Recomputed).
 
%% Convert Arweave key to Ethereum address
ArweaveKey = <<"base64url-encoded-key">>,
DecodedKey = hb_util:decode(ArweaveKey),
EthAddress = hb_keccak:key_to_ethereum_address(DecodedKey).

Performance Characteristics

NIF Implementation

Advantages:
  • Speed: Native C performance
  • Efficiency: No copying between Erlang/C for large inputs
  • Standard: Uses proven cryptographic libraries
Benchmarks:
  • Small inputs (<1KB): ~1-2 microseconds
  • Medium inputs (1KB-10KB): ~5-20 microseconds
  • Large inputs (>10KB): Linear with input size

Cross-Chain Compatibility

Ethereum Address Support

% Use case: Enable Ethereum wallet support in HyperBEAM
% User provides Ethereum address or derives from ECDSA key
 
% Option 1: User has ECDSA secp256k1 key
ECDSAKey = get_user_key(),
EthAddress = hb_keccak:key_to_ethereum_address(ECDSAKey).
 
% Option 2: User provides existing Ethereum address
% Verify checksum before use
ProvidedAddress = <<"0xb7B4360F7F6298dE2e7a11009270F35F189Bd77E">>,
case verify_checksum(ProvidedAddress) of
    true -> use_address(ProvidedAddress);
    false -> {error, invalid_checksum}
end.

Error Handling

NIF Not Loaded

% If NIF fails to load, stub functions raise error
try
    Hash = hb_keccak:keccak_256(Data)
catch
    error:not_loaded ->
        % NIF not loaded, check priv directory
        io:format("ERROR: hb_keccak NIF not loaded~n"),
        {error, nif_not_loaded}
end.

Invalid Key Format

% Public key must be 65 bytes
InvalidKey = <<1, 2, 3>>,
try
    Address = hb_keccak:key_to_ethereum_address(InvalidKey)
catch
    error:{badmatch, _} ->
        {error, invalid_key_size}
end.

Testing Best Practices

%% Test against known vectors
test_known_vector() ->
    % From Ethereum test suite
    Input = <<"">>,
    ExpectedHash = <<"C5D2460186F7233C927E7DB2DCC703C0E500B653CA82273B7BFAD8045D85A470">>,
    Hash = hb_keccak:keccak_256(Input),
    ?assertEqual(ExpectedHash, hb_util:to_hex(Hash)).
 
%% Test checksum validation
test_checksum_valid() ->
    % Valid checksummed address
    Valid = <<"0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed">>,
    ?assert(is_valid_ethereum_address(Valid)).
 
%% Test roundtrip
test_key_to_address_roundtrip() ->
    % Generate key
    {_Priv, Pub} = crypto:generate_key(ecdh, secp256k1),
    % Convert to address
    Addr1 = hb_keccak:key_to_ethereum_address(Pub),
    % Verify deterministic
    Addr2 = hb_keccak:key_to_ethereum_address(Pub),
    ?assertEqual(Addr1, Addr2).

Use Cases in HyperBEAM

1. Ethereum Bridge

% Generate Ethereum-compatible signature address
UserKey = get_user_ecdsa_key(),
EthAddress = hb_keccak:key_to_ethereum_address(UserKey),
% Use EthAddress for cross-chain operations

2. Message Integrity

% Hash message for integrity verification
Message = encode_message(Data),
Hash = hb_keccak:keccak_256(Message),
SignedMsg = sign(Hash, PrivateKey),
% Verify later with same hash

3. ECDSA Support

% Support ECDSA secp256k1 alongside RSA-4096
case SignatureType of
    {ecdsa, 256} ->
        Address = hb_keccak:key_to_ethereum_address(PublicKey),
        verify_ecdsa(Message, Signature, Address);
    {rsa, 65537} ->
        verify_rsa(Message, Signature, PublicKey)
end.

References


Notes

  1. NIF Required: Module depends on compiled C library
  2. Keccak vs SHA3: Different algorithms, different results
  3. Ethereum Uses Keccak: Not SHA3-256
  4. EIP-55 Checksum: Prevents address typos
  5. 65-Byte Keys: Uncompressed ECDSA secp256k1 format
  6. Performance: Native C implementation for speed
  7. Thread-Safe: NIFs can be called from multiple processes
  8. Load-Time Init: NIF loaded automatically on module load
  9. Error Handling: Stub functions raise not_loaded error
  10. Hex Encoding: Uses uppercase for consistency
  11. Address Format: Always includes "0x" prefix
  12. Deterministic: Same input always produces same output
  13. Cross-Chain: Enables Ethereum compatibility
  14. Standard Compliant: Follows EIP-55 specification
  15. Test Vectors: Verified against Ethereum test suite