Skip to content

hb.erl - HyperBEAM Core Module

Overview

Purpose: Core configuration, initialization, and utility functions for HyperBEAM
Module: hb
Role: Primary entry point for node configuration, wallet management, and debugging

This module provides the foundational utilities for the HyperBEAM node, including system initialization, node startup configurations, wallet management, and debugging tools. It serves as the main interface for configuring and operating a HyperBEAM node.

Dependencies

  • Erlang/OTP: application, file, erlang, rand, filelib
  • HyperBEAM: hb_opts, hb_name, hb_http_server, hb_http, hb_cache, hb_store, hb_message, hb_client, hb_util, hb_maps
  • Arweave: ar_wallet
  • External: r3 (rebar3 interface)

Public Functions Overview

%% Initialization
-spec init() -> ok.
-spec now() -> integer().
-spec build() -> term().
-spec deploy_scripts() -> ok.
 
%% Node Startup
-spec start_mainnet() -> binary().
-spec start_mainnet(Port | Opts) -> binary().
-spec start_simple_pay() -> binary().
-spec start_simple_pay(Addr) -> binary().
-spec start_simple_pay(Addr, Port) -> binary().
-spec topup(Node, Amount, Recipient) -> term().
-spec topup(Node, Amount, Recipient, Wallet) -> term().
 
%% Wallet Management
-spec wallet() -> ar_wallet:wallet().
-spec wallet(Location) -> ar_wallet:wallet().
-spec address() -> binary().
 
%% Debugging
-spec read(ID) -> term().
-spec read(ID, Scope | Store) -> term().
-spec no_prod(X, Mod, Line) -> X | no_return().
-spec debug_wait(T, Mod, Func, Line) -> ok.

Public Functions

1. init/0

-spec init() -> ok.

Description: Initialize system-wide settings for the HyperBEAM node. Starts the naming service and sets the Erlang backtrace depth for debugging.

Side Effects:
  • Starts hb_name service
  • Sets backtrace_depth system flag from config
Test Code:
-module(hb_init_test).
-include_lib("eunit/include/eunit.hrl").
 
init_test() ->
    % init/0 just sets backtrace depth and starts hb_name
    ?assertEqual(ok, hb:init()),
    
    % Verify backtrace depth was set
    Depth = erlang:system_flag(backtrace_depth, 20),
    ?assert(is_integer(Depth)).

2. now/0

-spec now() -> integer().

Description: Get the current time in milliseconds since epoch. Wrapper around erlang:system_time(millisecond).

Test Code:
-module(hb_now_test).
-include_lib("eunit/include/eunit.hrl").
 
now_test() ->
    % now/0 just returns erlang:system_time(millisecond)
    Time = hb:now(),
    ?assert(is_integer(Time)),
    ?assert(Time > 0),
    
    % Verify time increases
    T1 = hb:now(),
    timer:sleep(10),
    T2 = hb:now(),
    ?assert(T2 >= T1).

3. build/0

-spec build() -> term().

Description: Hot-recompile and load the HyperBEAM environment using rebar3. Useful during development for live code reloading.

Note: This function requires rebar3 to be available and is primarily for development use.

Test Code:
-module(hb_build_test).
-include_lib("eunit/include/eunit.hrl").
 
build_test() ->
    % build/0 requires r3 module (rebar3 shell only)
    % Just verify function is exported
    ?assert(erlang:function_exported(hb, build, 0)).

4. wallet/0, wallet/1

-spec wallet() -> ar_wallet:wallet().
-spec wallet(Location) -> ar_wallet:wallet()
    when Location :: string() | binary().

Description: Load or create a wallet from disk. If the wallet file doesn't exist, creates a new keyfile at the specified location.

Behavior:
  • 0-arity: Uses priv_key_location from config
  • 1-arity: Uses specified location

Note: Internally uses wallet/2 which accepts additional options, but this is not exported.

Test Code:
-module(hb_wallet_test).
-include_lib("eunit/include/eunit.hrl").
 
wallet_test() ->
    % wallet/1 creates or loads wallet from file
    TempPath = "/tmp/test_wallet_" ++ integer_to_list(erlang:unique_integer([positive])) ++ ".json",
    Wallet = hb:wallet(TempPath),
    ?assert(is_tuple(Wallet)),
    file:delete(TempPath).
 
