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).- Module loads
init/0called automatically (-on_load)- Finds
hb_keccak.soinprivdirectory - 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"- Hash the lowercase hex address
- If hash nibble ≥ 8, capitalize corresponding address character
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.-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"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:- Take lowercase hex address (40 chars, no 0x prefix)
- Compute Keccak-256 hash of lowercase address
- For each hex digit in address:
- If corresponding hash nibble ≥ 8: uppercase
- Else: lowercase
- 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
- 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 operations2. 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 hash3. 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
- Keccak: Keccak Hash Function
- SHA3: NIST FIPS 202
- EIP-55: Mixed-case checksum address encoding
- Ethereum Addresses: Ethereum Yellow Paper
Notes
- NIF Required: Module depends on compiled C library
- Keccak vs SHA3: Different algorithms, different results
- Ethereum Uses Keccak: Not SHA3-256
- EIP-55 Checksum: Prevents address typos
- 65-Byte Keys: Uncompressed ECDSA secp256k1 format
- Performance: Native C implementation for speed
- Thread-Safe: NIFs can be called from multiple processes
- Load-Time Init: NIF loaded automatically on module load
- Error Handling: Stub functions raise
not_loadederror - Hex Encoding: Uses uppercase for consistency
- Address Format: Always includes "0x" prefix
- Deterministic: Same input always produces same output
- Cross-Chain: Enables Ethereum compatibility
- Standard Compliant: Follows EIP-55 specification
- Test Vectors: Verified against Ethereum test suite