Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c713053efd | |||
| 751c099a44 | |||
| be0607f7c1 | |||
| 823291986e | |||
| c5349f5736 | |||
| 7252ecd40b | |||
| e8febcf8d5 | |||
| 8a42f4a7a3 | |||
| a305bf3511 | |||
| f2fa83c215 | |||
| 4c09490f8a | |||
| 39b92996aa | |||
| d23196e746 | |||
| e9b1bccf57 | |||
| c9cdedf85c | |||
| 11516cb177 | |||
| 7c2db6eab7 | |||
|
|
f770bc299e | ||
|
|
c934510859 |
@ -3,7 +3,7 @@
|
||||
{included_applications,[]},
|
||||
{applications,[stdlib,kernel]},
|
||||
{description,"Gajumaru interoperation library"},
|
||||
{vsn,"0.6.1"},
|
||||
{vsn,"0.7.0"},
|
||||
{modules,[hakuzaru,hz,hz_fetcher,hz_grids,hz_key_master,hz_man,
|
||||
hz_sup]},
|
||||
{mod,{hakuzaru,[]}}]}.
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
%%% @end
|
||||
|
||||
-module(hakuzaru).
|
||||
-vsn("0.6.1").
|
||||
-vsn("0.7.0").
|
||||
-author("Craig Everett <ceverett@tsuriai.jp>").
|
||||
-copyright("Craig Everett <ceverett@tsuriai.jp>").
|
||||
-license("GPL-3.0-or-later").
|
||||
|
||||
157
src/hz.erl
157
src/hz.erl
@ -9,7 +9,7 @@
|
||||
%%%
|
||||
%%% The get/set admin functions are for setting or checking things like the Gajumaru
|
||||
%%% "network ID" and list of addresses of nodes you want to use for answering
|
||||
%%% queries to the blockchain.
|
||||
%%% queries to the blockchain. Get functions are arity 0, and set functions are arity 1.
|
||||
%%%
|
||||
%%% The JSON query interface functions are the blockchain query functions themselves
|
||||
%%% which are translated to network queries and return Erlang messages as responses.
|
||||
@ -18,12 +18,12 @@
|
||||
%%% a desired call to a smart contract on the chain to call data serialized in a form
|
||||
%%% that a Gajumaru compatible wallet or library can sign and submit to a Gajumaru node.
|
||||
%%%
|
||||
%%% This module does not implement the OTP application behavior.
|
||||
%%% helper functions.
|
||||
%%% NOTE:
|
||||
%%% This module does not implement the OTP application behavior. Refer to hakuzaru.erl.
|
||||
%%% @end
|
||||
|
||||
-module(hz).
|
||||
-vsn("0.6.1").
|
||||
-vsn("0.7.0").
|
||||
-author("Craig Everett <ceverett@tsuriai.jp>").
|
||||
-copyright("Craig Everett <ceverett@tsuriai.jp>").
|
||||
-license("GPL-3.0-or-later").
|
||||
@ -73,8 +73,8 @@
|
||||
decode_bytearray_fate/1, decode_bytearray/2,
|
||||
spend/5, spend/10,
|
||||
sign_tx/2, sign_tx/3,
|
||||
sign_message/2,
|
||||
verify_signature/3]).
|
||||
sign_message/2, verify_signature/3,
|
||||
sign_binary/2, verify_bin_signature/3]).
|
||||
|
||||
|
||||
%%% Types
|
||||
@ -225,7 +225,7 @@
|
||||
NetworkID :: string(),
|
||||
Reason :: term().
|
||||
%% @doc
|
||||
%% Returns the network ID or the atom `none' if it is unset.
|
||||
%% Returns the network ID or the atom `none' if unavailable.
|
||||
%% Checking this is not normally necessary, but if network ID assignment is dynamic
|
||||
%% in your system it may be necessary to call this before attempting to form
|
||||
%% call data or perform other actions on chain that require a signature.
|
||||
@ -241,7 +241,9 @@ network_id() ->
|
||||
%% @doc
|
||||
%% Returns the list of currently assigned nodes.
|
||||
%% The normal reason to call this is in preparation for altering the nodes list or
|
||||
%% checking the current list in debugging.
|
||||
%% checking the current list in debugging. Note that the first node in the list is
|
||||
%% the "sticky" node: the one that will be used for submitting transactions and
|
||||
%% querying `next_nonce'.
|
||||
|
||||
chain_nodes() ->
|
||||
hz_man:chain_nodes().
|
||||
@ -251,19 +253,26 @@ chain_nodes() ->
|
||||
when List :: [chain_node()],
|
||||
Reason :: {invalid, [term()]}.
|
||||
%% @doc
|
||||
%% Sets the nodes that are intended to be used as your interface to the peer
|
||||
%% network. The common situation is that your project runs a non-mining node as
|
||||
%% part of your backend infrastructure. Typically one or two nodes is plenty, but
|
||||
%% this may need to expand depending on how much query load your application generates.
|
||||
%% The Hakuzaru manager will load balance by round-robin distribution.
|
||||
%% Sets the chain nodes that will be queried whenever you communicate with the chain.
|
||||
%%
|
||||
%% NOTE: When load balancing in this way be aware that there can be race conditions
|
||||
%% among the backend nodes with regard to a single account's current nonce when performing
|
||||
%% contract calls in quick succession. Round robin distribution is extremely useful when
|
||||
%% performing rapid lookups to the chain, but does not work well when submitting many
|
||||
%% transactions to the chain from a single user in a short period of time. A future version
|
||||
%% of this library will allow the caller to designate a single node as "sticky" to be used
|
||||
%% exclusively in the case of nonce reads and TX submissions.
|
||||
%% The common situation is that a project runs a non-mining node as part of the backend
|
||||
%% infrastructure. Typically one or two nodes is plenty, but this may need to expand
|
||||
%% depending on how much query load your application generates.
|
||||
%%
|
||||
%% There are two situations: one node, or multiple nodes.
|
||||
%%
|
||||
%% Single node:
|
||||
%% In the case of a single node, everything passes through that one node. Duh.
|
||||
%%
|
||||
%% Multiple nodes:
|
||||
%% In the case of multiple nodes a distinction is made between the node to which
|
||||
%% transactions that update the chain state are made and to which `next_nonce' queries
|
||||
%% are made, and nodes that are used for read-only queries. The node to which stateful
|
||||
%% transactions are submitted is called the "sticky node". This is the first node
|
||||
%% (head position) in the list of nodes submitted to the chain when `chain_nodes/1'
|
||||
%% is called. If using multiple nodes but the sticky node should also be used for
|
||||
%% read-only queries, submit the sticky node at the head of the list and again in
|
||||
%% the tail.
|
||||
|
||||
chain_nodes(List) when is_list(List) ->
|
||||
hz_man:chain_nodes(List).
|
||||
@ -271,7 +280,16 @@ chain_nodes(List) when is_list(List) ->
|
||||
|
||||
-spec tls() -> boolean().
|
||||
%% @doc
|
||||
%% Check whether TLS is in use.
|
||||
%% Check whether TLS is in use. The typical situation is to not use TLS as nodes that
|
||||
%% serve as part of the backend of an application are typically run in the same
|
||||
%% backend network as the application service. When accessing chain nodes over the WAN
|
||||
%% however, TLS is strongly recommended to avoid a MITM attack.
|
||||
%%
|
||||
%% In this version of Hakuzaru TLS is either on or off for all nodes, making a mixed
|
||||
%% infrastructure complicated to support without two Hakuzaru instances. This will
|
||||
%% likely become a per-node setting in the future.
|
||||
%%
|
||||
%% TLS defaults to `false'.
|
||||
|
||||
tls() ->
|
||||
hz_man:tls().
|
||||
@ -281,6 +299,8 @@ tls() ->
|
||||
%% @doc
|
||||
%% Set TLS true or false. That's what a boolean is, by the way, `true' or `false'.
|
||||
%% This is a condescending comment. That means I am talking down to you.
|
||||
%%
|
||||
%% TLS defaults to `false'.
|
||||
|
||||
tls(Boolean) ->
|
||||
hz_man:tls(Boolean).
|
||||
@ -291,6 +311,8 @@ tls(Boolean) ->
|
||||
when Timeout :: pos_integer() | infinity.
|
||||
%% @doc
|
||||
%% Returns the current request timeout setting in milliseconds.
|
||||
%% The default timeout is 5,000ms.
|
||||
%% The max timeout is 120,000ms.
|
||||
|
||||
timeout() ->
|
||||
hz_man:timeout().
|
||||
@ -300,6 +322,8 @@ timeout() ->
|
||||
when MS :: pos_integer() | infinity.
|
||||
%% @doc
|
||||
%% Sets the request timeout in milliseconds.
|
||||
%% The default timeout is 5,000ms.
|
||||
%% The max timeout is 120,000ms.
|
||||
|
||||
timeout(MS) ->
|
||||
hz_man:timeout(MS).
|
||||
@ -576,18 +600,18 @@ acc_pending_txs(AccountID) ->
|
||||
%% Retrieve the next nonce for the given account
|
||||
|
||||
next_nonce(AccountID) ->
|
||||
% case request(["/v3/accounts/", AccountID, "/next-nonce"]) of
|
||||
% {ok, #{"next_nonce" := Nonce}} -> {ok, Nonce};
|
||||
% {ok, #{"reason" := "Account not found"}} -> {ok, 1};
|
||||
% {ok, #{"reason" := Reason}} -> {error, Reason};
|
||||
% Error -> Error
|
||||
% end.
|
||||
case request(["/v3/accounts/", AccountID]) of
|
||||
{ok, #{"nonce" := Nonce}} -> {ok, Nonce + 1};
|
||||
case request_sticky(["/v3/accounts/", AccountID, "/next-nonce"]) of
|
||||
{ok, #{"next_nonce" := Nonce}} -> {ok, Nonce};
|
||||
{ok, #{"reason" := "Account not found"}} -> {ok, 1};
|
||||
{ok, #{"reason" := Reason}} -> {error, Reason};
|
||||
Error -> Error
|
||||
end.
|
||||
% case request_sticky(["/v3/accounts/", AccountID]) of
|
||||
% {ok, #{"nonce" := Nonce}} -> {ok, Nonce + 1};
|
||||
% {ok, #{"reason" := "Account not found"}} -> {ok, 1};
|
||||
% {ok, #{"reason" := Reason}} -> {error, Reason};
|
||||
% Error -> Error
|
||||
% end.
|
||||
|
||||
|
||||
-spec dry_run(TX) -> {ok, Result} | {error, Reason}
|
||||
@ -729,7 +753,7 @@ tx_info(ID) ->
|
||||
|
||||
post_tx(Data) when is_binary(Data) ->
|
||||
JSON = zj:binary_encode(#{tx => Data}),
|
||||
request("/v3/transactions", JSON);
|
||||
request_sticky("/v3/transactions", JSON);
|
||||
post_tx(Data) when is_list(Data) ->
|
||||
post_tx(list_to_binary(Data)).
|
||||
|
||||
@ -841,6 +865,14 @@ status_chainends() ->
|
||||
request("/v3/status/chain-ends").
|
||||
|
||||
|
||||
request_sticky(Path) ->
|
||||
hz_man:request_sticky(unicode:characters_to_list(Path)).
|
||||
|
||||
|
||||
request_sticky(Path, Payload) ->
|
||||
hz_man:request_sticky(unicode:characters_to_list(Path), Payload).
|
||||
|
||||
|
||||
request(Path) ->
|
||||
hz_man:request(unicode:characters_to_list(Path)).
|
||||
|
||||
@ -890,7 +922,7 @@ contract_create(CreatorID, Path, InitArgs) ->
|
||||
when CreatorID :: pubkey(),
|
||||
Nonce :: pos_integer(),
|
||||
Amount :: non_neg_integer(),
|
||||
TTL :: pos_integer(),
|
||||
TTL :: non_neg_integer(),
|
||||
Gas :: pos_integer(),
|
||||
GasPrice :: pos_integer(),
|
||||
Path :: file:filename(),
|
||||
@ -1143,9 +1175,10 @@ assemble_calldata2(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallDat
|
||||
read_aci(Path) ->
|
||||
case file:read_file(Path) of
|
||||
{ok, Bin} ->
|
||||
case zx_lib:b_to_ts(Bin) of
|
||||
error -> {error, bad_aci};
|
||||
OK -> OK
|
||||
try
|
||||
{ok, binary_to_term(Bin, [safe])}
|
||||
catch
|
||||
error:badarg -> {error, bad_aci}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
@ -1226,7 +1259,7 @@ contract_call(CallerID, Gas, AACI, ConID, Fun, Args) ->
|
||||
Gas :: pos_integer(),
|
||||
GasPrice :: pos_integer(),
|
||||
Amount :: non_neg_integer(),
|
||||
TTL :: pos_integer(),
|
||||
TTL :: non_neg_integer(),
|
||||
AACI :: aaci(),
|
||||
ConID :: unicode:chardata(),
|
||||
Fun :: string(),
|
||||
@ -1855,9 +1888,11 @@ coerce({_, _, contract}, {contract, Bin}, from_fate) ->
|
||||
Address = gmser_api_encoder:encode(contract_pubkey, Bin),
|
||||
{ok, unicode:characters_to_list(Address)};
|
||||
coerce({_, _, signature}, S, to_fate) when is_binary(S) andalso (byte_size(S) =:= 64) ->
|
||||
% If it is a binary of 64 bytes then it can be used as is... If it is an
|
||||
% sg_... string of 64 bytes, then it is too short to be valid, so just
|
||||
% interpret it as a binary directly.
|
||||
% Usually to pass a binary in, you need to wrap it as {raw, Binary}, but
|
||||
% since sg_... strings OR hex blobs can be used as signatures in Sophia, we
|
||||
% special case this case based on the length. Even if a binary starts with
|
||||
% "sg_", 64 characters is not enough to represent a 64 byte signature, so
|
||||
% the most optimistic interpretation is to use the binary directly.
|
||||
{ok, S};
|
||||
coerce({O, N, signature}, S, to_fate) ->
|
||||
coerce_chain_object(O, N, signature, signature, S);
|
||||
@ -1973,6 +2008,8 @@ coerce_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count ->
|
||||
coerce_bytes(_, _, _, Bytes) ->
|
||||
{ok, Bytes}.
|
||||
|
||||
coerce_chain_object(_, _, _, _, {raw, Binary}) ->
|
||||
{ok, Binary};
|
||||
coerce_chain_object(O, N, T, Tag, S) ->
|
||||
case decode_chain_object(Tag, S) of
|
||||
{ok, Data} -> {ok, coerce_chain_object2(T, Data)};
|
||||
@ -2394,7 +2431,7 @@ spend3(DSenderID,
|
||||
Sig :: binary().
|
||||
|
||||
sign_message(Message, SecKey) ->
|
||||
Prefix = <<"Gajumaru Signed Message:\n">>,
|
||||
Prefix = message_sig_prefix(),
|
||||
{ok, PSize} = vencode(byte_size(Prefix)),
|
||||
{ok, MSize} = vencode(byte_size(Message)),
|
||||
Smashed = iolist_to_binary([PSize, Prefix, MSize, Message]),
|
||||
@ -2403,7 +2440,7 @@ sign_message(Message, SecKey) ->
|
||||
|
||||
|
||||
-spec verify_signature(Sig, Message, PubKey) -> Result
|
||||
when Sig :: binary(),
|
||||
when Sig :: string(), % base64 encoded signature,
|
||||
Message :: iodata(),
|
||||
PubKey :: pubkey(),
|
||||
Result :: {ok, Outcome :: boolean()}
|
||||
@ -2428,7 +2465,7 @@ verify_signature2(Sig, Message, PK) ->
|
||||
% the user from accidentally signing a transaction disguised as a message.
|
||||
%
|
||||
% Salt the message then hash with blake2b.
|
||||
Prefix = <<"Gajumaru Signed Message:\n">>,
|
||||
Prefix = message_sig_prefix(),
|
||||
{ok, PSize} = vencode(byte_size(Prefix)),
|
||||
{ok, MSize} = vencode(byte_size(Message)),
|
||||
Smashed = iolist_to_binary([PSize, Prefix, MSize, Message]),
|
||||
@ -2438,6 +2475,7 @@ verify_signature2(Sig, Message, PK) ->
|
||||
Result = ecu_eddsa:sign_verify_detached(Signature, Hashed, PK),
|
||||
{ok, Result}.
|
||||
|
||||
message_sig_prefix() -> <<"Gajumaru Signed Message:\n">>.
|
||||
|
||||
% This is Bitcoin's variable-length unsigned integer encoding
|
||||
% See: https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
|
||||
@ -2465,6 +2503,42 @@ eu(N, Size) ->
|
||||
<<Bytes/binary, ExtraZeros/binary>>.
|
||||
|
||||
|
||||
-spec sign_binary(Binary, SecKey) -> Sig
|
||||
when Binary :: binary(),
|
||||
SecKey :: binary(),
|
||||
Sig :: binary().
|
||||
|
||||
sign_binary(Binary, SecKey) ->
|
||||
Prefix = binary_sig_prefix(),
|
||||
Target = <<Prefix/binary, Binary/binary>>,
|
||||
{ok, Hash} = eblake2:blake2b(32, Target),
|
||||
ecu_eddsa:sign_detached(Hash, SecKey).
|
||||
|
||||
|
||||
-spec verify_bin_signature(Sig, Binary, PubKey) -> Result
|
||||
when Sig :: string(), % base64 encoded signature,
|
||||
Binary :: binary(),
|
||||
PubKey :: pubkey(),
|
||||
Result :: {ok, Outcome :: boolean()}
|
||||
| {error, Reason :: term()}.
|
||||
|
||||
verify_bin_signature(Sig, Binary, PubKey) ->
|
||||
case gmser_api_encoder:decode(PubKey) of
|
||||
{account_pubkey, PK} -> verify_bin_signature2(Sig, Binary, PK);
|
||||
Other -> {error, {bad_key, Other}}
|
||||
end.
|
||||
|
||||
verify_bin_signature2(Sig, Binary, PK) ->
|
||||
Prefix = binary_sig_prefix(),
|
||||
Target = <<Prefix/binary, Binary/binary>>,
|
||||
{ok, Hash} = eblake2:blake2b(32, Target),
|
||||
Signature = base64:decode(Sig),
|
||||
Result = ecu_eddsa:sign_verify_detached(Signature, Hash, PK),
|
||||
{ok, Result}.
|
||||
|
||||
binary_sig_prefix() -> <<"Gajumaru Signed Binary:">>.
|
||||
|
||||
|
||||
%%% Debug functionality
|
||||
|
||||
% debug_network() ->
|
||||
@ -2563,6 +2637,7 @@ coerce_signature_binary_test() ->
|
||||
169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222,
|
||||
249,2,3,85,173,106,150,243,253,89,128,248,52,195,140,95,114,
|
||||
233,110,119,143,206,137,124,36,63,154,85,7>>,
|
||||
{ok, Binary} = coerce(Type, {raw, Binary}, to_fate),
|
||||
{ok, Binary} = coerce(Type, Binary, to_fate),
|
||||
ok.
|
||||
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
-module(hz_fetcher).
|
||||
-vsn("0.6.1").
|
||||
-vsn("0.7.0").
|
||||
-author("Craig Everett <ceverett@tsuriai.jp>").
|
||||
-copyright("Craig Everett <ceverett@tsuriai.jp>").
|
||||
-license("MIT").
|
||||
|
||||
-export([connect/4, slowly_connect/4]).
|
||||
|
||||
-include("$zx_include/zx_logger.hrl").
|
||||
-export([connect/4, connect_slowly/4]).
|
||||
|
||||
|
||||
connect(Node = {Host, Port}, Request, From, Timeout) ->
|
||||
@ -78,7 +76,7 @@ parse(Received, Sock, From, Timer) ->
|
||||
<<"HTTP/1.1 500 Internal Server Error\r\n", Tail/binary>> ->
|
||||
parse2(500, Tail, Sock, From, Timer);
|
||||
_ ->
|
||||
ok = zx_net:disconnect(Sock),
|
||||
ok = disconnect(Sock),
|
||||
ok = erlang:cancel_timer(Timer, [{async, true}]),
|
||||
gen_server:reply(From, {error, {received, Received}})
|
||||
end.
|
||||
@ -115,7 +113,7 @@ consume2(Length, Received, Sock, From, Timer) ->
|
||||
if
|
||||
Size == Length ->
|
||||
ok = erlang:cancel_timer(Timer, [{async, true}]),
|
||||
ok = zx_net:disconnect(Sock),
|
||||
ok = disconnect(Sock),
|
||||
Result = zj:decode(Received),
|
||||
gen_server:reply(From, Result);
|
||||
Size < Length ->
|
||||
@ -208,7 +206,7 @@ read_hval(_, Received, _, _, _) ->
|
||||
{error, headers}.
|
||||
|
||||
|
||||
slowly_connect(Node, {get, Path}, From, Timeout) ->
|
||||
connect_slowly(Node, {get, Path}, From, Timeout) ->
|
||||
HttpOptions = [{connect_timeout, 3000}, {timeout, Timeout}],
|
||||
URL = lists:flatten(url(Node, Path)),
|
||||
Request = {URL, []},
|
||||
@ -219,7 +217,7 @@ slowly_connect(Node, {get, Path}, From, Timeout) ->
|
||||
BAD -> {error, BAD}
|
||||
end,
|
||||
gen_server:reply(From, Result);
|
||||
slowly_connect(Node, {post, Path, Payload}, From, Timeout) ->
|
||||
connect_slowly(Node, {post, Path, Payload}, From, Timeout) ->
|
||||
HttpOptions = [{connect_timeout, 3000}, {timeout, Timeout}],
|
||||
URL = lists:flatten(url(Node, Path)),
|
||||
Request = {URL, [], "application/json", Payload},
|
||||
@ -236,3 +234,45 @@ url({Node, Port}, Path) when is_list(Node) ->
|
||||
["https://", Node, ":", integer_to_list(Port), Path];
|
||||
url({Node, Port}, Path) when is_tuple(Node) ->
|
||||
["https://", inet:ntoa(Node), ":", integer_to_list(Port), Path].
|
||||
|
||||
|
||||
disconnect(Socket) ->
|
||||
case peername(Socket) of
|
||||
{ok, {Addr, Port}} ->
|
||||
Host = inet:ntoa(Addr),
|
||||
disconnect(Socket, Host, Port);
|
||||
{error, Reason} ->
|
||||
log(warning, "Disconnect failed with: ~w", [Reason])
|
||||
end.
|
||||
|
||||
disconnect(Socket, Host, Port) ->
|
||||
case gen_tcp:shutdown(Socket, read_write) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, enotconn} ->
|
||||
receive
|
||||
{tcp_closed, Socket} -> ok
|
||||
after 0 -> ok
|
||||
end;
|
||||
{error, E} ->
|
||||
ok = log(warning, "~ts:~w disconnect failed with: ~w", [Host, Port, E]),
|
||||
receive
|
||||
{tcp_closed, Socket} -> ok
|
||||
after 0 -> ok
|
||||
end
|
||||
end.
|
||||
|
||||
peername(Socket) ->
|
||||
case inet:peername(Socket) of
|
||||
{ok, {{0, 0, 0, 0, 0, 65535, X, Y}, Port}} ->
|
||||
<<A:8, B:8, C:8, D:8>> = <<X:16, Y:16>>,
|
||||
{ok, {{A, B, C, D}, Port}};
|
||||
Other ->
|
||||
Other
|
||||
end.
|
||||
|
||||
|
||||
log(Level, Format, Args) ->
|
||||
Raw = io_lib:format("~w ~w: " ++ Format, [?MODULE, self() | Args]),
|
||||
Entry = unicode:characters_to_list(Raw),
|
||||
logger:log(Level, Entry).
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
%%% @end
|
||||
|
||||
-module(hz_grids).
|
||||
-vsn("0.6.1").
|
||||
-vsn("0.7.0").
|
||||
-export([url/2, parse/1, req/2, req/3]).
|
||||
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
%%% @end
|
||||
|
||||
-module(hz_key_master).
|
||||
-vsn("0.6.1").
|
||||
-vsn("0.7.0").
|
||||
|
||||
|
||||
-export([make_key/1, encode/1, decode/1]).
|
||||
@ -91,9 +91,8 @@ chunksize(N, C, A) -> chunksize(N div C, C, A + 1).
|
||||
|
||||
|
||||
read_words() ->
|
||||
{ok, V} = zx_lib:string_to_version(proplists:get_value(vsn, module_info(attributes))),
|
||||
HZ_Lib = zx_lib:ppath(lib, {"otpr", "hakuzaru", V}),
|
||||
Path = filename:join([HZ_Lib, "priv", "words4096.txt"]),
|
||||
ModPath = code:which(?MODULE),
|
||||
Path = filename:join([filename:dirname(filename:dirname(ModPath)), "priv", "words4096.txt"]),
|
||||
{ok, Bin} = file:read_file(Path),
|
||||
string:lexemes(Bin, "\n").
|
||||
|
||||
|
||||
108
src/hz_man.erl
108
src/hz_man.erl
@ -9,7 +9,7 @@
|
||||
%%% @end
|
||||
|
||||
-module(hz_man).
|
||||
-vsn("0.6.1").
|
||||
-vsn("0.7.0").
|
||||
-behavior(gen_server).
|
||||
-author("Craig Everett <ceverett@tsuriai.jp>").
|
||||
-copyright("Craig Everett <ceverett@tsuriai.jp>").
|
||||
@ -21,16 +21,13 @@
|
||||
timeout/0, timeout/1]).
|
||||
|
||||
%% The whole point of this module:
|
||||
-export([request/1, request/2]).
|
||||
-export([request_sticky/1, request_sticky/2, request/1, request/2]).
|
||||
|
||||
%% gen_server goo
|
||||
-export([start_link/0]).
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
||||
code_change/3, terminate/2]).
|
||||
|
||||
%% TODO: Make logging more flexible
|
||||
-include("$zx_include/zx_logger.hrl").
|
||||
|
||||
|
||||
%%% Type and Record Definitions
|
||||
|
||||
@ -43,11 +40,11 @@
|
||||
req = none :: none | binary()}).
|
||||
|
||||
-record(s,
|
||||
{tls = false :: boolean(),
|
||||
chain_nodes = {[], []} :: {[hz:chain_node()], [hz:chain_node()]},
|
||||
sticky = none :: none | hz:chain_node(),
|
||||
fetchers = [] :: [#fetcher{}],
|
||||
timeout = 5000 :: pos_integer()}).
|
||||
{tls = false :: boolean(),
|
||||
chain_nodes = {[], []} :: {[hz:chain_node()], [hz:chain_node()]},
|
||||
sticky = none :: none | hz:chain_node(),
|
||||
fetchers = [] :: [#fetcher{}],
|
||||
timeout = 5000 :: pos_integer()}).
|
||||
|
||||
|
||||
-type state() :: #s{}.
|
||||
@ -97,6 +94,25 @@ timeout(Value) when 0 < Value, Value =< 120000 ->
|
||||
gen_server:cast(?MODULE, {timeout, Value}).
|
||||
|
||||
|
||||
-spec request_sticky(Path) -> {ok, Value} | {error, Reason}
|
||||
when Path :: unicode:charlist(),
|
||||
Value :: map(),
|
||||
Reason :: hz:chain_error().
|
||||
|
||||
request_sticky(Path) ->
|
||||
gen_server:call(?MODULE, {request_sticky, {get, Path}}, infinity).
|
||||
|
||||
|
||||
-spec request_sticky(Path, Data) -> {ok, Value} | {error, Reason}
|
||||
when Path :: unicode:charlist(),
|
||||
Data :: unicode:charlist(),
|
||||
Value :: map(),
|
||||
Reason :: hz:chain_error().
|
||||
|
||||
request_sticky(Path, Data) ->
|
||||
gen_server:call(?MODULE, {request_sticky, {post, Path, Data}}, infinity).
|
||||
|
||||
|
||||
-spec request(Path) -> {ok, Value} | {error, Reason}
|
||||
when Path :: unicode:charlist(),
|
||||
Value :: map(),
|
||||
@ -148,10 +164,13 @@ init(none) ->
|
||||
handle_call({request, Request}, From, State) ->
|
||||
NewState = do_request(Request, From, State),
|
||||
{noreply, NewState};
|
||||
handle_call({request_sticky, Request}, From, State) ->
|
||||
NewState = do_request_sticky(Request, From, State),
|
||||
{noreply, NewState};
|
||||
handle_call(tls, _, State = #s{tls = TLS}) ->
|
||||
{reply, TLS, State};
|
||||
handle_call(chain_nodes, _, State = #s{chain_nodes = {Wait, Used}}) ->
|
||||
Nodes = lists:append(Wait, Used),
|
||||
handle_call(chain_nodes, _, State) ->
|
||||
Nodes = do_chain_nodes(State),
|
||||
{reply, Nodes, State};
|
||||
handle_call(timeout, _, State = #s{timeout = Value}) ->
|
||||
{reply, Value, State};
|
||||
@ -163,10 +182,9 @@ handle_call(Unexpected, From, State) ->
|
||||
handle_cast({tls, Boolean}, State) ->
|
||||
NewState = do_tls(Boolean, State),
|
||||
{noreply, NewState};
|
||||
handle_cast({chain_nodes, []}, State) ->
|
||||
{noreply, State#s{chain_nodes = {[], []}}};
|
||||
handle_cast({chain_nodes, ToUse}, State) ->
|
||||
{noreply, State#s{chain_nodes = {ToUse, []}}};
|
||||
handle_cast({chain_nodes, List}, State) ->
|
||||
NewState = do_chain_nodes(List, State),
|
||||
{noreply, NewState};
|
||||
handle_cast({timeout, Value}, State) ->
|
||||
{noreply, State#s{timeout = Value}};
|
||||
handle_cast(Unexpected, State) ->
|
||||
@ -221,6 +239,23 @@ terminate(_, _) ->
|
||||
|
||||
%%% Doer Functions
|
||||
|
||||
do_chain_nodes(#s{sticky = none, chain_nodes = {Wait, Used}}) ->
|
||||
lists:append(Wait, Used);
|
||||
do_chain_nodes(#s{sticky = Sticky, chain_nodes = {Wait, Used}}) ->
|
||||
case lists:append(Wait, Used) of
|
||||
[Sticky] -> [Sticky];
|
||||
Nodes -> [Sticky | Nodes]
|
||||
end.
|
||||
|
||||
|
||||
do_chain_nodes([], State) ->
|
||||
State#s{sticky = none, chain_nodes = {[], []}};
|
||||
do_chain_nodes(List = [Sticky], State) ->
|
||||
State#s{sticky = Sticky, chain_nodes = {List, []}};
|
||||
do_chain_nodes([Sticky | List], State) ->
|
||||
State#s{sticky = Sticky, chain_nodes = {List, []}}.
|
||||
|
||||
|
||||
do_tls(true, State) ->
|
||||
ok = ssl:start(),
|
||||
State#s{tls = true};
|
||||
@ -230,17 +265,21 @@ do_tls(_, State) ->
|
||||
State.
|
||||
|
||||
|
||||
do_request(_, From, State = #s{chain_nodes = {[], []}}) ->
|
||||
do_request_sticky(_, From, State = #s{sticky = none}) ->
|
||||
ok = gen_server:reply(From, {error, no_nodes}),
|
||||
State;
|
||||
do_request(Request,
|
||||
From,
|
||||
State = #s{tls = false,
|
||||
fetchers = Fetchers,
|
||||
chain_nodes = {[Node | Rest], Used},
|
||||
timeout = Timeout}) ->
|
||||
do_request_sticky(Request,
|
||||
From,
|
||||
State = #s{tls = TLS,
|
||||
fetchers = Fetchers,
|
||||
sticky = Node,
|
||||
timeout = Timeout}) ->
|
||||
Now = erlang:system_time(nanosecond),
|
||||
Fetcher = fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end,
|
||||
Fetcher =
|
||||
case TLS of
|
||||
true -> fun() -> hz_fetcher:connect_slowly(Node, Request, From, Timeout) end;
|
||||
false -> fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end
|
||||
end,
|
||||
{PID, Mon} = spawn_monitor(Fetcher),
|
||||
New = #fetcher{pid = PID,
|
||||
mon = Mon,
|
||||
@ -248,15 +287,24 @@ do_request(Request,
|
||||
node = Node,
|
||||
from = From,
|
||||
req = Request},
|
||||
State#s{fetchers = [New | Fetchers], chain_nodes = {Rest, [Node | Used]}};
|
||||
State#s{fetchers = [New | Fetchers]}.
|
||||
|
||||
|
||||
do_request(_, From, State = #s{chain_nodes = {[], []}}) ->
|
||||
ok = gen_server:reply(From, {error, no_nodes}),
|
||||
State;
|
||||
do_request(Request,
|
||||
From,
|
||||
State = #s{tls = true,
|
||||
State = #s{tls = TLS,
|
||||
fetchers = Fetchers,
|
||||
chain_nodes = {[Node | Rest], Used},
|
||||
timeout = Timeout}) ->
|
||||
Now = erlang:system_time(nanosecond),
|
||||
Fetcher = fun() -> hz_fetcher:slowly_connect(Node, Request, From, Timeout) end,
|
||||
Fetcher =
|
||||
case TLS of
|
||||
true -> fun() -> hz_fetcher:connect_slowly(Node, Request, From, Timeout) end;
|
||||
false -> fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end
|
||||
end,
|
||||
{PID, Mon} = spawn_monitor(Fetcher),
|
||||
New = #fetcher{pid = PID,
|
||||
mon = Mon,
|
||||
@ -268,3 +316,9 @@ do_request(Request,
|
||||
do_request(Request, From, State = #s{chain_nodes = {[], Used}}) ->
|
||||
Fresh = lists:reverse(Used),
|
||||
do_request(Request, From, State#s{chain_nodes = {Fresh, []}}).
|
||||
|
||||
|
||||
log(Level, Format, Args) ->
|
||||
Raw = io_lib:format("~w ~w: " ++ Format, [?MODULE, self() | Args]),
|
||||
Entry = unicode:characters_to_list(Raw),
|
||||
logger:log(Level, Entry).
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
%%% @end
|
||||
|
||||
-module(hz_sup).
|
||||
-vsn("0.6.1").
|
||||
-vsn("0.7.0").
|
||||
-behaviour(supervisor).
|
||||
-author("Craig Everett <zxq9@zxq9.com>").
|
||||
-copyright("Craig Everett <zxq9@zxq9.com>").
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
{prefix,"hz"}.
|
||||
{desc,"Gajumaru interoperation library"}.
|
||||
{author,"Craig Everett"}.
|
||||
{package_id,{"otpr","hakuzaru",{0,6,1}}}.
|
||||
{package_id,{"otpr","hakuzaru",{0,7,0}}}.
|
||||
{deps,[{"otpr","sophia",{9,0,0}},
|
||||
{"otpr","gmserialization",{0,1,3}},
|
||||
{"otpr","gmbytecode",{3,4,1}},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user