Skip to content

Commit e4ee1fb

Browse files
Merge pull request #228 from permaweb/feat/name-devices
feat: impl persistent name lookup and storage
2 parents 6e6db2b + ac37ff2 commit e4ee1fb

File tree

5 files changed

+358
-2
lines changed

5 files changed

+358
-2
lines changed

src/dev_local_name.erl

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
%%% @doc A device for registering and looking up local names. This device uses
2+
%%% the node message to store a local cache of its known names, and the typical
3+
%%% non-volatile storage of the node message to store the names long-term.
4+
-module(dev_local_name).
5+
-export([lookup/3, register/3]).
6+
-include("include/hb.hrl").
7+
-include_lib("eunit/include/eunit.hrl").
8+
9+
%%% The location that the device should use in the store for its links.
10+
-define(DEV_CACHE, <<"local-name@1.0">>).
11+
12+
%% @doc Takes a `key` argument and returns the value of the name, if it exists.
13+
lookup(_, Req, Opts) ->
14+
Key = hb_ao:get(<<"key">>, Req, no_key_specified, Opts),
15+
?event(local_name, {lookup, Key}),
16+
hb_ao:resolve(
17+
find_names(Opts),
18+
Key,
19+
Opts
20+
).
21+
22+
%% @doc Takes a `key' and `value' argument and registers the name. The caller
23+
%% must be the node operator in order to register a name.
24+
register(_, Req, Opts) ->
25+
case dev_meta:is_operator(Req, Opts) of
26+
false ->
27+
{error,
28+
#{
29+
<<"status">> => 403,
30+
<<"message">> => <<"Unauthorized.">>
31+
}
32+
};
33+
true ->
34+
case hb_cache:write(hb_ao:get(<<"value">>, Req, Opts), Opts) of
35+
{ok, MsgPath} ->
36+
hb_cache:link(
37+
MsgPath,
38+
LinkPath =
39+
[
40+
?DEV_CACHE,
41+
Name = hb_ao:get(<<"key">>, Req, Opts)
42+
],
43+
Opts
44+
),
45+
load_names(Opts),
46+
?event(
47+
local_name,
48+
{registered,
49+
Name,
50+
{link, LinkPath},
51+
{msg, MsgPath}
52+
}
53+
),
54+
{ok, <<"Registered.">>};
55+
{error, _} ->
56+
not_found
57+
end
58+
end.
59+
60+
%% @doc Returns a message containing all known names.
61+
find_names(Opts) ->
62+
case hb_opts:get(local_names, not_found, Opts#{ only => local }) of
63+
not_found ->
64+
find_names(load_names(Opts));
65+
LocalNames ->
66+
LocalNames
67+
end.
68+
69+
%% @doc Loads all known names from the cache and returns the new `node message'
70+
%% with those names loaded into it.
71+
load_names(Opts) ->
72+
LocalNames =
73+
maps:from_list(lists:map(
74+
fun(Key) ->
75+
?event(local_name, {loading, Key}),
76+
case hb_cache:read([?DEV_CACHE, Key], Opts) of
77+
{ok, Value} ->
78+
{Key, Value};
79+
{error, _} ->
80+
{Key, not_found}
81+
end
82+
end,
83+
hb_cache:list(?DEV_CACHE, Opts)
84+
)),
85+
?event(local_name, {found_cache_keys, LocalNames}),
86+
update_names(LocalNames, Opts).
87+
88+
%% @doc Updates the node message with the new names. Further HTTP requests will
89+
%% use this new message, removing the need to look up the names from non-volatile
90+
%% storage.
91+
update_names(LocalNames, Opts) ->
92+
hb_http_server:set_opts(NewOpts = Opts#{ local_names => LocalNames }),
93+
NewOpts.
94+
95+
%%% Tests
96+
97+
generate_test_opts() ->
98+
Opts = #{
99+
store =>
100+
[
101+
#{
102+
<<"store-module">> => hb_store_fs,
103+
<<"prefix">> => "cache-TEST/"
104+
}
105+
],
106+
priv_wallet => hb:wallet()
107+
},
108+
Opts.
109+
110+
no_names_test() ->
111+
?assertEqual(
112+
{error, not_found},
113+
lookup(#{}, #{ <<"key">> => <<"name1">> }, #{})
114+
).
115+
116+
lookup_opts_name_test() ->
117+
?assertEqual(
118+
{ok, <<"value1">>},
119+
lookup(
120+
#{},
121+
#{ <<"key">> => <<"name1">> },
122+
#{ local_names => #{ <<"name1">> => <<"value1">>} }
123+
)
124+
).
125+
126+
register_test() ->
127+
TestName = <<"TEST-", (integer_to_binary(os:system_time(millisecond)))/binary>>,
128+
Value = <<"TEST-VALUE-", (integer_to_binary(os:system_time(millisecond)))/binary>>,
129+
Opts = generate_test_opts(),
130+
?assertEqual(
131+
{ok, <<"Registered.">>},
132+
register(
133+
#{},
134+
hb_message:commit(
135+
#{ <<"key">> => TestName, <<"value">> => Value },
136+
Opts
137+
),
138+
Opts
139+
)
140+
),
141+
?assertEqual(
142+
{ok, Value},
143+
lookup(#{}, #{ <<"key">> => TestName, <<"load">> => false }, Opts)
144+
).
145+
146+
unauthorized_test() ->
147+
Opts = generate_test_opts(),
148+
?assertEqual(
149+
{error, #{ <<"status">> => 403, <<"message">> => <<"Unauthorized.">> }},
150+
register(
151+
#{},
152+
hb_message:commit(
153+
#{ <<"key">> => <<"name1">>, <<"value">> => <<"value1">> },
154+
Opts#{ priv_wallet => ar_wallet:new() }
155+
),
156+
Opts
157+
)
158+
).
159+
160+
http_test() ->
161+
Opts = generate_test_opts(),
162+
Node = hb_http_server:start_node(Opts),
163+
hb_http:post(
164+
Node,
165+
<<"/~local-name@1.0/register">>,
166+
hb_message:commit(
167+
#{ <<"key">> => <<"name1">>, <<"value">> => <<"value1">> },
168+
Opts
169+
),
170+
Opts
171+
),
172+
?assertEqual(
173+
{ok, <<"value1">>},
174+
hb_http:get(
175+
Node,
176+
<<"/~local-name@1.0/lookup?key=name1">>,
177+
Opts
178+
)
179+
).

src/dev_meta.erl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
%%% the AO-Core resolver has returned a result.
99
-module(dev_meta).
1010
-export([info/1, info/3, handle/2, adopt_node_message/2]).
11+
%%% Public API
12+
-export([is_operator/2]).
1113
-include("include/hb.hrl").
1214
-include_lib("eunit/include/eunit.hrl").
1315

@@ -22,6 +24,26 @@
2224
%% function, we will need to find a different approach.
2325
info(_) -> #{ exports => [info] }.
2426

27+
%% @doc Utility function for determining if a request is from the `operator' of
28+
%% the node.
29+
is_operator(Request, NodeMsg) ->
30+
RequestSigners = hb_message:signers(Request),
31+
Operator =
32+
hb_opts:get(
33+
operator,
34+
case hb_opts:get(priv_wallet, no_viable_wallet, NodeMsg) of
35+
no_viable_wallet -> unclaimed;
36+
Wallet -> ar_wallet:to_address(Wallet)
37+
end,
38+
NodeMsg
39+
),
40+
EncOperator =
41+
case Operator of
42+
unclaimed -> unclaimed;
43+
NativeAddress -> hb_util:human_id(NativeAddress)
44+
end,
45+
EncOperator == unclaimed orelse lists:member(EncOperator, RequestSigners).
46+
2547
%% @doc Normalize and route messages downstream based on their path. Messages
2648
%% with a `Meta' key are routed to the `handle_meta/2' function, while all
2749
%% other messages are routed to the `handle_resolve/2' function.

src/dev_name.erl

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
%%% @doc A device for resolving names to their corresponding values, through the
2+
%%% use of a `resolver' interface. Each `resolver' is a message that can be
3+
%%% given a `key' and returns an associated value. The device will attempt to
4+
%%% match the key against each resolver in turn, and return the value of the
5+
%%% first resolver that matches.
6+
-module(dev_name).
7+
-export([info/1]).
8+
-include("include/hb.hrl").
9+
-include_lib("eunit/include/eunit.hrl").
10+
11+
%% @doc Configure the `default' key to proxy to the `resolver/4' function.
12+
%% Exclude the `keys' and `set' keys from being processed by this device, as
13+
%% these are needed to modify the base message itself.
14+
info(_) ->
15+
#{
16+
default => fun resolve/4,
17+
exclude => [<<"keys">>, <<"set">>]
18+
}.
19+
20+
%% @doc Resolve a name to its corresponding value. The name is given by the key
21+
%% called. For example, `GET /~name@1.0/hello&load=false' grants the value of
22+
%% `hello'. If the `load' key is set to `true', the value is treated as a
23+
%% pointer and its contents is loaded from the cache. For example,
24+
%% `GET /~name@1.0/reference' yields the message at the path specified by the
25+
%% `reference' key.
26+
resolve(Key, _, Req, Opts) ->
27+
Resolvers = hb_opts:get(name_resolvers, [], Opts),
28+
?event({resolvers, Resolvers}),
29+
case match_resolver(Key, Resolvers, Opts) of
30+
{ok, Resolved} ->
31+
case hb_util:atom(hb_ao:get(<<"load">>, Req, true, Opts)) of
32+
false ->
33+
{ok, Resolved};
34+
true ->
35+
hb_cache:read(Resolved, Opts)
36+
end;
37+
not_found ->
38+
not_found
39+
end.
40+
41+
%% @doc Find the first resolver that matches the key and return its value.
42+
match_resolver(_Key, [], _Opts) ->
43+
not_found;
44+
match_resolver(Key, [Resolver | Resolvers], Opts) ->
45+
case execute_resolver(Key, Resolver, Opts) of
46+
{ok, Value} ->
47+
?event({resolver_found, {key, Key}, {value, Value}}),
48+
{ok, Value};
49+
_ ->
50+
match_resolver(Key, Resolvers, Opts)
51+
end.
52+
53+
%% @doc Execute a resolver with the given key and return its value.
54+
execute_resolver(Key, Resolver, Opts) ->
55+
?event({executing, {key, Key}, {resolver, Resolver}}),
56+
hb_ao:resolve(
57+
Resolver,
58+
#{ <<"path">> => <<"lookup">>, <<"key">> => Key },
59+
Opts
60+
).
61+
62+
%%% Tests.
63+
64+
no_resolvers_test() ->
65+
?assertEqual(
66+
not_found,
67+
resolve(<<"hello">>, #{}, #{}, #{ only => local })
68+
).
69+
70+
message_lookup_device_resolver(Msg) ->
71+
#{
72+
<<"device">> => #{
73+
<<"lookup">> => fun(_, Req, Opts) ->
74+
Key = hb_ao:get(<<"key">>, Req, Opts),
75+
?event({test_resolver_executing, {key, Key}, {req, Req}, {msg, Msg}}),
76+
case maps:get(Key, Msg, not_found) of
77+
not_found ->
78+
?event({test_resolver_not_found, {key, Key}, {msg, Msg}}),
79+
{error, not_found};
80+
Value ->
81+
?event({test_resolver_found, {key, Key}, {value, Value}}),
82+
{ok, Value}
83+
end
84+
end
85+
}
86+
}.
87+
88+
single_resolver_test() ->
89+
?assertEqual(
90+
{ok, <<"world">>},
91+
resolve(
92+
<<"hello">>,
93+
#{},
94+
#{ <<"load">> => false },
95+
#{
96+
name_resolvers => [
97+
message_lookup_device_resolver(
98+
#{<<"hello">> => <<"world">>}
99+
)
100+
]
101+
}
102+
)
103+
).
104+
105+
multiple_resolvers_test() ->
106+
?assertEqual(
107+
{ok, <<"bigger-world">>},
108+
resolve(
109+
<<"hello">>,
110+
#{},
111+
#{ <<"load">> => false },
112+
#{
113+
name_resolvers => [
114+
message_lookup_device_resolver(
115+
#{<<"irrelevant">> => <<"world">>}
116+
),
117+
message_lookup_device_resolver(
118+
#{<<"hello">> => <<"bigger-world">>}
119+
)
120+
]
121+
}
122+
)
123+
).
124+
125+
%% @doc Test that we can resolve messages from a name loaded with the device.
126+
load_and_execute_test() ->
127+
TestKey = <<"test-key", (hb_util:bin(erlang:system_time(millisecond)))/binary>>,
128+
{ok, ID} = hb_cache:write(
129+
#{
130+
<<"deep">> => <<"PING">>
131+
},
132+
#{}
133+
),
134+
?assertEqual(
135+
{ok, <<"PING">>},
136+
hb_ao:resolve_many(
137+
[
138+
#{ <<"device">> => <<"name@1.0">> },
139+
#{ <<"path">> => TestKey },
140+
#{ <<"path">> => <<"deep">> }
141+
],
142+
#{
143+
name_resolvers => [
144+
message_lookup_device_resolver(#{ <<"irrelevant">> => ID }),
145+
message_lookup_device_resolver(#{ TestKey => ID })
146+
]
147+
}
148+
)
149+
).

src/hb_http_server.erl

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,12 @@ allowed_methods(Req, State) ->
324324
%% @doc Update the `Opts' map that the HTTP server uses for all future
325325
%% requests.
326326
set_opts(Opts) ->
327-
ServerRef = hb_opts:get(http_server, no_server_ref, Opts),
328-
ok = cowboy:set_env(ServerRef, node_msg, Opts).
327+
case hb_opts:get(http_server, no_server_ref, Opts) of
328+
no_server_ref ->
329+
ok;
330+
ServerRef ->
331+
ok = cowboy:set_env(ServerRef, node_msg, Opts)
332+
end.
329333

