Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d32ee9f
wip: 1.0
samcamwilliams Jun 25, 2026
d309978
wip: `~message@1.0`
samcamwilliams Jun 26, 2026
8b27fcd
wip: `hb_types`
samcamwilliams Jun 26, 2026
dab9c53
wip: `hb_ao:raw` varied resolver impl
samcamwilliams Jun 27, 2026
3262130
wip: allow device forcing without setting `Base/device` in `hb_ao:raw`
samcamwilliams Jun 27, 2026
52ccf7f
wip: AO-Core 1.0 spec draft-3
samcamwilliams Jun 28, 2026
d24e60d
impr: `hb_types:add_schema` to execution context
samcamwilliams Jun 30, 2026
61e0180
fix: message-to-device key resolver flow
samcamwilliams Jun 30, 2026
ef0c0a1
impr: add primitive debugging macro
samcamwilliams Jun 30, 2026
e108ef7
chore: add math test device with specs
samcamwilliams Jun 30, 2026
d8fafa8
wip: Cache device schema on load
samcamwilliams Jun 30, 2026
75b68d0
wip: schema loading
samcamwilliams Jul 1, 2026
e6411df
wip: resolver impl
samcamwilliams Jul 1, 2026
40d9def
wip: resolver flow
samcamwilliams Jul 1, 2026
264d32b
wip: resolver
samcamwilliams Jul 1, 2026
7262ba7
wip: resolution caching
samcamwilliams Jul 1, 2026
aaec66c
wip: AO-Core 1.0 resolver implementation
samcamwilliams Jul 2, 2026
2e762a6
wip: AO-Core 1.0 resolver re-org
samcamwilliams Jul 8, 2026
56cc23d
wip: resolver
samcamwilliams Jul 9, 2026
bee48c1
wip: add persistent resolver and cache lookup to resolver
samcamwilliams Jul 9, 2026
013db25
wip: v0 draft of the new AO-Core resolver logic
samcamwilliams Jul 9, 2026
7e0aaff
wip: More AO-Core 1.0 resolver work
samcamwilliams Jul 10, 2026
f1352d6
impr: AO-Core 1.0 spec DRAFT-4
samcamwilliams Jul 11, 2026
1bd811f
impr: implements DRAFT-4 message-key precedence ordering
samcamwilliams Jul 11, 2026
c9a7069
fix: smaller resolver draft bugs
samcamwilliams Jul 11, 2026
14a5c07
wip: more AO-Core 1.0 resolver work. ~50% of `hb_ao_test_vectors` pass.
samcamwilliams Jul 12, 2026
986ed07
wip: Add AO-Core 1.0 hashpath parser and basic challenge logic
samcamwilliams Jul 12, 2026
e7b36fb
wip: allow selective part verification
samcamwilliams Jul 12, 2026
7c7dbb7
wip: hashpath generation ordering, small fixes
samcamwilliams Jul 13, 2026
65d1c90
wip: add execution context component derivation
samcamwilliams Jul 17, 2026
8644ae2
wip: Spec `DRAFT-5` edits, to `Vary` section
samcamwilliams Jul 21, 2026
8468877
wip: spec DRAFT-5
samcamwilliams Jul 21, 2026
b8a4e58
wip: more spec edits
samcamwilliams Jul 21, 2026
ba0aa3d
wip: DRAFT-5 copy errors
samcamwilliams Jul 21, 2026
e211569
wip: DRAFT-5 `no-content` | `not-found` semantics
samcamwilliams Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
549 changes: 549 additions & 0 deletions AO-CORE.md

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions erlang_ls.config
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ diagnostics:
apps_dirs:
- "src"
- "src/*"
include_dirs:
- "src/include"
include_dirs:
- "src"
- "src/include"
Expand Down
170 changes: 135 additions & 35 deletions src/core/device/hb_device.erl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
%%% Offers services for loading, verifying executability, and extracting Erlang
%%% functions from a device.
-module(hb_device).
-export([truncate_args/2, message_to_fun/3, message_to_device/2]).
-export([truncate_args/2, add_resolver/2, message_to_fun/3, message_to_fun/4, module/2]).
-export([id_or_direct_key/3]).
-export([is_direct_key_access/3, is_direct_key_access/4]).
-export([find_exported_function/5, is_exported/4, info/2, info/3]).
-include("include/hb.hrl").
Expand All @@ -29,6 +30,52 @@ truncate_args(Fun, Args) ->
{arity, Arity} = erlang:fun_info(Fun, arity),
lists:sublist(Args, Arity).