wallet_creates_new_test() ->
    TempPath = "/tmp/test_wallet_new_" ++ integer_to_list(erlang:unique_integer([positive])) ++ ".json",
    file:delete(TempPath),
    ?assertEqual({error, enoent}, file:read_file_info(TempPath)),
    
    Wallet = hb:wallet(TempPath),
    ?assert(is_tuple(Wallet)),
    ?assertMatch({ok, _}, file:read_file_info(TempPath)),
    
    file:delete(TempPath).
 
wallet_loads_existing_test() ->
    TempPath = "/tmp/test_wallet_exist_" ++ integer_to_list(erlang:unique_integer([positive])) ++ ".json",
    
    Wallet1 = hb:wallet(TempPath),
    Wallet2 = hb:wallet(TempPath),
    ?assertEqual(Wallet1, Wallet2),
    
    file:delete(TempPath).

5. address/0

-spec address() -> binary().

Description: Get the base64url-encoded address of the default wallet (from priv_key_location config).

Note: Internally uses address/1 which accepts a wallet tuple or location, but this is not exported.

Test Code:
-module(hb_address_test).
-include_lib("eunit/include/eunit.hrl").
 
address_test() ->
    % Test address via wallet - doesn't need mainnet
    TempPath = "/tmp/test_addr_" ++ integer_to_list(erlang:unique_integer([positive])) ++ ".json",
    Wallet = hb:wallet(TempPath),
    
    % Get address using ar_wallet (same as internal implementation)
    Addr = hb_util:encode(ar_wallet:to_address(Wallet)),
    ?assert(is_binary(Addr)),
    ?assertEqual(43, byte_size(Addr)),
    
    file:delete(TempPath).

6. read/1, read/2

-spec read(ID) -> term().
-spec read(ID, Scope | Store) -> term()
    when ID :: binary() | string(),
         Scope :: local | remote,
         Store :: term().

Description: Read a message from the cache. Used for debugging to inspect cached messages.

Parameters:
  • ID: Message identifier
  • Scope: local or remote atom to specify store scope
  • Store: Direct store tuple reference
Test Code:
-module(hb_read_test).
-include_lib("eunit/include/eunit.hrl").
 
