Skip to content

hb_ao.erl - AO-Core Resolution Engine

Overview

Purpose: Core message resolution and device execution engine
Module: hb_ao
Protocol: AO-Core
Pattern: Recursive message resolution with device-based computation

This module is the heart of the AO-Core protocol in HyperBEAM. It resolves messages to other messages by executing device functions, managing computation state, and maintaining cryptographic linkages between messages.

Core Concept

%% Basic Resolution
ao(BaseMessage, RequestMessage) -> {Status, ResultMessage}
 
%% Under the hood
DeviceMod:KeyFunction(Msg1, Msg2, Opts) -> {ok | error, Msg3}

Every message is a collection of keys that can be resolved via its device. The resolution process creates a cryptographic chain of computation.

Dependencies

  • HyperBEAM: hb_cache, hb_store, hb_path, hb_singleton, hb_ao_device, hb_message, hb_opts, hb_maps, hb_util
  • Includes: include/hb.hrl

Public Functions Overview

%% Core Resolution
-spec resolve(Path | Message, Opts) -> {Status, Result}.
-spec resolve(Msg1, Msg2, Opts) -> {Status, Result}.
-spec resolve_many(Messages, Opts) -> {Status, Result}.
 
%% Key/Value Operations  
-spec get(Msg, Key) -> Result.
-spec get(Msg, Key, Opts) -> Result.
-spec get_first(Msg, Keys) -> Result.
-spec set(Msg1, Msg2, Opts) -> UpdatedMsg.
-spec set(Msg, Key, Value, Opts) -> UpdatedMsg.
-spec remove(Msg, Key, Opts) -> UpdatedMsg.
 
%% Introspection
-spec keys(Msg) -> [Key].
-spec keys(Msg, Opts) -> [Key].
 
%% Normalization
-spec normalize_key(Key) -> NormalizedKey.
-spec normalize_keys(Msg) -> NormalizedMsg.
 
%% Utilities
-spec force_message(Msg, Opts) -> Message.

Resolution Phases

The resolver operates in 13 discrete phases:

1.  Normalization          % Prepare messages for execution
2.  Cache lookup           % Check if result cached
3.  Validation check       % Verify message validity
4.  Persistent resolver    % Check persistent store
5.  Device lookup          % Find device module
6.  Execution              % Run device function
7.  Step hook              % Execute step callback
8.  Subresolution          % Resolve nested messages
9.  Cryptographic linking  % Update hash path
10. Result caching         % Store result
11. Notify waiters         % Alert pending processes
12. Fork worker            % Spawn concurrent worker
13. Recurse or terminate   % Continue or return

Public Functions

1. resolve/2

-spec resolve(Path | Message, Opts) -> {Status, Result}
    when
        Path :: binary(),
        Message :: map(),
        Opts :: map(),
        Status :: ok | error,
        Result :: term().

Description: Resolve a path or message. If given a binary path, wraps it in a message with that path.