%% @doc Generates an execution context for a given key and device pair.
%% Returns `{ok, ExecCtx}`, where `ExecCtx` is a message containing:
%% #{
%% base: The base message, unvaried.
%% key: The key to be resolved in the execution.
%% forced-device: The device derived from the message itself.
%% priv/exec-device: The device from which the execution function originates.
%% If the `forced-device` resolvers from another device,
%% this key will differ from the `forced-device`.
%% priv/function: The function to execute to resolve the `forced-device`.
%% priv/add-key: Whether the execution function expects that we should
%% add the `key` as an additional argument to the start of
%% the argument list.
%% }
add_resolver(Context = #{ <<"base">> := Base, <<"key">> := Key }, Opts) ->
% TODO: What if we force a device _and_ have a direct key hit?
DevRes =
case maps:find(<<"forced-device">>, Context) of
{ok, ForcedBaseDevice} -> {ok, ForcedBaseDevice};
error -> id_or_direct_key(Base, Key, Opts)
end,
OldPriv = maps:get(<<"priv">>, Context, #{}),
case DevRes of
{hit, Value} ->
{ok,
Context#{
<<"result">> => Value
}
};
{ok, DeviceID} ->
{Type, ExecDev, ExecMod, Fun} =
message_to_fun(DeviceID, Base, Key, Opts),
{ok,
Context#{
<<"forced-device">> => DeviceID,
<<"resolver-device">> => ExecDev,
<<"priv">> =>
OldPriv#{
<<"resolver-module">> => ExecMod,
<<"add-key">> => Type == add_key,
<<"function">> => Fun
}
}
}
end.