read_integration_test() ->
    % Start mainnet server
    Port = 10000 + rand:uniform(50000),
    TempWallet = "/tmp/test_read_wallet_" ++ integer_to_list(Port) ++ ".json",
    
    URL = hb:start_mainnet(#{
        port => Port,
        priv_key_location => TempWallet
    }),
    timer:sleep(100),
    
    % Verify server is running
    ?assert(is_binary(URL)),
    
    % Create and sign a message
    Wallet = hb:wallet(TempWallet),
    Msg = hb_message:commit(#{
        <<"type">> => <<"test">>,
        <<"data">> => <<"hello world">>
    }, Wallet),
    
    % Get the message ID
    MsgID = hb_message:id(Msg),
    ?assert(is_binary(MsgID)),
    
    % Post message using hb_http
    {ok, _Response} = hb_http:post(URL, Msg, #{}),
    
    % Verify via HTTP GET
    GetURL = <<URL/binary, "/", (hb_util:encode(MsgID))/binary>>,
    {ok, {{_, GetStatus, _}, _, _}} = httpc:request(
        get,
        {binary_to_list(GetURL), []},
        [{timeout, 5000}],
        []
    ),
    ?assert(is_integer(GetStatus)),
    
    % Cleanup
    file:delete(TempWallet).

7. no_prod/3

-spec no_prod(X, Mod, Line) -> X | no_return()
    when X :: term(),
         Mod :: atom(),
         Line :: integer().

Description: Safety function to prevent non-production-ready code from running in production mode. If mode is prod, either exits or throws an error depending on exit_on_no_prod config.

Behavior:
  • In prod mode with exit_on_no_prod=true: Calls init:stop()
  • In prod mode with exit_on_no_prod=false: Throws the value X
  • In other modes: Returns X unchanged
Test Code:
-module(hb_no_prod_test).
-include_lib("eunit/include/eunit.hrl").
 
no_prod_test() ->
    % no_prod/3 just checks mode - doesn't need mainnet
    % In dev mode, returns value unchanged
    Value = {some, test, value},
    ?assertEqual(Value, hb:no_prod(Value, ?MODULE, ?LINE)),
    
    % Test with different types
    ?assertEqual(42, hb:no_prod(42, ?MODULE, ?LINE)),
    ?assertEqual(<<"binary">>, hb:no_prod(<<"binary">>, ?MODULE, ?LINE)),
    ?assertEqual([1,2,3], hb:no_prod([1,2,3], ?MODULE, ?LINE)).

8. debug_wait/4

-spec debug_wait(T, Mod, Func, Line) -> ok
    when T :: non_neg_integer(),
         Mod :: atom(),
         Func :: atom(),
         Line :: integer().

Description: Wait for a specified time while logging debug information. Useful for debugging timing-sensitive code.

Parameters:
  • T: Time to wait in milliseconds
  • Mod: Calling module name
  • Func: Calling function name
  • Line: Line number of call
Test Code:
-module(hb_debug_wait_test).
-include_lib("eunit/include/eunit.hrl").
 
debug_wait_test() ->
    % debug_wait/4 just sleeps - doesn't need mainnet
    ?assertEqual(ok, hb:debug_wait(10, ?MODULE, debug_wait_test, ?LINE)),
    
    % Verify it actually waits
    T1 = hb:now(),
    hb:debug_wait(50, ?MODULE, debug_wait_test, ?LINE),
    T2 = hb:now(),
    ?assert(T2 - T1 >= 50).

9. start_mainnet/0, start_mainnet/1

-spec start_mainnet() -> binary().
-spec start_mainnet(Port | Opts) -> binary()
    when Port :: integer(),
         Opts :: map().

Description: Start a mainnet server without payments. Initializes all required OTP applications and starts the HTTP server with filesystem-based storage.

Return Value: URL binary like <<"http://localhost:8080">>

Applications Started:
  • kernel, stdlib, inets, ssl, ranch, cowboy, gun, os_mon
Default Store:
#{<<"store-module">> => hb_store_fs, <<"name">> => <<"cache-mainnet">>}

Note: Integration tests start actual server instances on random ports.

Test Code:
-module(hb_start_mainnet_test).
-include_lib("eunit/include/eunit.hrl").
 
start_mainnet_integration_test() ->
    % Start on random port to avoid conflicts
    Port = 10000 + rand:uniform(50000),
    TempWallet = "/tmp/test_mainnet_wallet_" ++ integer_to_list(Port) ++ ".json",
    
    URL = hb:start_mainnet(#{
        port => Port,
        priv_key_location => TempWallet
    }),
    
    ?assert(is_binary(URL)),
    ?assertMatch(<<"http://localhost:", _/binary>>, URL),
    
    % Verify server is responding
    timer:sleep(100),
    {ok, {{_, StatusCode, _}, _, _}} = httpc:request(
        get,
        {binary_to_list(URL), []},
        [{timeout, 5000}],
        []
    ),
    % Any response (even 404) means server is running
    ?assert(is_integer(StatusCode)),
    
    % Cleanup
    file:delete(TempWallet).

10. start_simple_pay/0, start_simple_pay/1, start_simple_pay/2

-spec start_simple_pay() -> binary().
-spec start_simple_pay(Addr) -> binary()
    when Addr :: binary().
-spec start_simple_pay(Addr, Port) -> binary()
    when Addr :: binary(),
         Port :: integer().

Description: Start a server with simple-pay@1.0 pre-processor for payment handling. Uses P4 device for request/response processing.

Payment Configuration:
#{
    <<"device">> => <<"p4@1.0">>,
    <<"ledger-device">> => <<"simple-pay@1.0">>,
    <<"pricing-device">> => <<"simple-pay@1.0">>
}
Test Code:
-module(hb_start_simple_pay_test).
-include_lib("eunit/include/eunit.hrl").
 
