346 lines
12 KiB
Erlang
346 lines
12 KiB
Erlang
%%% -*- erlang-indent-level:4; indent-tabs-mode: nil -*-
|
|
%%%-------------------------------------------------------------------
|
|
%%% @copyright (C) 2025, QPQ AG
|
|
%%% @doc Gajumaru mining pool protocol messages
|
|
%%%
|
|
%%% @end
|
|
%%%-------------------------------------------------------------------
|
|
-module(gmmpp_msgs).
|
|
|
|
-export([
|
|
validate/2
|
|
, encode/3
|
|
, decode/3
|
|
, encode_request/4
|
|
, encode_reply/4
|
|
, encode_msg/3
|
|
]).
|
|
|
|
-export([ encode_connect/2 %% (Params, Id)
|
|
, decode_connect/1 %% (MsgBin)
|
|
, encode_connect_ack/2 %% (Params, Id)
|
|
, decode_connect_ack/1 %% (MsgBin)
|
|
]).
|
|
|
|
-export([ versions/0
|
|
, protocols/1
|
|
, latest_version/0
|
|
, connect_version/0
|
|
, connect_protocol/0
|
|
]).
|
|
|
|
-type protocol() :: binary().
|
|
-type version() :: binary().
|
|
|
|
-export_type([ protocol/0
|
|
, version/0 ]).
|
|
|
|
-define(VSN0, <<"0.1">>).
|
|
-define(VSN, ?VSN0).
|
|
-define(PROTOCOL_JSON, <<"json">>).
|
|
-define(PROTOCOL, ?PROTOCOL_JSON).
|
|
|
|
-spec latest_version() -> version().
|
|
latest_version() ->
|
|
?VSN.
|
|
|
|
connect_version() -> ?VSN0.
|
|
connect_protocol() -> ?PROTOCOL_JSON.
|
|
|
|
-spec versions() -> [version()].
|
|
%% List sorted highest priority first
|
|
versions() -> [?VSN0].
|
|
|
|
-spec protocols(version()) -> [protocol()].
|
|
%% List sorted highest priority first
|
|
protocols(_Vsn) -> [?PROTOCOL].
|
|
|
|
validate(#{ connect := #{ pubkey := PK
|
|
, protocols := Protocols
|
|
, versions := Versions
|
|
, pool_id := PoolId
|
|
, extra_pubkeys := Extra
|
|
, type := Type0
|
|
, nonces := Nonces
|
|
}} = Msg, _Vsn) ->
|
|
Type = to_atom(Type0),
|
|
valid({list, protocol}, Protocols),
|
|
valid({list, version} , Versions),
|
|
valid(pubkey, PK),
|
|
valid(contract, PoolId),
|
|
valid(pubkey, PK),
|
|
valid({list, pubkey}, Extra),
|
|
valid(type, Type),
|
|
valid(pos_int, Nonces),
|
|
Msg;
|
|
validate(#{ connect_ack := #{ protocol := Protocol
|
|
, version := Version }} = Msg, _Vsn) ->
|
|
valid(protocol, Protocol),
|
|
valid(version, Version),
|
|
Msg;
|
|
validate(#{ candidate := #{ seq := Seq
|
|
, candidate := C
|
|
, target := Target
|
|
, nonces := Nonces
|
|
, edge_bits := EdgeBits } } = Msg, _Vsn) ->
|
|
valid(candidate, C),
|
|
valid(target, Target),
|
|
valid(nonces, Nonces),
|
|
valid(seq, Seq),
|
|
valid(edge_bits, EdgeBits),
|
|
Msg;
|
|
validate(#{ get_nonces := #{ seq := Seq
|
|
, n := N }} = Msg, _Vsn) ->
|
|
valid(seq, Seq),
|
|
valid(pos_int, N),
|
|
Msg;
|
|
validate(#{ new_server := #{ host := Host, port := Port, keep := Keep }} = Msg, _Vsn) ->
|
|
valid(string, Host),
|
|
valid(pos_ind, Port),
|
|
valid(boolean, Keep),
|
|
Msg;
|
|
validate(#{ nonces := #{seq := Seq, nonces := Ns}} = Msg, _vsn) ->
|
|
valid(seq, Seq),
|
|
valid(nonces, Ns),
|
|
Msg;
|
|
validate(#{ solutions := #{ seq := Seq
|
|
, found := Solutions } } = Msg, _Vsn) ->
|
|
valid(seq, Seq),
|
|
valid(solutions, Solutions),
|
|
Msg;
|
|
validate(#{ solution_accepted := #{ seq := Seq }} = Msg, _Vsn) ->
|
|
valid(seq, Seq),
|
|
Msg;
|
|
validate(#{ no_solution := #{ seq := Seq
|
|
, nonce := Nonce } } = Msg, _Vsn) ->
|
|
valid(seq, Seq),
|
|
valid(nonce, Nonce),
|
|
Msg;
|
|
validate(#{ stop_mining := #{} } = Msg, _Vsn) ->
|
|
Msg.
|
|
|
|
encode_connect(#{ protocols := _Protocols
|
|
, versions := _Versions
|
|
, pool_id := _PoolId
|
|
, pubkey := _Pubkey
|
|
, extra_pubkeys := _Extra
|
|
, type := _Type
|
|
, signature := _Sig } = Params, Id) ->
|
|
encode_request(#{connect => Params}, Id, connect_protocol(), connect_version()).
|
|
|
|
decode_connect(MsgBin) ->
|
|
decode(MsgBin, connect_protocol(), connect_version()).
|
|
|
|
encode_connect_ack(#{ protocol := _
|
|
, version := _ } = Params, Id) ->
|
|
encode_reply(#{ connect_ack => Params }, Id, connect_protocol(), connect_version()).
|
|
|
|
decode_connect_ack(MsgBin) ->
|
|
decode(MsgBin, connect_protocol(), connect_version()).
|
|
|
|
decode(MsgBin, ?PROTOCOL_JSON, Vsn) ->
|
|
case json:decode(MsgBin) of
|
|
#{ <<"jsonrpc">> := <<"2.0">> } = Msg ->
|
|
case Msg of
|
|
#{ <<"method">> := Method
|
|
, <<"params">> := Params
|
|
, <<"id">> := Id } ->
|
|
%% JSON-RPC call request
|
|
#{ call => #{ id => Id
|
|
, req => validate(decode_msg_(Method, Params), Vsn) }};
|
|
#{ <<"method">> := Method
|
|
, <<"params">> := Params } ->
|
|
%% JSON-RPC notification
|
|
#{ notification => validate(decode_msg_(Method, Params), Vsn) };
|
|
#{ <<"id">> := Id
|
|
, <<"result">> := Result } ->
|
|
#{ reply => #{ id => Id
|
|
, result => decode_result(Result, Vsn) }};
|
|
#{ <<"id">> := Id
|
|
, <<"error">> := #{ <<"code">> := Code
|
|
, <<"message">> := Message }} ->
|
|
#{ error => #{ id => Id
|
|
, code => Code
|
|
, message => Message }}
|
|
end
|
|
end.
|
|
|
|
encode(#{call := Req0}, P, V) ->
|
|
{Id, Req} = maps:take(id, Req0),
|
|
encode_request(Req, Id, P, V);
|
|
encode(#{notification := Msg}, P, V) ->
|
|
encode_msg(Msg, P, V);
|
|
encode(#{reply := Reply0}, P, V) when is_map(Reply0) ->
|
|
{Id, Reply} = maps:take(id, Reply0),
|
|
encode_reply(Reply, Id, P, V).
|
|
|
|
encode_msg(Msg0, ?PROTOCOL_JSON, Vsn) ->
|
|
Msg = validate(Msg0, Vsn),
|
|
[{Method, Args}] = maps:to_list(Msg),
|
|
json_encode(#{ <<"jsonrpc">> => <<"2.0">>
|
|
, <<"method">> => Method
|
|
, <<"params">> => Args }).
|
|
|
|
encode_request(Req0, Id, ?PROTOCOL_JSON, Vsn) ->
|
|
Req = validate(Req0, Vsn),
|
|
[{Method, Args}] = maps:to_list(Req),
|
|
json_encode(#{ <<"jsonrpc">> => <<"2.0">>
|
|
, <<"id">> => Id
|
|
, <<"method">> => Method
|
|
, <<"params">> => Args }).
|
|
|
|
encode_reply(Reply0, Id, ?PROTOCOL_JSON, Vsn) when is_map(Reply0) ->
|
|
Reply = validate(Reply0, Vsn),
|
|
Msg = #{ <<"jsonrpc">> => <<"2.0">>
|
|
, <<"id">> => Id
|
|
, <<"result">> => Reply },
|
|
json_encode(Msg);
|
|
encode_reply(Reply, Id, ?PROTOCOL_JSON, _Vsn) ->
|
|
Msg = case Reply of
|
|
{error, Reason} ->
|
|
Error = encode_error(Reason),
|
|
#{ <<"jsonrpc">> => <<"2.0">>
|
|
, <<"id">> => Id
|
|
, <<"error">> => Error};
|
|
ok ->
|
|
#{ <<"jsonrpc">> => <<"2.0">>
|
|
, <<"id">> => Id
|
|
, <<"result">> => <<"ok">> };
|
|
continue ->
|
|
#{ <<"jsonrpc">> => <<"2.0">>
|
|
, <<"id">> => Id
|
|
, <<"result">> => <<"continue">> }
|
|
end,
|
|
json_encode(Msg).
|
|
|
|
json_encode(Msg) ->
|
|
iolist_to_binary(json:encode(Msg)).
|
|
|
|
encode_error(#{code := _, message := _} = E) ->
|
|
E;
|
|
encode_error(Reason) ->
|
|
#{ <<"code">> => error_code(Reason)
|
|
, <<"message">> => Reason }.
|
|
|
|
error_code(mining_disabled ) -> -32000;
|
|
error_code(nyi ) -> -32001; %% random.org uses this code for nyi
|
|
error_code(pool_not_found ) -> -32002;
|
|
error_code(pool_exists ) -> -32003;
|
|
error_code(unknown_contract ) -> -32004;
|
|
error_code(invalid_prefix ) -> -32005;
|
|
error_code(invalid_encoding ) -> -32006;
|
|
error_code(outdated ) -> -32007;
|
|
error_code(solution_mismatch) -> -32008;
|
|
error_code(invalid_input ) -> -32009;
|
|
error_code(unknown_method ) -> -32601;
|
|
error_code(_ ) -> -32603. % internal error
|
|
|
|
decode_result(<<"ok">>, _) -> ok;
|
|
decode_result(<<"continue">>, _) -> continue;
|
|
decode_result(#{<<"connect_ack">> := #{ <<"protocol">> := P
|
|
, <<"version">> := V }}, Vsn) ->
|
|
Msg = #{connect_ack => #{ protocol => P
|
|
, version => V }},
|
|
validate(Msg, Vsn);
|
|
decode_result(#{<<"nonces">> := #{ <<"seq">> := Seq
|
|
, <<"nonces">> := Nonces}}, _) ->
|
|
valid(seq, Seq),
|
|
valid(nonces, Nonces),
|
|
#{nonces => #{seq => Seq, nonces => Nonces}}.
|
|
|
|
%% Mapping types
|
|
decode_msg_(<<"connect">>, #{ <<"protocols">> := Protos
|
|
, <<"versions">> := Vsns
|
|
, <<"pool_id">> := PoolId
|
|
, <<"pubkey">> := PK
|
|
, <<"extra_pubkeys">> := Extra
|
|
, <<"type">> := Type
|
|
, <<"nonces">> := Nonces }) ->
|
|
#{connect => #{ protocols => Protos
|
|
, versions => Vsns
|
|
, pool_id => PoolId
|
|
, pubkey => PK
|
|
, extra_pubkeys => Extra
|
|
, type => Type
|
|
, nonces => Nonces
|
|
}};
|
|
decode_msg_(<<"new_server">>, #{ <<"host">> := Host
|
|
, <<"port">> := Port
|
|
, <<"keep">> := Keep }) ->
|
|
#{new_server => #{host => Host, port => Port, keep => Keep}};
|
|
decode_msg_(<<"get_nonces">>, #{ <<"seq">> := Seq
|
|
, <<"n">> := N }) ->
|
|
#{get_nonces => #{seq => Seq, n => N}};
|
|
decode_msg_(<<"candidate">>, #{ <<"candidate">> := C
|
|
, <<"target">> := Target
|
|
, <<"nonces">> := Nonces
|
|
, <<"seq">> := Seq
|
|
, <<"edge_bits">> := EdgeBits }) ->
|
|
#{candidate => #{ candidate => C
|
|
, target => Target
|
|
, nonces => Nonces
|
|
, seq => Seq
|
|
, edge_bits => EdgeBits }};
|
|
decode_msg_(<<"solutions">>, #{ <<"seq">> := Seq
|
|
, <<"found">> := Found }) ->
|
|
Solutions = lists:map(
|
|
fun(#{ <<"nonce">> := Nonce
|
|
, <<"evidence">> := Evidence }) ->
|
|
#{nonce => Nonce, evidence => Evidence}
|
|
end, Found),
|
|
#{solutions => #{ seq => Seq
|
|
, found => Solutions }};
|
|
decode_msg_(<<"solution_accepted">>, #{<<"seq">> := Seq}) ->
|
|
#{solution_accepted => #{seq => Seq}};
|
|
decode_msg_(<<"no_solution">>, #{ <<"seq">> := Seq
|
|
, <<"nonce">> := Nonce }) ->
|
|
#{no_solution => #{ seq => Seq
|
|
, nonce => Nonce }}.
|
|
|
|
valid(Type, Val) ->
|
|
try true = valid_(Type, Val)
|
|
catch
|
|
error:_ ->
|
|
error({invalid, {Type, Val}})
|
|
end.
|
|
|
|
valid_({list,T}, Ps) -> lists:all(fun(X) -> valid_(T, X) end, Ps);
|
|
valid_(protocol, P) -> is_binary(P);
|
|
valid_(version, V) -> is_binary(V);
|
|
valid_(pubkey, PK) -> ok_tuple(gmser_api_encoder:safe_decode(account_pubkey, PK));
|
|
valid_(seq, Seq) -> pos_integer(Seq);
|
|
valid_(nonce, Nonce) -> pos_integer(Nonce);
|
|
valid_(target, T) -> pos_integer(T);
|
|
valid_(edge_bits, E) -> pos_integer(E);
|
|
valid_(pos_int, I) -> pos_integer(I);
|
|
valid_(string, S) -> is_binary(S);
|
|
valid_(boolean, B) -> is_boolean(B);
|
|
valid_(contract, Id) -> ok_tuple(gmser_api_encoder:safe_decode(contract_pubkey, Id));
|
|
valid_(type, T) -> lists:member(T, [miner, monitor]);
|
|
valid_(solutions, S) -> lists:all(fun(#{nonce := N, evidence := Evd}) ->
|
|
valid_(pos_int, N),
|
|
valid_({list, pos_int}, Evd)
|
|
end, S);
|
|
valid_(nonces, Ns) ->
|
|
case Ns of
|
|
[N] -> pos_integer(N);
|
|
[A,B] -> pos_integer(A) andalso pos_integer(B);
|
|
_ ->
|
|
false
|
|
end;
|
|
valid_(candidate, C) -> ok_tuple(gmser_api_encoder:safe_decode(bytearray, C)).
|
|
|
|
ok_tuple(V) ->
|
|
case V of
|
|
{ok, _} -> true;
|
|
_ -> false
|
|
end.
|
|
|
|
pos_integer(I) ->
|
|
is_integer(I) andalso I >= 0.
|
|
|
|
to_atom(A) when is_atom(A) ->
|
|
A;
|
|
to_atom(B) when is_binary(B) ->
|
|
binary_to_existing_atom(B, utf8).
|