gmminer/src/gmminer_pow_cuckoo.erl

674 lines
25 KiB
Erlang

%%%-------------------------------------------------------------------
%%% @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),
<<Val:32/big-unsigned-integer-unit:8>> = 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]) || <<B:8>> <= 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(<<Nonce:64/little-unsigned-integer>>),
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, <<Acc/binary, H:Bits>>).