From 3a061f057c1a3d589a3f6101d50b7ef7477ddad8 Mon Sep 17 00:00:00 2001 From: Ulf Wiger Date: Thu, 6 Mar 2025 17:46:02 +0100 Subject: [PATCH] First commit --- rebar.config | 13 ++ src/gm_mining_pool_protocol.app.src | 17 ++ src/gmmpp_msgs.erl | 230 ++++++++++++++++++++++++++++ 3 files changed, 260 insertions(+) create mode 100644 rebar.config create mode 100644 src/gm_mining_pool_protocol.app.src create mode 100644 src/gmmpp_msgs.erl diff --git a/rebar.config b/rebar.config new file mode 100644 index 0000000..03988b9 --- /dev/null +++ b/rebar.config @@ -0,0 +1,13 @@ +%% -*- mode: erlang; erlang-indent-level: 4; indent-tabs-mode: nil -*- +{erl_opts, [debug_info]}. +{plugins, [rebar3_hex]}. +{deps, [ + {ranch, "2.2.0"}, + {enoise, {git, "https://git.qpq.swiss/QPQ-AG/enoise.git", {ref, "8acbce9"}}} + ]}. + +{xref_checks, [undefined_function_calls, undefined_functions, + locals_not_used, + deprecated_function_calls, deprecated_functions]}. + +{dialyzer, [{warnings, [unknown]}]}. diff --git a/src/gm_mining_pool_protocol.app.src b/src/gm_mining_pool_protocol.app.src new file mode 100644 index 0000000..80bdfb6 --- /dev/null +++ b/src/gm_mining_pool_protocol.app.src @@ -0,0 +1,17 @@ +%% -*- mode: erlang; erlang-indent-level: 4; indent-tabs-mode: nil -*- +{application, gm_mining_pool_protocol, + [{description, "Gajumaru Mining Pool Protocol (server- + client-side)"}, + {vsn, "0.1.0"}, + {registered, []}, + {application, + [ + kernel + , stdlib + , gmserialization + ]}, + {env, []}, + {modules, []}, + {maintainers, ["QPQ IaaS AG"]}, + {licensens, ["ISC"]}, + {links, [{"gitea", "https://git.qpq.swiss/gm_mining_pool_protocol"}]} + ]}. diff --git a/src/gmmpp_msgs.erl b/src/gmmpp_msgs.erl new file mode 100644 index 0000000..68cc6ce --- /dev/null +++ b/src/gmmpp_msgs.erl @@ -0,0 +1,230 @@ +%%% -*- 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 + , 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, pool_ws_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 }} = Msg, _Vsn) -> + valid(pubkey, PK), + Msg; +validate(#{ connect_ack := #{ pubkey := PK + , edge_bits := EB }} = Msg, _Vsn) -> + valid(pubkey, PK), + valid(edgebits, EB), + Msg; +validate(#{ nonces := #{ seq := Seq + , n := N } } = Msg, _Vsn) -> + valid(seq, Seq), + valid(pos_int, N), + Msg; +validate(#{ candidate := #{ candidate := C + , nonces := Nonces + , seq := Seq } } = Msg, _Vsn) -> + valid(candidate, C), + valid(nonces, Nonces), + valid(seq, Seq), + Msg; +validate(#{ solution := #{ seq := Seq + , nonce := Nonce + , evidence := Evidence } } = Msg, _Vsn) -> + valid(seq, Seq), + valid(nonce, Nonce), + valid(evidence, Evidence), + Msg; +validate(#{ solution_ack := #{ 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 + , 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 => #{ msg => validate(decode_msg_(Method, Params), Vsn) }}; + #{ <<"id">> := Id + , <<"result">> := Result } -> + #{ reply => #{ id => Id + , result => validate(decode_result(Result), Vsn) }}; + #{ <<"id">> := Id + , <<"error">> := #{ <<"code">> := Code + , <<"message">> := Message }} -> + #{ error => #{ id => Id + , code => Code + , message => Message }} + end + end. + +encode_msg(Msg0, ?PROTOCOL_JSON, Vsn) -> + Msg = validate(Msg0, Vsn), + [{Method, Args}] = maps:to_list(Msg), + json:encode(#{ <<"jsonrpc">> => <<"2.0">> + , <<"method">> => Method + , <<"args">> => 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 + , <<"args">> => Args }). + +encode_reply(Reply0, Id, ?PROTOCOL_JSON, Vsn) -> + Reply = validate(Reply0, Vsn), + Msg = case Reply of + {error, Reason} -> + #{ <<"jsonrpc">> => <<"2.0">> + , <<"id">> => Id + , <<"error">> => #{ <<"code">> => error_code(Reason) + , <<"message">> => Reason }}; + Result -> + #{ <<"jsonrpc">> => <<"2.0">> + , <<"id">> => Id + , <<"result">> => Result } + end, + json:encode(Msg). + +error_code(mining_disabled ) -> -32000; +error_code(pool_exists ) -> -32001; +error_code(pool_not_found ) -> -32002; +error_code(unknown_contract ) -> -32003; +error_code(invalid_prefix ) -> -32004; +error_code(invalid_encoding ) -> -32005; +error_code(outdated ) -> -32006; +error_code(solution_mismatch) -> -32007; +error_code(unknown_method ) -> -32601; +error_code(_ ) -> -32603. % internal error + +decode_result(<<"ok">>) -> ok; +decode_result(#{<<"nonces">> := Nonces}) -> #{nonces => Nonces}. + +%% Mapping types +decode_msg_(<<"connect">>, #{ <<"pubkey">> := PK }) -> + #{connect => #{pubkey => PK}}; +decode_msg_(<<"nonces">>, #{ <<"seq">> := Seq + , <<"n">> := N }) -> + #{nonces => #{seq => Seq, n => N}}; +decode_msg_(<<"candidate">>, #{ <<"candidate">> := C + , <<"nonces">> := Nonces + , <<"seq">> := Seq }) -> + #{candidate => #{ candidate => C + , nonces => Nonces + , seq => Seq }}; +decode_msg_(<<"solution">>, #{ <<"seq">> := Seq + , <<"nonces">> := Nonces + , <<"evidence">> := Evidence }) -> + #{solution => #{ seq => Seq + , nonce => Nonces + , seq => Seq + , evidence => Evidence}}. + +valid(Type, Val) -> + try valid_(Type, Val) + catch + error:_ -> + false + end. + +valid_(pubkey, PK) -> ok_tuple(aeser_api:safe_decode(account_pubkey, PK)); +valid_(seq, Seq) -> pos_integer(Seq); +valid_(edgebits, E) -> pos_integer(E); +valid_(pos_int, I) -> pos_integer(I); +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(aeser_api:safe_decode(bytearray, C)). + +ok_tuple(V) -> + case V of + {ok, _} -> true; + _ -> false + end. + +pos_integer(I) -> + is_integer(I) andalso I >= 0.