330334
get_opts(NodeMsg) ->
331335
ServerRef = hb_opts:get(http_server, no_server_ref, NodeMsg),

src/hb_opts.erl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,15 @@ default_message() ->
6262
#{<<"name">> => <<"hyperbuddy@1.0">>, <<"module">> => dev_hyperbuddy},
6363
#{<<"name">> => <<"json@1.0">>, <<"module">> => dev_codec_json},
6464
#{<<"name">> => <<"json-iface@1.0">>, <<"module">> => dev_json_iface},
65+
#{<<"name">> => <<"local-name@1.0">>, <<"module">> => dev_local_name},
6566
#{<<"name">> => <<"lookup@1.0">>, <<"module">> => dev_lookup},
6667
#{<<"name">> => <<"lua@5.3a">>, <<"module">> => dev_lua},
6768
#{<<"name">> => <<"manifest@1.0">>, <<"module">> => dev_manifest},
6869
#{<<"name">> => <<"message@1.0">>, <<"module">> => dev_message},
6970
#{<<"name">> => <<"meta@1.0">>, <<"module">> => dev_meta},
7071
#{<<"name">> => <<"monitor@1.0">>, <<"module">> => dev_monitor},
7172
#{<<"name">> => <<"multipass@1.0">>, <<"module">> => dev_multipass},
73+
#{<<"name">> => <<"name@1.0">>, <<"module">> => dev_name},
7274
#{<<"name">> => <<"p4@1.0">>, <<"module">> => dev_p4},
7375
#{<<"name">> => <<"patch@1.0">>, <<"module">> => dev_patch},
7476
#{<<"name">> => <<"poda@1.0">>, <<"module">> => dev_poda},

0 commit comments

Comments
 (0)