start_simple_pay_integration_test() ->
    % Start on random port
    Port = 10000 + rand:uniform(50000),
    TempWallet = "/tmp/test_simplepay_wallet_" ++ integer_to_list(Port) ++ ".json",
    
    % Create wallet - we'll use a placeholder address since address/1 is not exported
    _Wallet = hb:wallet(TempWallet),
    % Use a simple test address (operator address is mainly for display)
    Addr = <<"test_operator_address">>,
    
    URL = hb:start_simple_pay(Addr, Port),
    
    ?assert(is_binary(URL)),
    ?assertMatch(<<"http://localhost:", _/binary>>, URL),
    
    % Verify server is responding
    timer:sleep(100),
    {ok, {{_, StatusCode, _}, _, _}} = httpc:request(
        get,
        {binary_to_list(URL), []},
        [{timeout, 5000}],
        []
    ),
    ?assert(is_integer(StatusCode)),
    
    % Cleanup
    file:delete(TempWallet).

11. topup/3, topup/4

-spec topup(Node, Amount, Recipient) -> term().
-spec topup(Node, Amount, Recipient, Wallet) -> term()
    when Node :: binary(),
         Amount :: integer(),
         Recipient :: binary(),
         Wallet :: ar_wallet:wallet().

Description: Helper for topping up a user's balance on a simple-pay node. Sends a signed message to the node's /~simple-pay@1.0/topup endpoint.

Message Format:
#{
    <<"path">> => <<"/~simple-pay@1.0/topup">>,
    <<"amount">> => Amount,
    <<"recipient">> => Recipient
}
Test Code:
-module(hb_topup_test).
-include_lib("eunit/include/eunit.hrl").
 
topup_integration_test() ->
    % Start a simple-pay server
    Port = 10000 + rand:uniform(50000),
    TempWallet = "/tmp/test_topup_wallet_" ++ integer_to_list(Port) ++ ".json",
    
    % Create operator wallet and get address
    OperatorWallet = hb:wallet(TempWallet),
    OperatorAddr = hb_util:human_id(ar_wallet:to_address(OperatorWallet)),
    
    % Start simple-pay with operator address - operator must match wallet
    URL = hb:start_simple_pay(OperatorAddr, Port),
    timer:sleep(100),
    
    % Create recipient wallet
    RecipientWalletPath = "/tmp/test_topup_recipient_" ++ integer_to_list(Port) ++ ".json",
    RecipientWallet = hb:wallet(RecipientWalletPath),
    RecipientAddr = hb_util:human_id(ar_wallet:to_address(RecipientWallet)),
    
    % Top up the recipient - operator can topup for free
    TopupAmount = 5000,
    {ok, NewBalance} = hb:topup(URL, TopupAmount, RecipientAddr, OperatorWallet),
    
    % Verify the new balance equals topup amount
    ?assertEqual(TopupAmount, NewBalance),
    
    % Cleanup
    file:delete(TempWallet),
    file:delete(RecipientWalletPath).

12. deploy_scripts/0

-spec deploy_scripts() -> ok.

Description: Upload all Lua scripts from the scripts/ directory to Arweave. Prints the ID of each uploaded script.

Script Message Format:
#{
    <<"data-protocol">> => <<"ao">>,
    <<"variant">> => <<"ao.N.1">>,
    <<"type">> => <<"module">>,
    <<"content-type">> => <<"application/lua">>,
    <<"name">> => FileName,
    <<"body">> => ScriptContent
}
Test Code:
-module(hb_deploy_scripts_test).
-include_lib("eunit/include/eunit.hrl").
 
