%%%------------------------------------------------------------------- %%% @copyright (C) 2025, QPQ AG %%% @copyright (C) 2019, Aeternity Anstalt %%% @doc %%% A library providing Cuckoo Cycle PoW generation and verification. %%% Using (as an independent OS process) the C/C++ Cuckoo Cycle implementation of %%% John Tromp: https://github.com/tromp/cuckoo %%% White paper: https://github.com/tromp/cuckoo/blob/master/doc/cuckoo.pdf?raw=true %%% %%% We use erlang:open_port/2 to start an OS process that runs this C code. %%% The reasons for using this over os:cmd and erlexec are: %%% - no additional C-based dependency which is Unix-focused and therefore hard to port to Windows %%% - widely tested and multiplatform-enabled solution %%% %%% @end %%%------------------------------------------------------------------- -module(gmminer_pow_cuckoo). -export([config/7, addressed_instances/1, repeats/1, exec/1, extra_args/1, hex_enc_header/1, get_node_size/1 ]). -export([hash_data/1, generate/5, generate_all/5, generate_from_hash/5, generate_from_hash/6, verify/5, verify_proof/4, verify_proof_from_hash/4, get_target/2, test_target/3 ]). -export([ set_edge_bits/2 ]). -export_type([hashable/0, hash/0, exec/0, exec_group/0, extra_args/0, hex_enc_header/0, repeats/0, instance/0, instances/0, edge_bits/0, solution/0, config/0 ]). -ifdef(TEST). -export([verify_proof_/3, solution_to_binary/2 ]). -endif. -include_lib("hut/include/hut.hrl"). -include("gmminer.hrl"). -type hashable() :: gmminer_blake2b_256:hashable(). -type hash() :: gmminer_blake2b_256:hash(). -type nonce() :: gmminer_pow:nonce(). -type int_target() :: gmminer_pow:int_target(). -type sci_target() :: gmminer_pow:sci_target(). -type instance() :: gmminer_pow:instance() | undefined. -type exec() :: binary(). -type exec_group() :: binary(). -type extra_args() :: binary(). -type hex_enc_header() :: boolean(). -type repeats() :: non_neg_integer(). -type edge_bits() :: pos_integer(). -type instances() :: [gmminer_pow:instance()] | undefined. -type solution() :: [integer()]. -type solutions() :: [{nonce(), solution()}]. -type output_parser_fun() :: fun(([string()], state()) -> {ok, term(), term()} | {error, term()}). -record(config, { exec :: exec(), exec_group :: exec_group(), extra_args :: extra_args(), hex_enc_header :: hex_enc_header(), repeats :: repeats(), edge_bits :: edge_bits(), instances :: instances() }). -opaque config() :: #config{}. -record(state, { os_pid :: integer() | undefined, port :: port() | undefined, buffer = [] :: string(), solutions = [] :: solutions(), keep_going = false :: boolean(), target :: sci_target() | undefined, edge_bits :: edge_bits(), parser :: output_parser_fun() }). -type state() :: #state{}. -define(IS_CONFIG(Exec, ExecGroup, ExtraArgs, HexEncHdr, Repeats, EdgeBits, Instances), is_binary(Exec) and is_binary(ExecGroup) and is_binary(ExtraArgs) and is_boolean(HexEncHdr) and (is_integer(Repeats) and (Repeats > 0)) and (is_integer(EdgeBits) and (EdgeBits > 0)) and (is_list(Instances) or (Instances =:= undefined))). %%%============================================================================= %%% API %%%============================================================================= config(Exec, ExecGroup, ExtraArgs, HexEncHdr, Repeats, EdgeBits, Instances) when ?IS_CONFIG(Exec, ExecGroup, ExtraArgs, HexEncHdr, Repeats, EdgeBits, Instances) -> #config{ exec = Exec, exec_group = ExecGroup, extra_args = ExtraArgs, hex_enc_header = HexEncHdr, repeats = Repeats, edge_bits = EdgeBits, instances = Instances}. -spec addressed_instances(config()) -> instances(). addressed_instances(#config{instances = Instances}) -> Instances. -spec repeats(config()) -> repeats(). repeats(#config{repeats = Repeats}) -> Repeats. -spec exec(config()) -> exec(). exec(#config{exec = Exec}) -> Exec. -spec extra_args(config()) -> extra_args(). extra_args(#config{extra_args = ExtraArgs}) -> ExtraArgs. -spec hex_enc_header(config()) -> hex_enc_header(). hex_enc_header(#config{hex_enc_header = HexEncHdr}) -> HexEncHdr. -spec hash_data(hashable()) -> hash(). hash_data(Data) -> gmminer_blake2b_256:hash(Data). -spec set_edge_bits(edge_bits(), config()) -> config(). set_edge_bits(Bits, Config) when is_integer(Bits), Bits > 0, is_record(Config, config) -> Config#config{edge_bits = Bits}. %%------------------------------------------------------------------------------ %% Proof of Work generation with default settings %% %% According to my experiments, increasing the number of trims from the default %% 7 in John Tromp's code does not really affect speed, reducing it causes failure. %% %% Measured execution times (seconds) for 7 trims for threads: %% 1: 44.61 46.92 46.41 %% 2: 15.17 15.75 19.19 %% 3: 9.35 10.92 9.73 %% 4: 10.66 7.83 10.02 %% 5: 7.41 7.47 7.32 %% 10: 7.27 6.70 6.38 %% 20: 6.25 6.74 6.41 %% %% Very slow below 3 threads, not improving significantly above 5, let us take 5. %%------------------------------------------------------------------------------ -spec generate(hashable(), sci_target(), nonce(), config(), instance()) -> {ok, {nonce(), solution()}} | {error, no_solution} | {error, {runtime, term()}}. generate(Data, Target, Nonce, Config, Instance) when Nonce >= 0, Nonce =< ?MAX_NONCE -> Hash = gmminer_blake2b_256:hash(Data), generate_from_hash(Hash, Target, Nonce, Config, Instance). -spec generate_all(hashable(), sci_target(), nonce(), config(), instance()) -> {ok, solutions()} | {error, no_solution} | {error, {runtime, term()}}. generate_all(Data, Target, Nonce, Config, Instance) when Nonce >= 0, Nonce =< ?MAX_NONCE -> Hash = gmminer_blake2b_256:hash(Data), generate_from_hash(Hash, Target, Nonce, Config, Instance, true). -spec generate_from_hash(hash(), sci_target(), nonce(), config(), instance()) -> {ok, {nonce(), solution()}} | {error, no_solution} | {error, {runtime, term()}}. generate_from_hash(Hash, Target, Nonce, Config, Instance) -> generate_from_hash(Hash, Target, Nonce, Config, Instance, false). -spec generate_from_hash(hash(), sci_target(), nonce(), config(), instance(), boolean()) -> {ok, {nonce(), solution()}} | {ok, solutions()} | {error, no_solution} | {error, {runtime, term()}}. generate_from_hash(Hash, Target, Nonce, Config, Instance, KeepGoing) -> Hash64 = base64:encode_to_string(Hash), ?log(debug, "Generating solution for data hash ~p and nonce ~p with target ~p.", [Hash, Nonce, Target]), case generate_int(Hash64, Nonce, Target, Config, Instance, KeepGoing) of {ok, Nonce1, Soln} -> {ok, {Nonce1, Soln}}; {ok, _} = Ok -> Ok; {error, no_value} -> ?log(debug, "No cuckoo solution found", []), {error, no_solution}; {error, Rsn} -> %% Exec failed (segfault, not found, etc.): let miner decide {error, {runtime, Rsn}} end. %%------------------------------------------------------------------------------ %% Proof of Work verification (with difficulty check) %%------------------------------------------------------------------------------ -spec verify(hashable(), nonce(), solution(), sci_target(), edge_bits()) -> boolean(). verify(Data, Nonce, Soln, Target, EdgeBits) when is_list(Soln), Nonce >= 0, Nonce =< ?MAX_NONCE -> case test_target(Soln, Target, EdgeBits) of true -> verify_proof(Data, Nonce, Soln, EdgeBits); false -> false end. %%------------------------------------------------------------------------------ %% @doc %% Proof of Work verification %% @end %%------------------------------------------------------------------------------ -spec verify_proof(hashable(), nonce(), solution(), edge_bits()) -> boolean(). verify_proof(Data, Nonce, Solution, EdgeBits) -> %% Cuckoo has an 80 byte header, we have to use that as well %% packed Hash + Nonce = 56 bytes, add 24 bytes of 0:s Hash = gmminer_blake2b_256:hash(Data), verify_proof_from_hash(Hash, Nonce, Solution, EdgeBits). -spec verify_proof_from_hash(hash(), nonce(), solution(), edge_bits()) -> boolean(). verify_proof_from_hash(Hash, Nonce, Solution, EdgeBits) -> Header0 = pack_header_and_nonce(Hash, Nonce), Header = <<(list_to_binary(Header0))/binary, 0:(8*24)>>, verify_proof_(Header, Solution, EdgeBits). -spec get_target(solution(), edge_bits()) -> int_target(). get_target(Soln, EdgeBits) when is_list(Soln), length(Soln) =:= ?SOLUTION_SIZE -> NodeSize = get_node_size(EdgeBits), Bin = solution_to_binary(lists:sort(Soln), NodeSize * 8, <<>>), Hash = gmminer_blake2b_256:hash(Bin), <> = Hash, Val. %% Internal functions. generate_int(Hash, Nonce, Target, #config{exec = Exec0, extra_args = ExtraArgs0, hex_enc_header = HexEncHdr} = Config, Instance, KeepGoing) -> ExtraArgs = case is_miner_instance_addressation_enabled(Config) of true -> binary_to_list(ExtraArgs0) ++ " -d " ++ integer_to_list(Instance); false -> binary_to_list(ExtraArgs0) end, EncodedHash = case HexEncHdr of true -> hex_string(Hash); false -> Hash end, ExecBinDir = exec_bin_dir(Config), Exec = binary_to_list(Exec0), generate_int(EncodedHash, Nonce, Target, ExecBinDir, Exec, ExtraArgs, Config, KeepGoing). generate_int(Hash, Nonce, Target, MinerBinDir, MinerBin, MinerExtraArgs, #config{repeats = Repeats0, edge_bits = EdgeBits}, KeepGoing) -> Repeats = integer_to_list(Repeats0), Args = ["-h", Hash, "-n", integer_to_list(Nonce), "-r", Repeats | string:tokens(MinerExtraArgs, " ")], ?log(info, "Executing cmd '~s ~s'", [MinerBin, lists:concat(lists:join(" ", Args))]), Old = process_flag(trap_exit, true), try exec_run(MinerBin, MinerBinDir, Args) of {ok, Port, OsPid} -> wait_for_result(#state{os_pid = OsPid, port = Port, buffer = [], target = Target, keep_going = KeepGoing, edge_bits = EdgeBits, parser = fun parse_generation_result/2}); {error, _Rsn} = Err -> Err catch C:E -> {error, {unknown, {C, E}}} after process_flag(trap_exit, Old), receive {'EXIT',_From, shutdown} -> exit(shutdown) after 0 -> ok end end. hex_string(S) -> Bin = list_to_binary(S), lists:flatten([io_lib:format("~2.16.0B", [B]) || <> <= Bin]). is_miner_instance_addressation_enabled(#config{instances = Instances}) -> case Instances of I when is_list(I) -> true; undefined -> false end. exec_bin_dir(#config{exec_group = ExecGroup}) -> case ExecGroup of <<"aecuckoo">> -> aecuckoo:bin_dir(); <<"aecuckooprebuilt">> -> code:priv_dir(aecuckooprebuilt) end. -define(POW_TOO_BIG(Nonce), {error, {nonce_too_big, Nonce}}). -define(POW_TOO_SMALL(Nonce, PrevNonce), {error, {nonces_not_ascending, Nonce, PrevNonce}}). -define(POW_NON_MATCHING, {error, endpoints_do_not_match_up}). -define(POW_BRANCH, {error, branch_in_cycle}). -define(POW_DEAD_END, {error, cycle_dead_ends}). -define(POW_SHORT_CYCLE, {error, cycle_too_short}). verify_proof_(Header, Solution, EdgeBits) -> {K0, K1, K2, K3} = gmminer_siphash24:create_keys(Header), EdgeMask = (1 bsl EdgeBits) - 1, try %% Generate Uv pairs representing endpoints by hashing the proof %% XOR points together: for a closed cycle they must match somewhere %% making one of the XORs zero. {Xor0, Xor1, _, Uvs} = lists:foldl( fun(N, _) when N > EdgeMask -> throw(?POW_TOO_BIG(N)); (N, {_Xor0, _Xor1, PrevN, _Uvs}) when N =< PrevN -> throw(?POW_TOO_SMALL(N, PrevN)); (N, {Xor0C, Xor1C, _PrevN, UvsC}) -> Uv0 = sipnode(K0, K1, K2, K3, N, 0, EdgeMask), Uv1 = sipnode(K0, K1, K2, K3, N, 1, EdgeMask), {Xor0C bxor Uv0, Xor1C bxor Uv1, N, [{Uv0, Uv1} | UvsC]} end, {16#0, 16#0, -1, []}, Solution), case Xor0 bor Xor1 of 0 -> %% check cycle case check_cycle(Uvs) of ok -> true; {error, E} -> throw(E) end; _ -> %% matching endpoints imply zero xors throw(?POW_NON_MATCHING) end catch throw:{error, Rsn} -> ?log(info, "Proof verification failed for ~p: ~p", [Solution, Rsn]), false end. sipnode(K0, K1, K2, K3, Proof, UOrV, EdgeMask) -> SipHash = gmminer_siphash24:hash(K0, K1, K2, K3, 2*Proof + UOrV) band EdgeMask, (SipHash bsl 1) bor UOrV. check_cycle(Nodes0) -> Nodes = lists:keysort(2, Nodes0), {Evens0, Odds} = lists:unzip(Nodes), Evens = lists:sort(Evens0), %% Odd nodes are already sorted... UEvens = lists:usort(Evens), UOdds = lists:usort(Odds), %% Check that all nodes appear exactly twice (i.e. each node has %% exactly two edges). case length(UEvens) == (?SOLUTION_SIZE div 2) andalso length(UOdds) == (?SOLUTION_SIZE div 2) andalso UOdds == Odds -- UOdds andalso UEvens == Evens -- UEvens of false -> {error, ?POW_BRANCH}; true -> [{X0, Y0}, {X1, Y0} | Nodes1] = Nodes, check_cycle(X0, X1, Nodes1) end. %% If we reach the end in the last step everything is fine check_cycle(X, X, []) -> ok; %% If we reach the end too early the cycle is too short check_cycle(X, X, _) -> {error, ?POW_SHORT_CYCLE}; check_cycle(XEnd, XNext, Nodes) -> %% Find the outbound edge for XNext and follow that edge %% to an odd node and back again to NewXNext case find_node(XNext, Nodes, []) of Err = {error, _} -> Err; {XNext, NewXNext, NewNodes} -> check_cycle(XEnd, NewXNext, NewNodes) end. find_node(_, [], _Acc) -> {error, ?POW_DEAD_END}; find_node(X, [{X, Y}, {X1, Y} | Nodes], Acc) -> {X, X1, Nodes ++ Acc}; find_node(X, [{X1, Y}, {X, Y} | Nodes], Acc) -> {X, X1, Nodes ++ Acc}; find_node(X, [{X, _Y} | _], _Acc) -> {error, ?POW_DEAD_END}; find_node(X, [N1, N2 | Nodes], Acc) -> find_node(X, Nodes, [N1, N2 | Acc]). %%------------------------------------------------------------------------------ %% @doc %% Creates the Cuckoo buffer (hex encoded) from a base64-encoded hash and a %% uint64 nonce. %% Since this hash is purely internal, we don't use api encoding. %% @end %%------------------------------------------------------------------------------ -spec pack_header_and_nonce(binary(), gmminer_pow:nonce()) -> string(). pack_header_and_nonce(Hash, Nonce) when byte_size(Hash) == 32 -> %% Cuckoo originally uses 32-bit nonces inserted at the end of its 80-byte buffer. %% This buffer is hashed into the keys used by the main algorithm. %% %% We insert our 64-bit Nonce right after the hash of the block header We %% base64-encode both the hash of the block header and the nonce and pass %% the resulting command-line friendly string with the -h option to Cuckoo. %% %% The SHA256 hash is 32 bytes (44 chars base64-encoded), the nonce is 8 bytes %% (12 chars base64-encoded). That leaves plenty of room (80 - 56 = 24 %% bytes) for cuckoo to put its nonce (which will be 0 in our case) in. %% %% (Base64 encoding: see RFC 3548, Section 3: %% https://tools.ietf.org/html/rfc3548#page-4 %% converts every triplet of bytes to 4 characters: from N bytes to 4*ceil(N/3) %% bytes.) %% %% Like Cuckoo, we use little-endian for the nonce here. NonceStr = base64:encode_to_string(<>), HashStr = base64:encode_to_string(Hash), %% Cuckoo will automatically fill bytes not given with -h option to 0, thus %% we need only return the two base64 encoded strings concatenated. %% 44 + 12 = 56 bytes HashStr ++ NonceStr. %%------------------------------------------------------------------------------ %% @doc %% Receive and process notifications about the fate of the process and its %% output. The receieved stdout tends to be in large chunks, we keep a buffer %% for the last line fragment w/o NL. %% @end %%------------------------------------------------------------------------------ wait_for_result(#state{os_pid = OsPid, port = Port, buffer = Buffer} = State) -> receive {Port, {data, Msg}} -> Str = binary_to_list(Msg), {Lines, NewBuffer} = handle_fragmented_lines(Str, Buffer), (State#state.parser)(Lines, State#state{buffer = NewBuffer}); {Port, {exit_status, 0}} -> wait_for_result(State); {'EXIT',_From, shutdown} -> %% Someone is telling us to stop stop_execution(OsPid), exit(shutdown); {'EXIT', Port, normal} -> %% Process ended but no value found case State#state.solutions of [] -> {error, no_value}; [_|_] = Sols0 -> Sols = lists:reverse(Sols0), case State#state.keep_going of false -> %% For compatibility, pick the one first found [{N, S}|_] = Sols, {ok, N, S}; true -> {ok, Sols} end end; _Other -> wait_for_result(State) end. %%------------------------------------------------------------------------------ %% @doc %% Prepend the first new incoming line with the last line fragment stored %% in Buffer and replace Buffer with the possible new line fragment at the %% end of Str. %% @end %%------------------------------------------------------------------------------ handle_fragmented_lines(Str, Buffer) -> Lines = string:tokens(Str, "\n"), %% Add previous truncated line if present to first line Lines2 = case Buffer of [] -> Lines; _ -> [Line1 | More] = Lines, [Buffer ++ Line1 | More] end, %% Keep last fraction (w/o NL) in buffer case lists:last(Str) of $\n -> {Lines2, ""}; _ -> {L3, [Bf]} = lists:split(length(Lines) - 1, Lines2), {L3, Bf} end. %%------------------------------------------------------------------------------ %% @doc %% Parse miner output %% @end %%------------------------------------------------------------------------------ parse_generation_result([], State) -> wait_for_result(State); parse_generation_result(["Solution" ++ NonceValuesStr | Rest], #state{os_pid = OsPid, edge_bits = EdgeBits, target = Target, keep_going = KeepGoing} = State) -> [NonceStr | SolStrs] = string:tokens(NonceValuesStr, " "), Soln = [list_to_integer(string:trim(V, both, [$\r]), 16) || V <- SolStrs], case {length(Soln), test_target(Soln, Target, EdgeBits)} of {?SOLUTION_SIZE, true} -> ?log(debug, "Solution found, KeepGoing = ~p", [KeepGoing]), case KeepGoing of true -> ok; false -> stop_execution(OsPid) end, case parse_nonce_str(NonceStr) of {ok, Nonce} -> ?log(debug, "Solution found: ~p", [Soln]), case KeepGoing of true -> Sols = State#state.solutions, State1 = State#state{solutions = [{Nonce, Soln}|Sols]}, parse_generation_result(Rest, State1); false -> {ok, Nonce, Soln} end; Err = {error, _} -> ?log(debug, "Bad nonce: ~p", [Err]), Err end; {N, _} when N /= ?SOLUTION_SIZE -> ?log(debug, "Solution has wrong length (~p) should be ~p", [N, ?SOLUTION_SIZE]), %% No nonce in solution, old miner exec? stop_execution(OsPid), {error, bad_miner}; {_, false} -> %% failed to meet target: go on, we may find another solution ?log(debug, "Failed to meet target (~p)", [Target]), parse_generation_result(Rest, State) end; parse_generation_result([_Msg | T], State) -> parse_generation_result(T, State). parse_nonce_str(S) -> try {ok, list_to_integer(string:trim(S, both, "()"), 16)} catch _:_ -> {error, bad_nonce} end. %%------------------------------------------------------------------------------ %% @doc %% Stop the OS process %% @end %%------------------------------------------------------------------------------ stop_execution(OsPid) -> exec_kill(OsPid), ?log(debug, "Mining OS process ~p stopped", [OsPid]), ok. %%------------------------------------------------------------------------------ %% @doc %% The Cuckoo solution is a list of uint32 integers unless the graph size is %% greater than 33 (when it needs u64 to store). Hash result for difficulty %% control accordingly. %% @end %% Refs: %% * https://github.com/tromp/cuckoo/blob/488c03f5dbbfdac6d2d3a7e1d0746c9a7dafc48f/src/Makefile#L214-L215 %% * https://github.com/tromp/cuckoo/blob/488c03f5dbbfdac6d2d3a7e1d0746c9a7dafc48f/src/cuckoo.h#L26-L30 %%------------------------------------------------------------------------------ -spec get_node_size(pos_integer()) -> non_neg_integer(). get_node_size(EdgeBits) when is_integer(EdgeBits), EdgeBits > 31 -> 8; get_node_size(EdgeBits) when is_integer(EdgeBits), EdgeBits > 0 -> 4. exec_run(Cmd, Dir, Args) -> PortSettings = [binary, exit_status, hide, in, overlapped_io, stderr_to_stdout, {args, Args}, {cd, Dir} ], PortName = {spawn_executable, os:find_executable(Cmd, Dir)}, try Port = erlang:open_port(PortName, PortSettings), case erlang:port_info(Port, os_pid) of {os_pid, OsPid} -> ?log(debug, "External mining process started with OS pid ~p", [OsPid]), {ok, Port, OsPid}; undefined -> ?log(warning, "External mining process finished before ~p could acquire the OS pid", [?MODULE]), {ok, Port, undefined} end catch C:E -> {error, {port_error, {C, E}}} end. exec_kill(undefined) -> ok; exec_kill(OsPid) -> case is_unix() of true -> os:cmd(io_lib:format("kill -9 ~p", [OsPid])), ok; false -> os:cmd(io_lib:format("taskkill /PID ~p /T /F", [OsPid])), ok end. is_unix() -> case erlang:system_info(system_architecture) of "win32" -> false; _ -> true end. %%------------------------------------------------------------------------------ %% White paper, section 9: rather than adjusting the nodes/edges ratio, a %% hash-based target is suggested: the sha256 hash of the cycle nonces %% is restricted to be under the target value (0 < target < 2^256). %%------------------------------------------------------------------------------ test_target(Soln, Target, EdgeBits) -> test_target1(Soln, Target, get_node_size(EdgeBits)). test_target1(Soln, Target, NodeSize) -> Bin = solution_to_binary(lists:sort(Soln), NodeSize * 8), Hash = gmminer_blake2b_256:hash(Bin), gmminer_pow:test_target(Hash, Target). %%------------------------------------------------------------------------------ %% Convert solution (a list of 42 numbers) to a binary %% in a languauge-independent way %%------------------------------------------------------------------------------ solution_to_binary(Soln, Bits) -> solution_to_binary(Soln, Bits, <<>>). solution_to_binary([], _Bits, Acc) -> Acc; solution_to_binary([H | T], Bits, Acc) -> solution_to_binary(T, Bits, <>).