diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b0f206 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +_build +c_src +priv +ebin diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..25a7179 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +REBAR = ./rebar3 + +.PHONY: all dialyzer ct eunit clean distclean console + +all: + $(REBAR) compile + +doc: + $(REBAR) doc + +dialyzer: + $(REBAR) dialyzer + +ct: all + $(REBAR) ct --suite=test/aecuckoo_SUITE + +eunit: + $(REBAR) eunit --module=aeminer_pow_tests,aeminer_pow_cuckoo_tests + +clean: + $(REBAR) clean + rm -rf doc/* + +distclean: clean + rm -rf _build + +console: + $(REBAR) shell + diff --git a/include/aeminer.hrl b/include/aeminer.hrl new file mode 100644 index 0000000..3091916 --- /dev/null +++ b/include/aeminer.hrl @@ -0,0 +1,11 @@ + +-define(HIGHEST_TARGET_SCI, 16#2100ffff). + +-define(HIGHEST_TARGET_INT, 16#ffff000000000000000000000000000000000000000000000000000000000000). + +-define(DIFFICULTY_INTEGER_FACTOR, 16#1000000). + +-define(MAX_NONCE, 16#ffffffffffffffff). + +-define(SOLUTION_SIZE, 42). + diff --git a/rebar.config b/rebar.config new file mode 100644 index 0000000..0dbdacf --- /dev/null +++ b/rebar.config @@ -0,0 +1,32 @@ + +{erl_opts, [{parse_transform, lager_transform}]}. + +{deps, [ + {lager, {git, "https://github.com/erlang-lager/lager.git", + {ref, "69b4ada"}}}, % tag: 3.6.7 + + %% Cuckoo prebuilt CUDA binaries. + {aecuckooprebuilt, + {aecuckooprebuilt_app_with_priv_from_git, {git, "https://github.com/aeternity/cuckoo-prebuilt.git", + {ref, "90afb699dc9cc41d033a7c8551179d32b3bd569d"}}}}, + + %% Cuckoo CPU miners (not prebuilt). + {aecuckoo, {git, "https://github.com/aeternity/aecuckoo.git", + {ref, "fa3d13e"}}}, + + %% This is used just in one place, just to get blake2b_256 hash. + {enacl, {git, "https://github.com/aeternity/enacl.git", + {ref, "26180f4"}}} + ]}. + +{plugins, [{rebar_aecuckooprebuilt_dep, {git, "https://github.com/aeternity/rebar3-cuckoo-prebuilt-plugin.git", + {ref, "2b2f3b3cf969ee91ba41d8351f3808530a8bf28e"}}} + ]}. + +{profiles, [{test, [{deps, [{meck, "0.8.12"}]}]}]}. + +{dialyzer, [{warnings, [unknown]}, + {plt_apps, all_deps}, + {base_plt_apps, [erts, kernel, stdlib, crypto]} + ]}. + diff --git a/rebar.lock b/rebar.lock new file mode 100644 index 0000000..db8521c --- /dev/null +++ b/rebar.lock @@ -0,0 +1,23 @@ +{"1.1.0", +[{<<"aecuckoo">>, + {git,"https://github.com/aeternity/aecuckoo.git", + {ref,"fa3d13e8c7003589153223f634c851d389b61b93"}}, + 0}, + {<<"aecuckooprebuilt">>, + {aecuckooprebuilt_app_with_priv_from_git, + {git,"https://github.com/aeternity/cuckoo-prebuilt.git", + {ref,"90afb699dc9cc41d033a7c8551179d32b3bd569d"}}}, + 0}, + {<<"enacl">>, + {git,"https://github.com/aeternity/enacl.git", + {ref,"26180f42c0b3a450905d2efd8bc7fd5fd9cece75"}}, + 0}, + {<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1}, + {<<"lager">>, + {git,"https://github.com/erlang-lager/lager.git", + {ref,"69b4ada2341b8ab2cf5c8e464ac936e5e4a9f62b"}}, + 0}]}. +[ +{pkg_hash,[ + {<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>}]} +]. diff --git a/rebar3 b/rebar3 new file mode 100755 index 0000000..e3a5ab0 Binary files /dev/null and b/rebar3 differ diff --git a/src/aeminer.app.src b/src/aeminer.app.src new file mode 100644 index 0000000..9a979e4 --- /dev/null +++ b/src/aeminer.app.src @@ -0,0 +1,18 @@ +{application, aeminer, + [{description, "Aeternity miner"}, + {vsn, "1.0.0"}, + {registered, []}, + {applications, + [kernel, + stdlib, + lager, + enacl, + aecuckoo, + aecuckooprebuilt + ]}, + {env,[]}, + {modules, []}, + {maintainers, []}, + {licenses, []}, + {links, []} + ]}. diff --git a/src/aeminer_blake2b_256.erl b/src/aeminer_blake2b_256.erl new file mode 100644 index 0000000..9b60f60 --- /dev/null +++ b/src/aeminer_blake2b_256.erl @@ -0,0 +1,17 @@ +-module(aeminer_blake2b_256). + +-export([hash/1]). + +-export_type([hashable/0, + hash/0 + ]). + +-type hashable() :: binary(). + +-type hash() :: binary(). + +-spec hash(hashable()) -> hash(). +hash(Bin) -> + {ok, Hash} = enacl:generichash(32, Bin), + Hash. + diff --git a/src/aeminer_pow.erl b/src/aeminer_pow.erl new file mode 100644 index 0000000..f114899 --- /dev/null +++ b/src/aeminer_pow.erl @@ -0,0 +1,172 @@ +%%%============================================================================= +%%% @copyright 2019, Aeternity Anstalt +%%% @doc +%%% Common functions and behaviour for Proof of Work +%%% @end +%%%============================================================================= +-module(aeminer_pow). + +-export([integer_to_scientific/1, + next_nonce/2, + pick_nonce/0, + scientific_to_integer/1, + target_to_difficulty/1, + test_target/2, + trim_nonce/2]). + +-export_type([nonce/0, + int_target/0, + sci_target/0, + bin_target/0, + difficulty/0, + instance/0 + ]). + +-include("aeminer.hrl"). + +-type nonce() :: 0..?MAX_NONCE. + +-type int_target() :: integer(). + +-type sci_target() :: integer(). + +-type bin_target() :: <<_:256>>. + +%% Difficulty: max threshold (0x00000000FFFF0000000000000000000000000000000000000000000000000000) +%% over the actual one. Always positive. +-type difficulty() :: integer(). + +-type instance() :: non_neg_integer(). + +-type config() :: aeminer_pow_cuckoo:config(). + +%% 10^24, approx. 2^80 +-define(NONCE_RANGE, 1000000000000000000000000). + +%%------------------------------------------------------------------------------ +%% Target threshold and difficulty +%% +%% The mining rate is controlled by setting a target threshold. The PoW nonce +%% is accepted if a hash value (the hash of the header for SHA-256, the hash of +%% the solution graph for Cuckoo Cycleas, converted to an integer) is below +%% this target threshold. +%% +%% A lower target represents a harder task (requiers the hash to start with a +%% number of zeros). +%% +%% The target thershold relates to another value: the diifculty. This is +%% proportional to the hardness of the PoW task: +%% +%% Difficulty = / Target, +%% +%% a floating point value. +%% Bitcoin uses 0x00000000FFFF0000000000000000000000000000000000000000000000000000 +%% as Difficulty 1 target (0x1d00ffff in scientific notation, see below). For +%% Cuckoo Cycle we need a lighter filtering of solutions than for SHA-256 as the +%% basic algorithm is much slower than a simple hash generation, so we use the +%% largest possible value: +%% 0xFFFF000000000000000000000000000000000000000000000000000000000000 (0x2100ffff +%% in scientific notation) as difficulty 1. +%% +%% We store the current target threshold in the block header in scientific notation. +%% Difficulty is used to select the winning fork of new blocks: the difficulty of a +%% chain of blocks is the sum of the diffculty of each block. +%% +%% Integers represented in scientific notation: +%% 2^24 * + the first 3 most significant bytes (i.e., +%% the significand, see https://en.wikipedia.org/wiki/Significand). +%% The + 3 corresponds to the length of the +%% significand (i.e., the int value is 0. * 8^). +%% https://en.bitcoin.it/wiki/Difficulty#How_is_difficulty_stored_in_blocks.3F) +%%------------------------------------------------------------------------------ + +%%%============================================================================= +%%% API +%%%============================================================================= + +-spec scientific_to_integer(sci_target()) -> int_target(). +scientific_to_integer(S) -> + {Exp, Significand} = break_up_scientific(S), + E3 = Exp - 3, + case Exp >= 0 of + true -> + Significand bsl (8 * E3); + false -> + Significand bsr (-8 * E3) + end. + +-spec integer_to_scientific(int_target()) -> sci_target(). +integer_to_scientific(I) -> + %% Find exponent and significand + {Exp, Significand} = integer_to_scientific(I, 3), + case Exp >= 0 of + true -> + %% 1st byte: exponent, next 3 bytes: significand + (Exp bsl 24) + Significand; + false -> + %% flip sign bit in significand + ((-Exp) bsl 24) + 16#800000 + Significand + end. + +%% We want difficulty to be an integer, to still have enough precision using +%% integer division we multiply by K (1 bsl 24). +-spec target_to_difficulty(sci_target()) -> difficulty(). +target_to_difficulty(SciTgt) -> + (?DIFFICULTY_INTEGER_FACTOR * ?HIGHEST_TARGET_INT) + div scientific_to_integer(SciTgt). + +-spec next_nonce(nonce(), config()) -> nonce(). +next_nonce(Nonce, Cfg) -> + Nonce + aeminer_pow_cuckoo:repeats(Cfg). + +-spec pick_nonce() -> nonce(). +pick_nonce() -> + rand:uniform(?NONCE_RANGE) band ?MAX_NONCE. + +-spec trim_nonce(nonce(), config()) -> nonce(). +trim_nonce(Nonce, Cfg) -> + case Nonce + aeminer_pow_cuckoo:repeats(Cfg) < ?MAX_NONCE of + true -> Nonce; + false -> 0 + end. + +%%------------------------------------------------------------------------------ +%% Test if binary is under the target threshold +%%------------------------------------------------------------------------------ +-spec test_target(bin_target(), sci_target()) -> boolean(). +test_target(Bin, Target) -> + Threshold = scientific_to_integer(Target), + <> = Bin, + Val < Threshold. + +%% TODO: get target +%Bin = solution_to_binary(lists:sort(Soln), NodeSize * 8, <<>>), +%%Hash = aec_hash:hash(pow, Bin), +%%<> = Hash, +%%Val + +%%%============================================================================= +%%% Internal functions +%%%============================================================================= + +integer_to_scientific(I, Exp) when I > 16#7fffff -> + integer_to_scientific(I bsr 8, Exp + 1); +integer_to_scientific(I, Exp) when I < 16#008000, I > 16#000000 -> + integer_to_scientific(I bsl 8, Exp - 1); +integer_to_scientific(I, Exp) -> + %% Add the number of bytes in the significand + {Exp, I}. + +%% Return the exponent and significand of a sci_target(). +break_up_scientific(S) -> + SigMask = (1 bsl 24) - 1, + Exp = ((S bxor SigMask) bsr 24), + Significand = S band SigMask, + %% Remove the sign bit, apply to exponent + case 16#800000 band Significand of + 0 -> + {Exp, Significand}; + _ -> + {-Exp, Significand - 16#800000} + end. + diff --git a/src/aeminer_pow_cuckoo.erl b/src/aeminer_pow_cuckoo.erl new file mode 100644 index 0000000..570ba8c --- /dev/null +++ b/src/aeminer_pow_cuckoo.erl @@ -0,0 +1,589 @@ +%%%------------------------------------------------------------------- +%%% @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(aeminer_pow_cuckoo). + +-export([config/7, + addressed_instances/1, + repeats/1, + exec/1, + extra_args/1, + hex_enc_header/1, + get_node_size/1 + ]). + +-export([generate/5, + verify/5 + ]). + +-export_type([hashable/0, + exec/0, + exec_group/0, + extra_args/0, + hex_enc_header/0, + repeats/0, + edge_bits/0, + solution/0, + config/0 + ]). + +-ifdef(TEST). +-export([verify_proof_/3, + solution_to_binary/2 + ]). +-endif. + +-include("aeminer.hrl"). + +-type hashable() :: aeminer_blake2b_256:hashable(). + +-type nonce() :: aeminer_pow:nonce(). + +-type sci_target() :: aeminer_pow:sci_target(). + +-type instance() :: aeminer_pow:instance() + | undefined. + +-type exec() :: string(). + +-type exec_group() :: binary(). + +-type extra_args() :: string(). + +-type hex_enc_header() :: boolean(). + +-type repeats() :: non_neg_integer(). + +-type edge_bits() :: pos_integer(). + +-type instances() :: [aeminer_pow:instance()] + | undefined. + +-type solution() :: [integer()]. + +-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(), + 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))). + +-define(LOG_MODULE, application:get_env(aeminer, log_module)). + +-define(debug(F, A), lager:debug(F, A)). +-define(info(F, A), lager:info(F, A)). +-define(warning(F, A), lager:warning(F, A)). +-define(error(F, A), lager:error(F, A)). + +%%%============================================================================= +%%% API +%%%============================================================================= + +config(Exec, ExecGroup, ExtraArgs, HexEncHdr, Repeats, EdgeBits, Instances) when + ?IS_CONFIG(Exec, ExecGroup, ExtraArgs, HexEncHdr, Repeats, EdgeBits, Instances) -> + #config{ + exec = binary_to_list(Exec), + exec_group = ExecGroup, + extra_args = binary_to_list(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. + +%%------------------------------------------------------------------------------ +%% 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 Data and convert the resulting binary to a base64 string for Cuckoo + %% Since this hash is purely internal, we don't use api encoding + Hash = aeminer_blake2b_256:hash(Data), + Hash64 = base64:encode_to_string(Hash), + ?debug("Generating solution for data hash ~p and nonce ~p with target ~p.", + [Hash, Nonce, Target]), + case generate_int(Hash64, Nonce, Target, Config, Instance) of + {ok, Nonce1, Soln} -> + {ok, {Nonce1, Soln}}; + {error, no_value} -> + ?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 -> + Hash = aeminer_blake2b_256:hash(Data), + case test_target(Soln, Target, EdgeBits) of + true -> + verify_proof(Hash, Nonce, Soln, EdgeBits); + false -> + false + end. + +%% Internal functions. + +generate_int(Hash, Nonce, Target, + #config{exec = Exec, extra_args = ExtraArgs0, + hex_enc_header = HexEncHdr} = Config, Instance) -> + ExtraArgs = case is_miner_instance_addressation_enabled(Config) of + true -> ExtraArgs0 ++ " -d " ++ integer_to_list(Instance); + false -> ExtraArgs0 + end, + EncodedHash = case HexEncHdr of + true -> hex_string(Hash); + false -> Hash + end, + ExecBinDir = exec_bin_dir(Config), + generate_int(EncodedHash, Nonce, Target, ExecBinDir, Exec, ExtraArgs, Config). + +generate_int(Hash, Nonce, Target, MinerBinDir, MinerBin, MinerExtraArgs, + #config{repeats = Repeats0, edge_bits = EdgeBits}) -> + Repeats = integer_to_list(Repeats0), + Args = ["-h", Hash, "-n", integer_to_list(Nonce), "-r", Repeats | string:tokens(MinerExtraArgs, " ")], + ?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, + 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}). + +%%------------------------------------------------------------------------------ +%% @doc +%% Proof of Work verification (difficulty check should be done before calling +%% this function) +%% @end +%%------------------------------------------------------------------------------ + +verify_proof(Hash, 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 + Header0 = pack_header_and_nonce(Hash, Nonce), + Header = <<(list_to_binary(Header0))/binary, 0:(8*24)>>, + verify_proof_(Header, Solution, EdgeBits). + +verify_proof_(Header, Solution, EdgeBits) -> + {K0, K1, K2, K3} = aeminer_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} -> + ?info("Proof verification failed for ~p: ~p", [Solution, Rsn]), + false + end. + +sipnode(K0, K1, K2, K3, Proof, UOrV, EdgeMask) -> + SipHash = aeminer_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(), aeminer_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 + {error, no_value}; + _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} = 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 + {42, true} -> + stop_execution(OsPid), + case parse_nonce_str(NonceStr) of + {ok, Nonce} -> + ?debug("Solution found: ~p", [Soln]), + {ok, Nonce, Soln}; + Err = {error, _} -> + ?debug("Bad nonce: ~p", [Err]), + Err + end; + {N, _} when N /= 42 -> + ?debug("Solution has wrong length (~p) should be 42", [N]), + %% 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 + ?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), + ?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} -> + ?debug("External mining process started with OS pid ~p", [OsPid]), + {ok, Port, OsPid}; + undefined -> + ?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 = aeminer_blake2b_256:hash(Bin), + aeminer_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, <>). + diff --git a/src/aeminer_siphash24.erl b/src/aeminer_siphash24.erl new file mode 100644 index 0000000..33246ad --- /dev/null +++ b/src/aeminer_siphash24.erl @@ -0,0 +1,100 @@ +%%%============================================================================= +%%% @copyright (C) 2019, Aeternity Anstalt +%%% @doc +%%% SipHash-2-4 without standard IV xor and specialized to precomputed key and 8 byte nonces +%%% @end +%%%============================================================================= + +-module(aeminer_siphash24). + +-export([create_keys/1, + hash/5]). + +-ifdef(TEST). +-compile([export_all, nowarn_export_all]). +-endif. + +-define(MAX64, 16#ffffffffffffffff). + +-type hashable() :: integer(). +-type siphash_key() :: integer(). +-type sip_quadruple() :: {integer(), integer(), integer(), integer()}.%% in fact, uint64 + +-export_type([hashable/0, + siphash_key/0]). + +%%%============================================================================= +%%% API +%%%============================================================================= + +-spec create_keys(binary()) -> + {aeminer_siphash24:siphash_key(), + aeminer_siphash24:siphash_key(), + aeminer_siphash24:siphash_key(), + aeminer_siphash24:siphash_key()}. +create_keys(Header) -> + AuxHash = <<_:32/binary>> = aeminer_blake2b_256:hash(Header), + <> = AuxHash, + {K0, K1, K2, K3}. + +-spec hash(siphash_key(), siphash_key(), siphash_key(), siphash_key(), hashable()) -> hashable(). +hash(K0, K1, K2, K3, Nonce) -> + V0 = K0, + V1 = K1, + V2 = K2, + V3 = K3 bxor Nonce, + {V01, V11, V21, V31} = + sip_round(sip_round(sip_round(sip_round(sip_change(Nonce, sip_round(sip_round({V0, V1, V2, V3}))))))), + rotl64(((V01 bxor V11) bxor (V21 bxor V31)), 17) band 16#ffffffffffffffff. + + +%%%============================================================================= +%%% Internal functions +%%%============================================================================= + +%% 1: +%% v0 += v1; v2 += v3; v1 = ROTL(v1,13); \ +%% v3 = ROTL(v3,16); + +%% 2: +%% v1 ^= v0; v3 ^= v2; \ +%% v0 = ROTL(v0,32); + +%% 3: +%% v2 += v1; v0 += v3; \ +%% v1 = ROTL(v1,17); v3 = ROTL(v3,21); \ + +%% 4: +%% v1 ^= v2; v3 ^= v0; v2 = ROTL(v2,32); \ + +-spec sip_round(sip_quadruple()) -> sip_quadruple(). +sip_round({_V0, _V1, _V2, _V3} = Vs) -> + sip_round4(sip_round3(sip_round2(sip_round1(Vs)))). + +-spec sip_round1(sip_quadruple()) -> sip_quadruple(). +sip_round1({V0, V1, V2, V3}) -> + {(V0 + V1) band ?MAX64, rotl64(V1, 13), (V2 + V3) band ?MAX64, rotl64(V3, 16)}. + +-spec sip_round2(sip_quadruple()) -> sip_quadruple(). +sip_round2({V0, V1, V2, V3}) -> + {rotl64(V0, 32), V1 bxor V0, V2, V3 bxor V2}. + +-spec sip_round3(sip_quadruple()) -> sip_quadruple(). +sip_round3({V0, V1, V2, V3}) -> + {(V0 + V3) band ?MAX64, rotl64(V1, 17), (V2 + V1) band ?MAX64, rotl64(V3, 21)}. + +-spec sip_round4(sip_quadruple()) -> sip_quadruple(). +sip_round4({V0, V1, V2, V3}) -> + {V0, V1 bxor V2, rotl64(V2, 32), V3 bxor V0}. + +-spec sip_change(integer(), sip_quadruple()) -> sip_quadruple(). +sip_change(Nonce, {V0, V1, V2, V3}) -> + {V0 bxor Nonce, V1, V2 bxor 16#ff, V3}. + +-spec rotl64(integer(), integer()) -> integer(). +rotl64(X, B) -> + ((X bsl B) bor (X bsr (64 - B))) band 16#ffffffffffffffff. + diff --git a/test/aecuckoo_SUITE.erl b/test/aecuckoo_SUITE.erl new file mode 100644 index 0000000..54958f3 --- /dev/null +++ b/test/aecuckoo_SUITE.erl @@ -0,0 +1,65 @@ +%%%------------------------------------------------------------------- +%%% @copyright (C) 2019, Aeternity Anstalt +%%% @doc Basic sanity checks and examples on Cuckoo cycle PoW executables. +%%% @end +%%%------------------------------------------------------------------- +-module(aecuckoo_SUITE). + +%% common_test exports +-export( + [ + all/0, groups/0, + init_per_group/2, end_per_group/2 + ]). + +%% test case exports +-export([smoke_test/1]). + +-include_lib("common_test/include/ct.hrl"). + +-define(TEST_MODULE, aecuckoo). + +all() -> + [ + {group, smoke_tests_15} + ]. + +groups() -> + [ + {smoke_tests_15, [{group, mean15}, + {group, lean15}]}, + {mean15, [smoke_test]}, + {lean15, [smoke_test]} + ]. + +init_per_group(smoke_tests_15, Config) -> + [{nonce, 91} | Config]; +init_per_group(mean15, Config) -> + [{miner, 'mean15-generic'} | Config]; +init_per_group(lean15, Config) -> + [{miner, 'lean15-generic'} | Config]. + +end_per_group(_Group, _Config) -> + ok. + +smoke_test(Config) -> + Nonce = ?config(nonce, Config), + Miner = ?config(miner, Config), + + MinBin = ?TEST_MODULE:bin(atom_to_list(Miner)), + Cmd = io_lib:format("'~s' -n ~B | grep '^Solution'", [MinBin, Nonce]), + ct:log("Command: ~s~n", [Cmd]), + CmdRes = nonl(os:cmd(Cmd)), + ct:log("Command result: ~s~n", [CmdRes]), + + Solution = lists:map(fun(X) -> list_to_integer(X, 16) end, tl(string:tokens(CmdRes, " "))), + HeaderEquivalent = <<0:(44*8), (base64:encode(<>))/binary, 0:(24*8)>>, + + 42 = length(Solution), + true = aeminer_pow_cuckoo:verify_proof_(HeaderEquivalent, Solution, 15), + + ok. + +nonl([$\n]) -> []; +nonl([]) -> []; +nonl([H|T]) -> [H|nonl(T)]. diff --git a/test/aeminer_pow_cuckoo_tests.erl b/test/aeminer_pow_cuckoo_tests.erl new file mode 100644 index 0000000..6c7676d --- /dev/null +++ b/test/aeminer_pow_cuckoo_tests.erl @@ -0,0 +1,178 @@ +%%%============================================================================= +%%% @copyright (C) 2019, Aeternity Anstalt +%%% @doc +%%% Unit tests for the aeminer_pow_cuckoo module +%%% @end +%%%============================================================================= +-module(aeminer_pow_cuckoo_tests). + +-include_lib("eunit/include/eunit.hrl"). + +-include("aeminer.hrl"). + +-define(TEST_MODULE, aeminer_pow_cuckoo). + +-define(TEST_BIN, <<"wsffgujnjkqhduihsahswgdf">>). +-define(TEST_HIGH_NONCE, 38). %% Nonce with solution with high target. +-define(EDGE_BITS_15, 15). +-define(EDGE_BITS_29, 29). + +pow_test_() -> + [{"Generate with a winning nonce and high target threshold, verify it", + {timeout, 60, + fun() -> + Target = ?HIGHEST_TARGET_SCI, + Nonce = ?TEST_HIGH_NONCE, + Config = fast_and_deterministic_cuckoo_pow(), + Res = spawn_worker(fun() -> ?TEST_MODULE:generate(?TEST_BIN, Target, Nonce, Config, undefined) end), + {ok, {Nonce, Soln}} = Res, + ?assertMatch(L when length(L) == 42, Soln), + + %% verify the nonce and the solution + Res2 = ?TEST_MODULE:verify(?TEST_BIN, Nonce, Soln, Target, ?EDGE_BITS_15), + ?assert(Res2) + end} + }, + {"Generate with a winning nonce but low target threshold, shall fail", + {timeout, 90, + fun() -> + Target = 16#01010000, + Nonce = ?TEST_HIGH_NONCE, + Config = fast_and_deterministic_cuckoo_pow(), + Res1 = spawn_worker(fun() -> + ?TEST_MODULE:generate(?TEST_BIN, Target, Nonce, Config, undefined) + end), + ?assertEqual({error, no_solution}, Res1), + + %% Any attempts to verify such nonce with a solution + %% found with high target threshold shall fail. + %% + %% Obtain solution with high target threshold ... + HighTarget = ?HIGHEST_TARGET_SCI, + Res2 = spawn_worker(fun() -> + ?TEST_MODULE:generate(?TEST_BIN, HighTarget, Nonce, Config, undefined) + end), + {ok, {Nonce, Soln2}} = Res2, + ?assertMatch(L when length(L) == 42, Soln2), + %% ... then attempt to verify such solution (and + %% nonce) with the low target threshold (shall fail). + ?assertNot(?TEST_MODULE:verify(?TEST_BIN, Nonce, Soln2, Target, ?EDGE_BITS_15)) + end} + }, + {"Attempt to verify wrong solution for nonce that has a solution shall fail", + fun() -> + Target = ?HIGHEST_TARGET_SCI, + Nonce = ?TEST_HIGH_NONCE, + Config = fast_and_deterministic_cuckoo_pow(), + Res = spawn_worker(fun() -> ?TEST_MODULE:generate(?TEST_BIN, Target, Nonce, Config, undefined) end), + {ok, {Nonce, Soln}} = Res, + ?assertMatch(L when length(L) == 42, Soln), + + WrongSoln = lists:seq(0, 41), + ?assertMatch(L when length(L) == 42, WrongSoln), + ?assertNotEqual(Soln, WrongSoln), + ?assertNot(?TEST_MODULE:verify(?TEST_BIN, Nonce, WrongSoln, Target, ?EDGE_BITS_15)) + end}, + {"Attempt to verify nonce that does not have a solution (providing a dummy solution) shall fail", + fun() -> + Target = ?HIGHEST_TARGET_SCI, + Nonce = 1, + Config = fast_and_deterministic_cuckoo_pow(), + ?assertMatch({error, no_solution}, + spawn_worker(fun() -> ?TEST_MODULE:generate(?TEST_BIN, Target, Nonce, Config, undefined) end)), + + DummySoln = lists:seq(0, 41), + ?assertMatch(L when length(L) == 42, DummySoln), + ?assertNot(?TEST_MODULE:verify(?TEST_BIN, Nonce, DummySoln, Target, ?EDGE_BITS_15)) + end}, + {"Attempt to verify nonce that is too big shall fail gracefully", + fun() -> + % this is a premined working solution for size 27 + Hash = <<83,237,15,231,60,2,35,26,173,64,55,84,59,100,88,146,91, + 124,171,211,193,86,167,83,17,153,168,99,84,72,33,186>>, + Pow = [2253069,4506519,4850569,8551070,9391218,15176443,22052028, + 24045664,29484700,31332105,38588547,39046239,43427572, + 53979472,58387992,60256309,62282050,67357873,68186886, + 69815968,71809484,73494956,74992447,76953489,82132560, + 84075861,84934950,85804033,87920415,96539757,96818481, + 98049225,98464641,98907580,110711166,115480621,117062778, + 117537386,120015599,125293300,125684682,129332159], + Nonce = 17654096256755765485, + Target = 536940240, + ?assertNot(?TEST_MODULE:verify(Hash, Nonce, Pow, Target, ?EDGE_BITS_15)) + end} + ]. + +misc_test_() -> + [{"Conversion of a solution to binary", + fun() -> + Soln = [5936046,6000450,9980569,10770186,11256679,11557293, + 12330374,16556162,25308926,27241299,29693321,31019885, + 38091840,44351975,46970870,55597976,57712943,76763622, + 78513115,78670397,82776188,82841920,84299614,86421603, + 87878232,87913313,92453652,93430969,94032236,94428148, + 97119256,102408900,104747553,108943266,112048126, + 112561693,118817859,118965199,121744219,122178237, + 132944539,133889045], + NodeSize = ?TEST_MODULE:get_node_size(?EDGE_BITS_15), + ?assertEqual(42*NodeSize, size(?TEST_MODULE:solution_to_binary( + lists:sort(Soln), NodeSize * 8))) + end} + ]. + +kill_ospid_miner_test_() -> + [ {"Run miner in OS and kill it by killing parent", + fun() -> + Config = default_cuckoo_pow(), + Self = self(), + Pid = spawn(fun() -> + Self ! {?TEST_MODULE:generate(?TEST_BIN, 12837272, 128253, Config, undefined), self()} + end), + timer:sleep(200), %% give some time to start the miner OS pid + %% We did create a new one. + ?assertNotMatch([], os:cmd("ps -e | grep mean29- | grep -v grep")), + exit(Pid, shutdown), + timer:sleep(1000), %% give it some time to kill the miner OS pid + ?assertMatch([], os:cmd("ps -e | grep mean29- | grep -v grep")) + end} + ]. + +% This code is partially from aec_conductor + +spawn_worker(Fun) -> + Wrapper = wrap_worker_fun(Fun), + {Pid, _Ref} = spawn_monitor(Wrapper), + receive + {worker_reply, Pid, Res} -> + Res + end. + +prebuilt_miner_test_() -> + [{"Err if absent prebuilt miner", + fun() -> + Target = ?HIGHEST_TARGET_SCI, + Nonce = 1, + Config = prebuilt_cuckoo_pow(), + Res = spawn_worker(fun() -> ?TEST_MODULE:generate(?TEST_BIN, Target, Nonce, Config, undefined) end), + ?assertMatch({error,{runtime,{port_error,{error,enoent}}}}, Res) + end} + ]. + +wrap_worker_fun(Fun) -> + Server = self(), + fun() -> + Server ! {worker_reply, self(), Fun()} + end. + +fast_and_deterministic_cuckoo_pow() -> + ?TEST_MODULE:config(<<"mean15-generic">>, <<"aecuckoo">>, <<>>, false, 10, + ?EDGE_BITS_15, undefined). + +prebuilt_cuckoo_pow() -> + ?TEST_MODULE:config(<<"nonexistingminer">>, <<"aecuckooprebuilt">>, <<>>, + false, 1, ?EDGE_BITS_15, undefined). + +default_cuckoo_pow() -> + ?TEST_MODULE:config(<<"mean29-generic">>, <<"aecuckoo">>, <<>>, false, 1, + ?EDGE_BITS_29, undefined). + diff --git a/test/aeminer_pow_tests.erl b/test/aeminer_pow_tests.erl new file mode 100644 index 0000000..54097bb --- /dev/null +++ b/test/aeminer_pow_tests.erl @@ -0,0 +1,162 @@ +%%%============================================================================= +%%% @copyright (C) 2019, Aeternity Anstalt +%%% @doc +%%% Unit tests for the aeminer_pow module +%%% @end +%%%============================================================================= +-module(aeminer_pow_tests). + +-include_lib("eunit/include/eunit.hrl"). + +-define(TEST_MODULE, aeminer_pow). + +-include("aeminer.hrl"). + +conversion_test_() -> + {setup, + fun setup/0, + fun teardown/1, + [{"Integer to scientific conversion", + fun() -> + %% 02: shifted up 2 bytes to reach the [0x7fffff, 0x008000] range, + %% 8: sign as shifted up, 10000: significand + ?assertEqual(16#01010000, ?TEST_MODULE:integer_to_scientific(1)), + %% 01: shifted up 1 byte, 8: shifted up, 0ff00: significand + ?assertEqual(16#0200ff00, ?TEST_MODULE:integer_to_scientific(255)), + ?assertEqual(16#02010000, ?TEST_MODULE:integer_to_scientific(256)), + ?assertEqual(16#02010100, ?TEST_MODULE:integer_to_scientific(257)), + %% iput: 1 more than the largest possible significand: + %% shifted up 1 byte, the smallest possible significand + ?assertEqual(16#04008000, ?TEST_MODULE:integer_to_scientific(16#800000)), + %% same result: underflow + ?assertEqual(16#04008000, ?TEST_MODULE:integer_to_scientific(16#800001)), + %% example from BitCoin Wiki: + %% https://en.bitcoin.it/wiki/Difficulty#How_is_difficulty_calculated.3F_What_is_the_difference_between_bdiff_and_pdiff.3F: (256-bit hash: 64 hex digits) + ?assertEqual(16#1b0404cb, + ?TEST_MODULE:integer_to_scientific( + 16#00000000000404CB000000000000000000000000000000000000000000000000)), + %% highest possible target in bitcoin + ?assertEqual(16#1d00ffff, + ?TEST_MODULE:integer_to_scientific(16#00000000FFFF0000000000000000000000000000000000000000000000000000)), + %% highest expressible number + ?assertEqual(?HIGHEST_TARGET_SCI, + ?TEST_MODULE:integer_to_scientific(?HIGHEST_TARGET_INT)) + end}, + {"Scientific to integer conversion", + fun() -> ?assertEqual(1, ?TEST_MODULE:scientific_to_integer(16#01010000)), + ?assertEqual(255, ?TEST_MODULE:scientific_to_integer(16#0200ff00)), + ?assertEqual(16#800000, ?TEST_MODULE:scientific_to_integer(16#04008000)), + ?assertEqual(?HIGHEST_TARGET_INT, + aeminer_pow:scientific_to_integer(?HIGHEST_TARGET_SCI)) + end}, + {"Integer to scientific and back", + fun() -> + %% can be converted w/o losing accuracy + ?assertEqual(1, ?TEST_MODULE:scientific_to_integer( + ?TEST_MODULE:integer_to_scientific(1))), + ?assertEqual(255, ?TEST_MODULE:scientific_to_integer( + ?TEST_MODULE:integer_to_scientific(255))), + ?assertEqual(256, ?TEST_MODULE:scientific_to_integer( + ?TEST_MODULE:integer_to_scientific(256))), + %% losing accuracy (last digit: 257 = 1 0000 0001_2) + ?assertEqual(257, ?TEST_MODULE:scientific_to_integer( + ?TEST_MODULE:integer_to_scientific(257))), + %% can be converted w/o losing accuracy + ?assertEqual(16#800000, ?TEST_MODULE:scientific_to_integer( + ?TEST_MODULE:integer_to_scientific(16#800000))), + %% can be converted w/o losing accuracy + ?assertEqual(16#800000, ?TEST_MODULE:scientific_to_integer( + ?TEST_MODULE:integer_to_scientific(16#800001))), + %% can be converted w/o losing accuracy + Num1 = 16#00000000000404CB000000000000000000000000000000000000000000000000, + ?assertEqual(Num1, ?TEST_MODULE:scientific_to_integer( + ?TEST_MODULE:integer_to_scientific(Num1))), + Num2 = 16#00000000FFFF0000000000000000000000000000000000000000000000000000, + %% 0x1230000 = 1 0010 0011 0000 0000 0000_2, we lose the last 1 in conversion + ?assertEqual(Num2, ?TEST_MODULE:scientific_to_integer( + ?TEST_MODULE:integer_to_scientific(Num2))) + end}, + {"Testing difficulty", + fun() -> + %%---------------------------------------------------------------------- + %% More than 3 nonzero bytes + %%---------------------------------------------------------------------- + + ?assertEqual(true, ?TEST_MODULE:test_target( + <<0,0,0,0,0,16#04,16#04,16#ca, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0>>, + 16#1b0404cb)), + ?assertEqual(false, ?TEST_MODULE:test_target( + <<0,0,0,0,0,16#04,16#04,16#cc, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1>>, + 16#1b0404cb)), + ?assertEqual(false, ?TEST_MODULE:test_target( + <<0,0,1,0,0,16#04,16#04,16#ca, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1>>, + 16#1b0404cb)), + %%---------------------------------------------------------------------- + %% Less than 3 nonzero bytes + %%---------------------------------------------------------------------- + + %% 0403 < 0404 + ?assertEqual(true, ?TEST_MODULE:test_target( + <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,16#04,16#03>>, + 16#02040400)), + %% 0404 < 0404 fails + ?assertEqual(false, ?TEST_MODULE:test_target( + <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,16#04,16#04>>, + 16#02040400)), + %% 0405 < 0404 fails + ?assertEqual(false, ?TEST_MODULE:test_target( + <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,16#04,16#05>>, + 16#02040400)), + %% hide a 1 among zeros + ?assertEqual(false, ?TEST_MODULE:test_target( + <<0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,16#04,16#03>>, + 16#020404cb)), + %% 03 < 04 + ?assertEqual(true, ?TEST_MODULE:test_target( + <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,16#03>>, + 16#01040000)), + %% 04 < 04 fails + ?assertEqual(false, ?TEST_MODULE:test_target( + <<0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,16#04>>, + 16#01040000)), + + %%---------------------------------------------------------------------- + %% Exp > size of binary + %%---------------------------------------------------------------------- + + %% fffe < ffff + ?assertEqual(true, ?TEST_MODULE:test_target( + <<16#ff,16#fe,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0>>, + 16#2100ffff)), + %% fffffe < ffff00 fails + ?assertEqual(false, ?TEST_MODULE:test_target( + <<16#ff,16#ff,16#fe,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0>>, + 16#2100ffff)) + + end}, + {"Threshold to difficulty", + fun() -> + %% More than 3 nonzero bytes + Diff = ?TEST_MODULE:target_to_difficulty(16#1b0404cb), + ?assert(Diff == 1175073517793766964014) + end} + ] + }. + +setup() -> + application:start(crypto). + +teardown(_) -> + application:stop(crypto). +