Test Code:
-module(hb_ao_resolve_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
 
resolve_path_test() ->
    Msg = #{
        <<"path">> => <<"/data">>,
        <<"data">> => <<"value">>
    },
    {ok, Result} = hb_ao:resolve(Msg, #{}),
    ?assertEqual(<<"value">>, Result).
 
resolve_binary_path_test() ->
    {ok, Result} = hb_ao:resolve(<<"/id">>, #{}),
    ?assert(is_binary(Result) orelse is_map(Result)).
 
resolve_no_path_error_test() ->
    {error, Reason} = hb_ao:resolve(#{<<"data">> => <<"test">>}, #{}),
    ?assert(is_binary(Reason)).

2. resolve/3

-spec resolve(Msg1, Msg2 | Path, Opts) -> {Status, Result}
    when
        Msg1 :: map(),
        Msg2 :: map(),
        Path :: binary(),
        Opts :: map(),
        Status :: ok | error,
        Result :: term().

Description: Resolve Msg2 against Msg1. Msg2 specifies what computation to perform on Msg1.

Test Code:
-module(hb_ao_resolve3_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
 
resolve_with_base_test() ->
    Base = #{
        <<"key1">> => <<"value1">>,
        <<"key2">> => <<"value2">>
    },
    Request = #{<<"path">> => <<"key1">>},
    
    {ok, Result} = hb_ao:resolve(Base, Request, #{}),
    ?assertEqual(<<"value1">>, Result).
 
resolve_with_path_test() ->
    Base = #{<<"data">> => <<"test">>},
    {ok, Result} = hb_ao:resolve(Base, <<"/data">>, #{}),
    ?assertEqual(<<"test">>, Result).
 
resolve_nested_test() ->
    Base = #{
        <<"outer">> => #{
            <<"inner">> => <<"value">>
        }
    },
    Request = #{<<"path">> => <<"/outer/inner">>},
    
    {ok, Result} = hb_ao:resolve(Base, Request, #{}),
    ?assertEqual(<<"value">>, Result).

3. resolve_many/2

-spec resolve_many(Messages, Opts) -> {Status, Result}
    when
        Messages :: [Message] | Message,
        Message :: map() | binary(),
        Opts :: map(),
        Status :: ok | error,
        Result :: term().

Description: Resolve a sequence of messages. Each message's output becomes input for the next.

Special Cases:
  • Single ID: Direct read from store
  • Ordered map: Converts to list automatically
  • {as, DevID, Msg}: Subresolution with specific device
Test Code:
-module(hb_ao_resolve_many_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
 
resolve_many_sequence_test() ->
    Base = #{<<"count">> => 0},
    Req1 = #{<<"path">> => <<"set">>, <<"count">> => 1},
    Req2 = #{<<"path">> => <<"set">>, <<"count">> => 2},
    
    {ok, Result} = hb_ao:resolve_many([Base, Req1, Req2], #{}),
    ?assertEqual(2, maps:get(<<"count">>, Result)).
 
resolve_many_simple_path_test() ->
    Base = #{<<"data">> => <<"value">>},
    Req = #{<<"path">> => <<"/data">>},
    
    {ok, Result} = hb_ao:resolve_many([Base, Req], #{}),
    ?assertEqual(<<"value">>, Result).

4. get/2, get/3, get/4

-spec get(Path, Msg) -> Result.
-spec get(Path, Msg, Opts) -> Result.
-spec get(Path, Msg, Default, Opts) -> Result.

Description: Get value of a key/path from message. Resolves via device if needed.

Test Code:
-module(hb_ao_get_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
 
get_simple_test() ->
    Msg = #{<<"key">> => <<"value">>},
    Result = hb_ao:get(<<"key">>, Msg),
    ?assertEqual(<<"value">>, Result).
 
get_with_default_test() ->
    Msg = #{<<"existing">> => <<"value">>},
    Result = hb_ao:get(<<"missing">>, Msg, <<"default">>, #{}),
    ?assertEqual(<<"default">>, Result).
 
get_nested_test() ->
    Msg = #{
        <<"outer">> => #{
            <<"inner">> => <<"value">>
        }
    },
    Result = hb_ao:get(<<"/outer/inner">>, Msg, #{}),
    ?assertEqual(<<"value">>, Result).
 
get_missing_key_test() ->
    Msg = #{<<"key">> => <<"value">>},
    Result = hb_ao:get(<<"nonexistent">>, Msg, undefined, #{}),
    ?assertEqual(undefined, Result).

5. set/3, set/4

-spec set(Msg1, Msg2, Opts) -> UpdatedMsg.
-spec set(Msg, Key, Value, Opts) -> UpdatedMsg.

Description: Set key(s) in message using device's set function.

Test Code:
-module(hb_ao_set_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
 
set_single_key_test() ->
    Msg = #{<<"old">> => <<"value">>},
    Updated = hb_ao:set(Msg, <<"new">>, <<"data">>, #{}),
    ?assertEqual(<<"data">>, maps:get(<<"new">>, Updated)).
 
set_multiple_keys_test() ->
    Msg = #{<<"a">> => 1},
    Updates = #{<<"b">> => 2, <<"c">> => 3},
    Updated = hb_ao:set(Msg, Updates, #{}),
    ?assertEqual(2, maps:get(<<"b">>, Updated)),
    ?assertEqual(3, maps:get(<<"c">>, Updated)).
 
set_nested_path_test() ->
    Msg = #{<<"outer">> => #{}},
    Updated = hb_ao:set(Msg, <<"/outer/inner">>, <<"value">>, #{}),
    Inner = maps:get(<<"outer">>, Updated),
    ?assertEqual(<<"value">>, maps:get(<<"inner">>, Inner)).
 
set_override_test() ->
    Msg = #{<<"key">> => <<"old">>},
    Updated = hb_ao:set(Msg, <<"key">>, <<"new">>, #{}),
    ?assertEqual(<<"new">>, maps:get(<<"key">>, Updated)).

6. remove/2, remove/3

-spec remove(Msg, Key) -> UpdatedMsg.
-spec remove(Msg, Key, Opts) -> UpdatedMsg.

Description: Remove key from message using device's remove function.

Test Code:
-module(hb_ao_remove_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
 
remove_key_test() ->
    Msg = #{<<"key">> => <<"value">>, <<"keep">> => <<"this">>},
    Updated = hb_ao:remove(Msg, <<"key">>, #{}),
    ?assertEqual(false, maps:is_key(<<"key">>, Updated)),
    ?assertEqual(<<"this">>, maps:get(<<"keep">>, Updated)).
 
remove_nonexistent_test() ->
    Msg = #{<<"key">> => <<"value">>},
    Updated = hb_ao:remove(Msg, <<"missing">>, #{}),
    ?assertEqual(Msg, Updated).

7. keys/1, keys/2, keys/3

-spec keys(Msg) -> [Key].
-spec keys(Msg, Opts) -> [Key].
-spec keys(Msg, Recurse, Opts) -> [Key].

Description: Get list of keys from message. Can optionally recurse into nested structures.

Test Code:
-module(hb_ao_keys_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
 
keys_simple_test() ->
    Msg = #{<<"a">> => 1, <<"b">> => 2, <<"c">> => 3},
    Keys = hb_ao:keys(Msg),
    ?assertEqual(3, length(Keys)),
    ?assert(lists:member(<<"a">>, Keys)),
    ?assert(lists:member(<<"b">>, Keys)),
    ?assert(lists:member(<<"c">>, Keys)).
 
keys_empty_map_test() ->
    Msg = #{},
    Keys = hb_ao:keys(Msg),
    ?assertEqual([], Keys).

8. normalize_key/1, normalize_key/2

-spec normalize_key(Key) -> NormalizedKey
    when
        Key :: binary() | atom() | integer() | list(),
        NormalizedKey :: binary().

Description: Convert any key type to normalized binary form.

Test Code:
-module(hb_ao_normalize_key_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
 
normalize_binary_test() ->
    ?assertEqual(<<"test">>, hb_ao:normalize_key(<<"test">>)).
 
normalize_atom_test() ->
    ?assertEqual(<<"test">>, hb_ao:normalize_key(test)).
 
normalize_integer_test() ->
    ?assertEqual(<<"42">>, hb_ao:normalize_key(42)).
 
normalize_string_test() ->
    ?assertEqual(<<"test">>, hb_ao:normalize_key("test")).
 
normalize_path_list_test() ->
    ?assertEqual(<<"a/b/c">>, hb_ao:normalize_key([<<"a">>, <<"b">>, <<"c">>])).

9. normalize_keys/1, normalize_keys/2

-spec normalize_keys(Msg) -> NormalizedMsg
    when
        Msg :: map() | list(),
        NormalizedMsg :: map().

Description: Normalize all keys in a message to binary form. Converts lists to numbered maps.

Test Code:
-module(hb_ao_normalize_keys_test).
-include_lib("eunit/include/eunit.hrl").
-include("include/hb.hrl").
 
normalize_keys_map_test() ->
    Msg = #{atom_key => <<"value">>, 123 => <<"number">>},
    Normalized = hb_ao:normalize_keys(Msg),
    ?assert(maps:is_key(<<"atom_key">>, Normalized)),
    ?assert(maps:is_key(<<"123">>, Normalized)).
 
normalize_keys_list_test() ->
    List = [<<"a">>, <<"b">>, <<"c">>],
    Normalized = hb_ao:normalize_keys(List),
    ?assert(is_map(Normalized)),
    ?assertEqual(<<"a">>, maps:get(<<"1">>, Normalized)),
    ?assertEqual(<<"b">>, maps:get(<<"2">>, Normalized)),
    ?assertEqual(<<"c">>, maps:get(<<"3">>, Normalized)).

Device Resolution

Device Function Lookup

%% Priority order:
1. Handler function (info/handler)
2. Exported function matching key name
3. Default handler (info/default)
4. Default device (dev_message)
 
%% Example device
-module(my_device).
-export([my_key/3, info/0]).
 
my_key(Msg1, Msg2, Opts) ->
    {ok, #{ <<"result">> => <<"computed">> }}.
 
info() ->
    #{
        exports => [my_key],
        default => dev_message
    }.

Common Patterns

%% Simple resolution
{ok, Result} = hb_ao:resolve(
    #{<<"data">> => <<"test">>},
    #{<<"path">> => <<"/data">>},
    #{}
).
 
%% Sequential resolution
Base = #{<<"count">> => 0},
Increment1 = #{<<"path">> => <<"set">>, <<"count">> => 1},
Increment2 = #{<<"path">> => <<"set">>, <<"count">> => 2},
{ok, Final} = hb_ao:resolve_many([Base, Increment1, Increment2], #{}).
 
%% Get with default
Value = hb_ao:get(<<"key">>, Msg, <<"default">>, #{}).
 
%% Set nested value
Updated = hb_ao:set(Msg, <<"/path/to/key">>, <<"value">>, #{}).
 
%% Remove multiple keys
Updated1 = hb_ao:remove(Msg, <<"key1">>, #{}),
Updated2 = hb_ao:remove(Updated1, <<"key2">>, #{}).
 
%% Get all keys
Keys = hb_ao:keys(Msg, #{}).
 
%% Normalize input
NormMsg = hb_ao:normalize_keys(RawMsg),
NormKey = hb_ao:normalize_key(atom_key).

References

  • AO-Core Protocol - docs/ao-core-protocol.md
  • Device System - hb_ao_device.erl
  • Message Format - dev_message.erl
  • Path Resolution - hb_path.erl

Notes

  1. 13 Phases: Resolution goes through distinct phases
  2. Device Lookup: Hierarchical device function resolution
  3. Caching: Results automatically cached for reuse
  4. Hash Path: Cryptographic chain of computation maintained
  5. Recursive: Handles nested message structures
  6. Concurrent: Worker processes for parallel execution
  7. Normalization: All keys converted to binaries
  8. Lists to Maps: Lists become numbered maps (1, 2, 3...)
  9. Error Handling: Returns {ok | error, Result} tuples
  10. Options: Extensive runtime configuration via Opts map