%%% -*- 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).