%% @doc Calculate the Erlang function that should be called to get a value for
%% a given key from a device.
%%
Expand All @@ -54,16 +101,33 @@ truncate_args(Fun, Args) ->
%% Returns {ok | add_key, Fun} where Fun is the function to call, and add_key
%% indicates that the key should be added to the start of the call's arguments.
message_to_fun(Msg, Key, Opts) ->
% Get the device module from the message and recurse.
message_to_fun(message_to_device(Msg, Opts), Msg, Key, Opts).
message_to_fun(Dev, Msg, Key, Opts) ->
Info = info(Dev, Msg, Opts),
DeviceID = id_or_direct_key(Msg, Key, Opts),
message_to_fun(DeviceID, Msg, Key, Opts).
message_to_fun(DevID, Msg, Key, Opts) ->
message_to_fun(DevID, module(DevID, Opts), Msg, Key, Opts).
message_to_fun(DevID, DevMsg = #{ <<"device">> := _ }, _Msg, Key, _Opts) ->
% A message-valued device: execution is itself resolution. The request is
% resolved against the device message extended over the outermost state:
% `execute(Dev, Outer, Request) = resolve(set(Outer, Dev), Request)'.
% The key is forced upon the inner resolution: for forced-key executions
% (`vary', `set', ...) the request's own path differs from the key that
% this function was extracted to compute.
{ok,
DevID,
DevMsg,
fun(Base, Req, ExecOpts) ->
hb_ao:raw(undefined, Key, DevMsg#{ <<"...">> => Base }, Req, ExecOpts)
end
};
message_to_fun(DevID, DevMod, Msg, Key, Opts) ->
Info = info(DevMod, Msg, Opts),
% Is the key exported by the device?
Exported = is_exported(Info, Key, Opts),
?event(
ao_devices,
{message_to_fun,
{dev, Dev},
{device, DevID},
{module, DevMod},
{key, Key},
{is_exported, Exported},
{opts, Opts}
Expand All @@ -76,48 +140,48 @@ message_to_fun(Dev, Msg, Key, Opts) ->
% Case 2: The device has an explicit handler function.
?event(
ao_devices,
{handler_found, {dev, Dev}, {key, Key}, {handler, Handler}}
{handler_found, {dev, DevID}, {key, Key}, {handler, Handler}}
),
{Status, Func} = info_handler_to_fun(Handler, Msg, Key, Opts),
{Status, Dev, Func};
info_handler_to_fun(Handler, DevID, DevMod, Msg, Key, Opts);
_ ->
?event_debug(ao_devices, {no_override_handler, {dev, Dev}, {key, Key}}),
case {find_exported_function(Msg, Dev, Key, 3, 1, Opts), Exported} of
?event_debug(ao_devices, {no_override_handler, {dev, DevID}, {key, Key}}),
case {find_exported_function(Msg, DevMod, Key, 3, 1, Opts), Exported} of
{{ok, Func}, true} ->
% Case 3: The device has a function of the name `Key'.
{ok, Dev, Func};
{ok, DevID, DevMod, Func};
_ ->
case {hb_maps:find(default, Info, Opts), Exported} of
{{ok, DefaultFunc}, true} when is_function(DefaultFunc) ->
% Case 4: The device has a default handler.
?event_debug({found_default_handler, {func, DefaultFunc}}),
{add_key, Dev, DefaultFunc};
{add_key, DevID, DevMod, DefaultFunc};
{{ok, DefaultDevice}, true} when is_binary(DefaultDevice)
orelse is_atom(DefaultDevice) ->
% Case 5: The device gives a specific further device
% to default to. Recurse with it and apply the same
% rules.
?event_debug({found_default_device, {mod, DefaultDevice}}),
message_to_fun(
Msg#{ <<"device">> => DefaultDevice },
DefaultDevice,
hb_ao:as(DefaultDevice, Msg, Opts),
Key,
Opts
);
_ ->
% Case 6: The device has no default handler.
% We retry with the default unless the message
% already names it (loop guard).
case hb_maps:get(<<"device">>, Msg, undefined, Opts) of
?DEFAULT_DEVICE ->
throw({
error,
default_device_could_not_resolve_key,
{key, Key}
});
case DevID of
?DEFAULT_DEVICE ->
throw({
error,
default_device_could_not_resolve_key,
{key, Key}
});
_ ->
?event_debug({using_default_device, ?DEFAULT_DEVICE}),
message_to_fun(
Msg#{ <<"device">> => ?DEFAULT_DEVICE },
?DEFAULT_DEVICE,
hb_ao:as(?DEFAULT_DEVICE, Msg, Opts),
Key,
Opts
)
Expand All @@ -130,31 +194,60 @@ message_to_fun(Dev, Msg, Key, Opts) ->
%% message has no `<<"device">>' key, we resolve the default
%% (`message@1.0') just like any other device: There is no privileged
%% internal module-loading path.
message_to_device(Msg, Opts) ->
DevID = hb_maps:get(<<"device">>, Msg, ?DEFAULT_DEVICE, Opts),
module(DevMod, _Opts) when is_atom(DevMod) -> DevMod;
module(Dev, _Opts) when is_map(Dev) -> Dev;
module(DevID, Opts) ->
case hb_device_load:reference(DevID, Opts) of
{error, Reason} -> throw({error, {device_not_loadable, DevID, Reason}});
{ok, DevMod} -> DevMod
end.

%% @doc Return the device ID from a message, resolving through any ancestors
%% as necessary.
id_or_direct_key(_, <<"priv", _/binary>>, _) ->
{error, <<"`priv*` keys not resolvable.">>};
id_or_direct_key(Msg, Key, Opts) ->
do_id_or_direct_key(Msg, Key, Opts).
do_id_or_direct_key(List, _, _) when is_list(List) ->
{ok, <<"message@1.0">>};
do_id_or_direct_key(Msg, Key, Opts) ->
?event({finding_device_id, Msg}),
case hb_maps:find(Key, Msg, Opts) of
{ok, <<"unset">>} -> {error, not_found};
{ok, Value} -> {hit, Value};
error ->
case hb_maps:find(<<"device">>, Msg, Opts) of
{ok, Device} -> {ok, Device};
error ->
case hb_maps:find(<<"...">>, Msg, Opts) of
{ok, Ancestor} -> do_id_or_direct_key(Ancestor, Key, Opts);
error -> {ok, ?DEFAULT_DEVICE}
end
end
end.

%% @doc Parse a handler key given by a device's `info'.
info_handler_to_fun(Handler, _Msg, _Key, _Opts) when is_function(Handler) ->
{add_key, Handler};
info_handler_to_fun(HandlerMap, Msg, Key, Opts) ->
info_handler_to_fun(Handler, DevID, DevMod, _Msg, _Key, _Opts)
when is_function(Handler) ->
{add_key, DevID, DevMod, Handler};
info_handler_to_fun(HandlerMap, DevID, DevMod, Msg, Key, Opts) ->
case hb_maps:find(excludes, HandlerMap, Opts) of
{ok, Exclude} ->
case lists:member(Key, Exclude) of
true ->
MsgWithoutDevice =
hb_maps:without([<<"device">>], Msg, Opts),
true ->
MsgWithoutDevice =
hb_maps:without([<<"device">>], Msg, Opts),
message_to_fun(
MsgWithoutDevice#{ <<"device">> => ?DEFAULT_DEVICE },
?DEFAULT_DEVICE,
hb_ao:as(?DEFAULT_DEVICE, MsgWithoutDevice, Opts),
Key,
Opts
);
false -> {add_key, hb_maps:get(func, HandlerMap, undefined, Opts)}
false ->
{add_key, DevID, DevMod, hb_maps:get(func, HandlerMap, undefined, Opts)}
end;
error -> {add_key, hb_maps:get(func, HandlerMap, undefined, Opts)}
error ->
{add_key, DevID, DevMod, hb_maps:get(func, HandlerMap, undefined, Opts)}
end.

%% @doc Find the function with the highest arity that has the given name, if it
Expand Down Expand Up @@ -240,7 +333,14 @@ maybe_normalize_device_key(Key, Mode) ->

%% @doc Get the info map for a device, optionally giving it a message if the
%% device's info function is parameterized by one.
info(Msg, Opts) -> info(message_to_device(Msg, Opts), Msg, Opts).
info(Msg, Opts) ->
Device =
case id_or_direct_key(Msg, <<"device">>, Opts) of
{hit, Dev} -> Dev;
{ok, Dev} -> Dev;
{error, _} -> ?DEFAULT_DEVICE % `device => unset' masks: default.
end,
info(module(Device, Opts), Msg, Opts).
info(DevMod, Msg, Opts) ->
case find_exported_function(Msg, DevMod, info, 2, Opts) of
{ok, Fun} -> apply(Fun, truncate_args(Fun, [Msg, Opts]));
Expand Down
Loading