deploy_scripts_test() ->
    % deploy_scripts/0 reads .lua files from scripts/ directory
    LuaFiles = filelib:wildcard("scripts/*.lua"),
    case LuaFiles of
        [] ->
            % No lua files - returns ok immediately (no network needed)
            ?assertEqual(ok, hb:deploy_scripts());
        Files ->
            % Has lua files - start mainnet for wallet, attempt deploy
            Port = 10000 + rand:uniform(50000),
            TempWallet = "/tmp/test_deploy_wallet_" ++ integer_to_list(Port) ++ ".json",
            
            _URL = hb:start_mainnet(#{
                port => Port,
                priv_key_location => TempWallet
            }),
            timer:sleep(100),
            
            % deploy_scripts returns ok after processing all files
            % (upload status is printed but doesn't affect return value)
            ?assertEqual(ok, hb:deploy_scripts()),
            
            % Verify it found the expected files
            ?assert(length(Files) > 0),
            
            file:delete(TempWallet)
    end.

Configuration Options

The hb module uses hb_opts:get/1 and hb_opts:get/3 for configuration:

OptionDescriptionDefault
debug_stack_depthErlang backtrace depth20
priv_key_locationPath to wallet keyfile-
portHTTP server port8080
modeOperating mode (dev, prod)dev
exit_on_no_prodExit on no_prod in prod modefalse
storeStore configuration-
addressNode operator address-

Common Patterns

%% Initialize the node
hb:init().
 
%% Get current timestamp
Timestamp = hb:now().
 
%% Load default wallet
Wallet = hb:wallet().
 
%% Get wallet address
Address = hb:address().
 
%% Load wallet from specific location
Wallet = hb:wallet("/path/to/wallet.json").
 
%% Read cached message
{ok, Msg} = hb:read(<<"message_id">>).
 
%% Read from specific scope
{ok, Msg} = hb:read(<<"message_id">>, remote).
 
%% Start mainnet node on default port
URL = hb:start_mainnet().
 
%% Start mainnet node on specific port
URL = hb:start_mainnet(9000).
 
%% Start simple-pay node
URL = hb:start_simple_pay().
 
%% Top up user balance
hb:topup(<<"http://localhost:8080">>, 1000, UserAddress).
 
%% Hot reload during development
hb:build().
 
%% Debug wait with logging
hb:debug_wait(1000, ?MODULE, my_function, ?LINE).
 
%% Safety check for non-prod code
Value = hb:no_prod(dangerous_value, ?MODULE, ?LINE).

Architecture

Initialization Flow

hb:init()

  ├─> hb_name:start()
  │   └─> Start naming/registry service

  └─> erlang:system_flag(backtrace_depth, N)
      └─> Configure debug stack traces

Server Startup Flow

hb:start_mainnet(Opts)

  ├─> application:ensure_all_started([...])
  │   └─> Start OTP dependencies

  ├─> hb:wallet(Location)
  │   └─> Load or create wallet

  ├─> hb_http_server:set_default_opts(Opts)
  │   └─> Configure server options

  └─> hb_http_server:start_node(FinalOpts)
      └─> Start HTTP server

Wallet Management

Key Types

The module uses ar_wallet for cryptographic operations:

% Default key type from hb.hrl
?DEFAULT_KEY_TYPE  % Typically {rsa, 65537} or {ecdsa, secp256k1}

Wallet Tuple Structure

% RSA wallet
{{rsa, 65537}, {PublicKey, PrivateKey}}
 
% ECDSA wallet  
{{ecdsa, secp256k1}, {PublicKey, PrivateKey}}

Address Encoding

Addresses are base64url-encoded SHA-256 hashes of the public key:

  • 43 characters for standard addresses
  • URL-safe encoding (no padding)

Debugging

Stack Trace Configuration

% Set via hb_opts
hb_opts:get(debug_stack_depth)  % Default: 20
 
% Applied during init
erlang:system_flag(backtrace_depth, Depth)

Debug Wait Usage

% In code requiring timing investigation
hb:debug_wait(5000, ?MODULE, problematic_function, ?LINE).
% Logs: {debug_wait, {5000, module, problematic_function, 42}}
% Then waits 5 seconds

Production Safety

% Mark non-production-ready code
Result = ?NO_PROD(experimental_feature()).
% Expands to: hb:no_prod(experimental_feature(), ?MODULE, ?LINE)

References

  • hb_opts - Configuration management
  • hb_http_server - HTTP server implementation
  • hb_cache - Message caching layer
  • hb_store - Persistent storage abstraction
  • ar_wallet - Arweave wallet operations
  • hb_name - Process naming service

Notes

  1. Initialization Order: hb:init/0 must be called before most operations
  2. Wallet Auto-Creation: Missing wallet files are automatically created
  3. Mode Safety: no_prod/3 prevents accidental prod deployment of dev code
  4. Hot Reload: build/0 enables live code updates during development
  5. Timestamp Precision: now/0 returns millisecond-precision timestamps
  6. Address Format: 43-character base64url-encoded strings
  7. Store Scopes: local and remote provide different cache views
  8. Payment Integration: simple-pay provides basic payment processing
  9. Script Deployment: Lua scripts uploaded with AO protocol metadata
  10. Port Randomization: start_simple_pay/0 uses random port (10000-60000)