diff --git a/include/blockchain_vars.hrl b/include/blockchain_vars.hrl index f3d1b843ab..b1eef14369 100644 --- a/include/blockchain_vars.hrl +++ b/include/blockchain_vars.hrl @@ -179,6 +179,8 @@ -define(poc_witnesses_percent, poc_witnesses_percent). -define(poc_challengers_percent, poc_challengers_percent). -define(dc_percent, dc_percent). +-define(witness_redundancy, witness_redundancy). +-define(poc_reward_decay_rate, poc_reward_decay_rate). %%% %%% bundle txn vars diff --git a/src/transactions/v1/blockchain_txn_poc_receipts_v1.erl b/src/transactions/v1/blockchain_txn_poc_receipts_v1.erl index bd8f99e4c3..c3a5f400fb 100644 --- a/src/transactions/v1/blockchain_txn_poc_receipts_v1.erl +++ b/src/transactions/v1/blockchain_txn_poc_receipts_v1.erl @@ -774,7 +774,7 @@ absorb(Txn, Chain) -> catch What:Why:Stacktrace -> lager:error([{poc_id, POCID}], "poc receipt calculation failed: ~p ~p ~p", [What, Why, Stacktrace]), - {error, state_missing} + {error, state_missing} end. -spec get_lower_and_upper_bounds(Secret :: binary(), diff --git a/src/transactions/v1/blockchain_txn_rewards_v1.erl b/src/transactions/v1/blockchain_txn_rewards_v1.erl index 8ce834dd0c..5ae1b6fcae 100644 --- a/src/transactions/v1/blockchain_txn_rewards_v1.erl +++ b/src/transactions/v1/blockchain_txn_rewards_v1.erl @@ -25,7 +25,8 @@ absorb/2, calculate_rewards/3, print/1, - to_json/2 + to_json/2, + legit_witnesses/6 ]). -ifdef(TEST). @@ -202,10 +203,12 @@ calculate_rewards(Start, End, Chain) -> %%-------------------------------------------------------------------- -spec print(txn_rewards()) -> iodata(). print(undefined) -> <<"type=rewards undefined">>; -print(#blockchain_txn_rewards_v1_pb{start_epoch=Start, end_epoch=End, +print(#blockchain_txn_rewards_v1_pb{start_epoch=Start, + end_epoch=End, rewards=Rewards}) -> + PrintableRewards = [blockchain_txn_reward_v1:print(R) || R <- Rewards], io_lib:format("type=rewards start_epoch=~p end_epoch=~p rewards=~p", - [Start, End, Rewards]). + [Start, End, PrintableRewards]). -spec to_json(txn_rewards(), blockchain_json:opts()) -> blockchain_json:json_object(). to_json(Txn, _Opts) -> @@ -291,6 +294,17 @@ get_reward_vars(Start, End, Ledger) -> {ok, R4} -> R4; _ -> 1 end, + + WitnessRedundancy = case blockchain:config(?witness_redundancy, Ledger) of + {ok, WR} -> WR; + _ -> undefined + end, + + DecayRate = case blockchain:config(?poc_reward_decay_rate, Ledger) of + {ok, R} -> R; + _ -> undefined + end, + EpochReward = calculate_epoch_reward(Start, End, Ledger), #{ monthly_reward => MonthlyReward, @@ -306,7 +320,9 @@ get_reward_vars(Start, End, Ledger) -> sc_grace_blocks => SCGrace, sc_version => SCVersion, poc_version => POCVersion, - reward_version => RewardVersion + reward_version => RewardVersion, + witness_redundancy => WitnessRedundancy, + poc_reward_decay_rate => DecayRate }. -spec calculate_epoch_reward(pos_integer(), pos_integer(), blockchain_ledger_v1:ledger()) -> float(). @@ -442,7 +458,7 @@ normalize_challenger_rewards(ChallengerRewards, #{epoch_reward := EpochReward, Ledger :: blockchain_ledger_v1:ledger(), map()) -> #{{gateway, libp2p_crypto:pubkey_bin()} => non_neg_integer()}. poc_challengees_rewards(Transactions, - #{poc_version := Version}, + Vars, Chain, Ledger, ExistingRewards) -> @@ -453,7 +469,7 @@ poc_challengees_rewards(Transactions, Acc0; true -> Path = blockchain_txn_poc_receipts_v1:path(Txn), - poc_challengees_rewards_(Version, Path, Path, Txn, Chain, Ledger, true, Acc0) + poc_challengees_rewards_(Vars, Path, Path, Txn, Chain, Ledger, true, Acc0) end end, ExistingRewards, @@ -465,6 +481,7 @@ normalize_challengee_rewards(ChallengeeRewards, #{epoch_reward := EpochReward, TotalChallenged = lists:sum(maps:values(ChallengeeRewards)), ShareOfDCRemainder = share_of_dc_rewards(poc_challengees_percent, Vars), ChallengeesReward = (EpochReward * PocChallengeesPercent) + ShareOfDCRemainder, + lager:info("TotalChallenged: ~p", [TotalChallenged]), maps:fold( fun(Challengee, Challenged, Acc) -> PercentofReward = (Challenged*100/TotalChallenged)/100, @@ -476,29 +493,20 @@ normalize_challengee_rewards(ChallengeeRewards, #{epoch_reward := EpochReward, ChallengeeRewards ). -poc_challengees_rewards_(_Version, [], _StaticPath, _Txn, _Chain, _Ledger, _, Acc) -> +poc_challengees_rewards_(_Vars, [], _StaticPath, _Txn, _Chain, _Ledger, _, Acc) -> Acc; -poc_challengees_rewards_(Version, [Elem|Path], StaticPath, Txn, Chain, Ledger, IsFirst, Acc0) when Version >= 2 -> +poc_challengees_rewards_(#{poc_version := Version}=Vars, + [Elem|Path], + StaticPath, + Txn, + Chain, + Ledger, + IsFirst, + Acc0) when Version >= 2 -> + WitnessRedundancy = maps:get(witness_redundancy, Vars, undefined), + DecayRate = maps:get(poc_reward_decay_rate, Vars, undefined), %% check if there were any legitimate witnesses - Witnesses = case Version of - V when is_integer(V), V >= 9 -> - try - %% Get channels without validation - {ok, Channels} = blockchain_txn_poc_receipts_v1:get_channels(Txn, Chain), - ElemPos = blockchain_utils:index_of(Elem, StaticPath), - WitnessChannel = lists:nth(ElemPos, Channels), - ValidWitnesses = blockchain_txn_poc_receipts_v1:valid_witnesses(Elem, WitnessChannel, Ledger), - lager:debug("ValidWitnesses: ~p", [[blockchain_utils:addr2name(blockchain_poc_witness_v1:gateway(W)) || W <- ValidWitnesses]]), - ValidWitnesses - catch What:Why:ST -> - lager:error("failed to calculate poc_challengees_rewards, error ~p:~p:~p", [What, Why, ST]), - [] - end; - V when is_integer(V), V > 4 -> - blockchain_txn_poc_receipts_v1:good_quality_witnesses(Elem, Ledger); - _ -> - blockchain_poc_path_element_v1:witnesses(Elem) - end, + Witnesses = legit_witnesses(Txn, Chain, Ledger, Elem, StaticPath, Version), Challengee = blockchain_poc_path_element_v1:challengee(Elem), I = maps:get(Challengee, Acc0, 0), case blockchain_poc_path_element_v1:receipt(Elem) of @@ -511,18 +519,30 @@ poc_challengees_rewards_(Version, [Elem|Path], StaticPath, Txn, Chain, Ledger, I %% while we don't have a receipt for this node, we do know %% there were witnesses or the path continued which means %% the challengee transmitted - maps:put(Challengee, I+1, Acc0); + case poc_challengee_reward_unit(WitnessRedundancy, DecayRate, Witnesses) of + {error, _} -> + %% Old behavior + maps:put(Challengee, I+1, Acc0); + {ok, ToAdd} -> + maps:put(Challengee, I+ToAdd, Acc0) + end; true when is_integer(Version), Version > 4, IsFirst == false -> %% while we don't have a receipt for this node, we do know %% there were witnesses or the path continued which means %% the challengee transmitted %% Additionally, we know this layer came in over radio so %% there's an implicit rx as well - maps:put(Challengee, I+2, Acc0); + case poc_challengee_reward_unit(WitnessRedundancy, DecayRate, Witnesses) of + {error, _} -> + %% Old behavior + maps:put(Challengee, I+2, Acc0); + {ok, ToAdd} -> + maps:put(Challengee, I+ToAdd, Acc0) + end; _ -> Acc0 end, - poc_challengees_rewards_(Version, Path, StaticPath, Txn, Chain, Ledger, false, Acc1); + poc_challengees_rewards_(Vars, Path, StaticPath, Txn, Chain, Ledger, false, Acc1); Receipt -> case blockchain_poc_receipt_v1:origin(Receipt) of radio -> @@ -534,14 +554,32 @@ poc_challengees_rewards_(Version, [Elem|Path], StaticPath, Txn, Chain, Ledger, I %% this challengee both rx'd and tx'd over radio %% AND sent a receipt %% so give them 3 payouts - maps:put(Challengee, I+3, Acc0); + case poc_challengee_reward_unit(WitnessRedundancy, DecayRate, Witnesses) of + {error, _} -> + %% Old behavior + maps:put(Challengee, I+3, Acc0); + {ok, ToAdd} -> + maps:put(Challengee, I+ToAdd, Acc0) + end; false when is_integer(Version), Version > 4 -> %% this challengee rx'd and sent a receipt - maps:put(Challengee, I+2, Acc0); + case poc_challengee_reward_unit(WitnessRedundancy, DecayRate, Witnesses) of + {error, _} -> + %% Old behavior + maps:put(Challengee, I+2, Acc0); + {ok, ToAdd} -> + maps:put(Challengee, I+ToAdd, Acc0) + end; _ -> - maps:put(Challengee, I+1, Acc0) + case poc_challengee_reward_unit(WitnessRedundancy, DecayRate, Witnesses) of + {error, _} -> + %% Old behavior + maps:put(Challengee, I+1, Acc0); + {ok, ToAdd} -> + maps:put(Challengee, I+ToAdd, Acc0) + end end, - poc_challengees_rewards_(Version, Path, StaticPath, Txn, Chain, Ledger, false, Acc1); + poc_challengees_rewards_(Vars, Path, StaticPath, Txn, Chain, Ledger, false, Acc1); p2p -> %% if there are legitimate witnesses or the path continues %% the challengee did their job @@ -554,22 +592,48 @@ poc_challengees_rewards_(Version, [Elem|Path], StaticPath, Txn, Chain, Ledger, I Acc0; true when is_integer(Version), Version > 4 -> %% Sent a receipt and the path continued on - maps:put(Challengee, I+2, Acc0); + case poc_challengee_reward_unit(WitnessRedundancy, DecayRate, Witnesses) of + {error, _} -> + %% Old behavior + maps:put(Challengee, I+2, Acc0); + {ok, ToAdd} -> + maps:put(Challengee, I+ToAdd, Acc0) + end; true -> - maps:put(Challengee, I+1, Acc0) + case poc_challengee_reward_unit(WitnessRedundancy, DecayRate, Witnesses) of + {error, _} -> + %% Old behavior + maps:put(Challengee, I+1, Acc0); + {ok, ToAdd} -> + maps:put(Challengee, I+ToAdd, Acc0) + end end, - poc_challengees_rewards_(Version, Path, StaticPath, Txn, Chain, Ledger, false, Acc1) + poc_challengees_rewards_(Vars, Path, StaticPath, Txn, Chain, Ledger, false, Acc1) end end; -poc_challengees_rewards_(Version, [Elem|Path], StaticPath, Txn, Chain, Ledger, _IsFirst, Acc0) -> +poc_challengees_rewards_(Vars, [Elem|Path], StaticPath, Txn, Chain, Ledger, _IsFirst, Acc0) -> case blockchain_poc_path_element_v1:receipt(Elem) of undefined -> - poc_challengees_rewards_(Version, Path, StaticPath, Txn, Chain, Ledger, false, Acc0); + poc_challengees_rewards_(Vars, Path, StaticPath, Txn, Chain, Ledger, false, Acc0); _Receipt -> Challengee = blockchain_poc_path_element_v1:challengee(Elem), I = maps:get(Challengee, Acc0, 0), Acc1 = maps:put(Challengee, I+1, Acc0), - poc_challengees_rewards_(Version, Path, StaticPath, Txn, Chain, Ledger, false, Acc1) + poc_challengees_rewards_(Vars, Path, StaticPath, Txn, Chain, Ledger, false, Acc1) + end. + +-spec poc_challengee_reward_unit(WitnessRedundancy :: undefined | pos_integer(), + DecayRate :: undefined | float(), + Witnesses :: blockchain_poc_witness_v1:poc_witnesses()) -> {error, any()} | {ok, float()}. +poc_challengee_reward_unit(WitnessRedundancy, DecayRate, Witnesses) -> + case {WitnessRedundancy, DecayRate} of + {undefined, _} -> {error, witness_redundancy_undefined}; + {_, undefined} -> {error, poc_reward_decay_rate_undefined}; + {N, R} -> + W = length(Witnesses), + Unit = poc_reward_tx_unit(R, W, N), + lager:info("poc_challengee_reward_unit: ~p", [Unit]), + {ok, Unit} end. -spec poc_witnesses_rewards(Transactions :: blockchain_txn:txns(), @@ -578,10 +642,12 @@ poc_challengees_rewards_(Version, [Elem|Path], StaticPath, Txn, Chain, Ledger, _ Ledger :: blockchain_ledger_v1:ledger(), WitnessRewards :: map()) -> #{{gateway, libp2p_crypto:pubkey_bin()} => non_neg_integer()}. poc_witnesses_rewards(Transactions, - #{poc_version := POCVersion}, + #{poc_version := POCVersion}=Vars, Chain, Ledger, WitnessRewards) -> + WitnessRedundancy = maps:get(witness_redundancy, Vars, undefined), + DecayRate = maps:get(poc_reward_decay_rate, Vars, undefined), lists:foldl( fun(Txn, Acc0) -> case blockchain_txn:type(Txn) == blockchain_txn_poc_receipts_v1 of @@ -604,11 +670,26 @@ poc_witnesses_rewards(Transactions, [] -> Acc1; ValidWitnesses -> + %% We found some valid witnesses, we only apply the witness_redundancy and decay_rate if BOTH + %% are set as chain variables, otherwise we default to the old behavior and set ToAdd=1 + %% + %% If both witness_redundancy and decay_rate are set, we calculate a scaled rx unit (the value ToAdd) + %% This is determined using the formulae mentioned in hip15 + ToAdd = case {WitnessRedundancy, DecayRate} of + {undefined, _} -> 1; + {_, undefined} -> 1; + {N, R} -> + W = length(ValidWitnesses), + U = poc_reward_rx_unit(R, W, N), + lager:info("poc_reward_rx_unit: ~p", [U]), + U + end, + lists:foldl( fun(WitnessRecord, Map) -> Witness = blockchain_poc_witness_v1:gateway(WitnessRecord), I = maps:get(Witness, Map, 0), - maps:put(Witness, I+1, Map) + maps:put(Witness, I+ToAdd, Map) end, Acc1, ValidWitnesses @@ -826,6 +907,47 @@ normalize_dc_rewards(DCRewards0, #{epoch_reward := EpochReward, DCRewards )}. +-spec poc_reward_tx_unit(R :: float(), + W :: pos_integer(), + N :: pos_integer()) -> float(). +poc_reward_tx_unit(R, W, N) when W =< N -> + lager:info("nonorm, R: ~p, W: ~p, N: ~p, poc_reward_tx_unit: ~p", [R, W, N, W / N]), + blockchain_utils:normalize_float(W / N); +poc_reward_tx_unit(R, W, N) -> + NoNorm = 1 + (1 - math:pow(R, (W - N))), + lager:info("nonorm, R: ~p, W: ~p, N: ~p, poc_reward_tx_unit: ~p", [R, W, N, NoNorm]), + blockchain_utils:normalize_float(NoNorm). + +-spec poc_reward_rx_unit(R :: float(), + W :: pos_integer(), + N :: pos_integer()) -> float(). +poc_reward_rx_unit(_R, W, N) when W =< N -> + 1; +poc_reward_rx_unit(R, W, N) -> + blockchain_utils:normalize_float((N - (1 - math:pow(R, (W - N))))/W). + +legit_witnesses(Txn, Chain, Ledger, Elem, StaticPath, Version) -> + case Version of + V when is_integer(V), V >= 9 -> + try + %% Get channels without validation + {ok, Channels} = blockchain_txn_poc_receipts_v1:get_channels(Txn, Chain), + ElemPos = blockchain_utils:index_of(Elem, StaticPath), + WitnessChannel = lists:nth(ElemPos, Channels), + ValidWitnesses = blockchain_txn_poc_receipts_v1:valid_witnesses(Elem, WitnessChannel, Ledger), + lager:debug("ValidWitnesses: ~p", + [[blockchain_utils:addr2name(blockchain_poc_witness_v1:gateway(W)) || W <- ValidWitnesses]]), + ValidWitnesses + catch What:Why:ST -> + lager:error("failed to calculate poc_challengees_rewards, error ~p:~p:~p", [What, Why, ST]), + [] + end; + V when is_integer(V), V > 4 -> + blockchain_txn_poc_receipts_v1:good_quality_witnesses(Elem, Ledger); + _ -> + blockchain_poc_path_element_v1:witnesses(Elem) + end. + %%-------------------------------------------------------------------- %% @doc %% @end @@ -1341,7 +1463,7 @@ old_poc_witnesses_rewards_test() -> test_utils:cleanup_tmp_dir(BaseDir). dc_rewards_test() -> - BaseDir = test_utils:tmp_dir("poc_challengees_rewards_2_test"), + BaseDir = test_utils:tmp_dir("dc_rewards_test"), Ledger = blockchain_ledger_v1:new(BaseDir), Ledger1 = blockchain_ledger_v1:new_context(Ledger), @@ -1388,7 +1510,7 @@ dc_rewards_test() -> test_utils:cleanup_tmp_dir(BaseDir). dc_rewards_v3_test() -> - BaseDir = test_utils:tmp_dir("poc_challengees_rewards_2_test"), + BaseDir = test_utils:tmp_dir("dc_rewards_v3_test"), Ledger = blockchain_ledger_v1:new(BaseDir), Ledger1 = blockchain_ledger_v1:new_context(Ledger), @@ -1436,7 +1558,7 @@ dc_rewards_v3_test() -> test_utils:cleanup_tmp_dir(BaseDir). dc_rewards_v3_spillover_test() -> - BaseDir = test_utils:tmp_dir("poc_challengees_rewards_2_test"), + BaseDir = test_utils:tmp_dir("dc_rewards_v3_spillover_test"), Block = blockchain_block:new_genesis_block([]), {ok, Chain} = blockchain:new(BaseDir, Block, undefined, undefined), Ledger = blockchain:ledger(Chain), @@ -1447,7 +1569,7 @@ dc_rewards_v3_spillover_test() -> dc_percent => 0.05, consensus_percent => 0.1, poc_challengees_percent => 0.20, - poc_challengers_percent => 0.15, + poc_challengers_percent => 0.15, poc_witnesses_percent => 0.15, securities_percent => 0.35, sc_version => 2, diff --git a/src/transactions/v1/blockchain_txn_vars_v1.erl b/src/transactions/v1/blockchain_txn_vars_v1.erl index 8980559059..1f7b994af5 100644 --- a/src/transactions/v1/blockchain_txn_vars_v1.erl +++ b/src/transactions/v1/blockchain_txn_vars_v1.erl @@ -914,6 +914,10 @@ validate_var(?poc_challengers_percent, Value) -> validate_float(Value, "poc_challengers_percent", 0.0, 1.0); validate_var(?dc_percent, Value) -> validate_float(Value, "dc_percent", 0.0, 1.0); +validate_var(?witness_redundancy, Value) -> + validate_int(Value, "witness_redundancy", 2, 100, false); +validate_var(?poc_reward_decay_rate, Value) -> + validate_float(Value, "poc_reward_decay_rate", 0.0, 1.0); validate_var(?reward_version, Value) -> case Value of N when is_integer(N), N >= 1, N =< 5 -> diff --git a/test/blockchain_reward_SUITE.erl b/test/blockchain_reward_SUITE.erl new file mode 100644 index 0000000000..ef09d55133 --- /dev/null +++ b/test/blockchain_reward_SUITE.erl @@ -0,0 +1,521 @@ +-module(blockchain_reward_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +-include("blockchain_vars.hrl"). + +-export([ + all/0, + init_per_suite/1, + end_per_suite/1, + init_per_testcase/2, + end_per_testcase/2 +]). + +-export([ + lower_than_reduntant_test/1, + higher_than_reduntant_test/1, + lower_than_reduntant_no_var_test/1, + higher_than_reduntant_no_var_test/1, + compare_lower_test/1, + compare_higher_test/1, + compare_lower_higher_test/1 +]). + +all() -> + [ + lower_than_reduntant_test, + higher_than_reduntant_test, + lower_than_reduntant_no_var_test, + higher_than_reduntant_no_var_test, + compare_lower_test, + compare_higher_test, + compare_lower_higher_test + ]. + +%%-------------------------------------------------------------------- +%% TEST SUITE SETUP +%%-------------------------------------------------------------------- + +init_per_suite(Config) -> + {ok, StorePid} = blockchain_test_reward_store:start(), + [{store, StorePid} | Config]. + +%%-------------------------------------------------------------------- +%% TEST SUITE TEARDOWN +%%-------------------------------------------------------------------- + +end_per_suite(_Config) -> + blockchain_test_reward_store:stop(), + ok. + +%%-------------------------------------------------------------------- +%% TEST CASE SETUP +%%-------------------------------------------------------------------- + +init_per_testcase(TestCase, Config) -> + Config0 = blockchain_ct_utils:init_base_dir_config(?MODULE, TestCase, Config), + Balance = 5000, + {ok, Sup, {PrivKey, PubKey}, Opts} = test_utils:init(?config(base_dir, Config0)), + + ExtraVars = + case TestCase of + lower_than_reduntant_no_var_test -> + #{}; + higher_than_reduntant_no_var_test -> + #{}; + _ -> + #{ + %% configured on chain + ?poc_version => 9, + ?reward_version => 5, + %% new vars for testing + ?poc_reward_decay_rate => 0.8, + ?witness_redundancy => 4 + } + end, + + {ok, GenesisMembers, _GenesisBlock, ConsensusMembers, Keys} = + test_utils:init_chain(Balance, {PrivKey, PubKey}, true, ExtraVars), + + Chain = blockchain_worker:blockchain(), + Swarm = blockchain_swarm:swarm(), + N = length(ConsensusMembers), + + % Check ledger to make sure everyone has the right balance + Ledger = blockchain:ledger(Chain), + Entries = blockchain_ledger_v1:entries(Ledger), + _ = lists:foreach( + fun(Entry) -> + Balance = blockchain_ledger_entry_v1:balance(Entry), + 0 = blockchain_ledger_entry_v1:nonce(Entry) + end, + maps:values(Entries) + ), + + meck:new(blockchain_txn_rewards_v1, [passthrough]), + meck:new(blockchain_txn_poc_receipts_v1, [passthrough]), + + [ + {balance, Balance}, + {sup, Sup}, + {pubkey, PubKey}, + {privkey, PrivKey}, + {opts, Opts}, + {chain, Chain}, + {swarm, Swarm}, + {n, N}, + {consensus_members, ConsensusMembers}, + {genesis_members, GenesisMembers}, + {tc_name, TestCase}, + Keys + | Config0 + ]. + +%%-------------------------------------------------------------------- +%% TEST CASE TEARDOWN +%%-------------------------------------------------------------------- + +end_per_testcase(_TestCase, Config) -> + meck:unload(blockchain_txn_rewards_v1), + meck:unload(blockchain_txn_poc_receipts_v1), + meck:unload(), + Sup = ?config(sup, Config), + % Make sure blockchain saved on file = in memory + case erlang:is_process_alive(Sup) of + true -> + true = erlang:exit(Sup, normal), + ok = test_utils:wait_until(fun() -> false =:= erlang:is_process_alive(Sup) end); + false -> + ok + end, + ok. + +%%-------------------------------------------------------------------- +%% TEST CASES +%%-------------------------------------------------------------------- + +lower_than_reduntant_no_var_test(Config) -> + Witnesses = [i, j], + run_test(Witnesses, Config). + +higher_than_reduntant_no_var_test(Config) -> + Witnesses = [b, c, e, f, g], + run_test(Witnesses, Config). + +lower_than_reduntant_test(Config) -> + %% - We have gateways: [a, b, c, d, e, f, g, h, i, j, k] + %% - We'll make a poc receipt txn by hand, without any validation + %% - We'll also consider that all witnesses are legit (legit_witnesses) + %% - The poc transaction will have path like so: a -> d -> h + %% - For a -> d; i, j will be the only witnesses; no scaling should happen + %% - For d -> h; 0 witnesses + Witnesses = [i, j], + run_test(Witnesses, Config). + +higher_than_reduntant_test(Config) -> + %% - We have gateways: [a, b, c, d, e, f, g, h, i, j, k] + %% - We'll make a poc receipt txn by hand, without any validation + %% - We'll also consider that all witnesses are legit (legit_witnesses) + %% - The poc transaction will have path like so: a -> d -> h + %% - For a -> d; [b, c, e, f, g] will all be witnesses, their rewards should get scaled + %% - For d -> h; 0 witnesses + Witnesses = [b, c, e, f, g], + run_test(Witnesses, Config). + +compare_higher_test(_Config) -> + HigherThanReduntantWitnessRewards = blockchain_test_reward_store:fetch( + higher_than_reduntant_test_witness_rewards + ), + HigherThanReduntantChallengeeRewards = blockchain_test_reward_store:fetch( + higher_than_reduntant_test_challengee_rewards + ), + HigherThanReduntantNoVarWitnessRewards = blockchain_test_reward_store:fetch( + higher_than_reduntant_no_var_test_witness_rewards + ), + HigherThanReduntantNoVarChallengeeRewards = blockchain_test_reward_store:fetch( + higher_than_reduntant_no_var_test_challengee_rewards + ), + + ct:pal("HigherThanReduntantWitnessRewards: ~p", [HigherThanReduntantWitnessRewards]), + ct:pal("HigherThanReduntantNoVarWitnessRewards: ~p", [HigherThanReduntantNoVarWitnessRewards]), + ct:pal("HigherThanReduntantChallengeeRewards: ~p", [HigherThanReduntantChallengeeRewards]), + ct:pal("HigherThanReduntantNoVarChallengeeRewards: ~p", [ + HigherThanReduntantNoVarChallengeeRewards + ]), + + %% we _know_ this is how it should be + StaticChallengees = [a, d, h], + StaticWitnesses = [b, c, e, f, g], + + %% Check1: only a, d, h get challengee rewards + true = + lists:sort(StaticChallengees) == + lists:sort(maps:keys(HigherThanReduntantChallengeeRewards)), + true = + lists:sort(StaticChallengees) == + lists:sort(maps:keys(HigherThanReduntantNoVarChallengeeRewards)), + + %% Check2: only i, j get witness rewards + true = lists:sort(StaticWitnesses) == lists:sort(maps:keys(HigherThanReduntantWitnessRewards)), + true = + lists:sort(StaticWitnesses) == + lists:sort(maps:keys(HigherThanReduntantNoVarWitnessRewards)), + + %% Check3: every single challengee reward for a hotspot in no_var_test is higher than with_var_test + true = lists:all( + fun(Gw) -> + Hip15Value = maps:get(Gw, HigherThanReduntantChallengeeRewards), + NonHip15Value = maps:get(Gw, HigherThanReduntantNoVarChallengeeRewards), + Hip15Value < NonHip15Value + end, + StaticChallengees + ), + + %% Check4: every single witness reward for a hotspot in no_var_test is higher than with_var_test + true = lists:all( + fun(Gw) -> + Hip15Value = maps:get(Gw, HigherThanReduntantWitnessRewards), + NonHip15Value = maps:get(Gw, HigherThanReduntantNoVarWitnessRewards), + Hip15Value < NonHip15Value + end, + StaticWitnesses + ), + + ok. + +compare_lower_higher_test(_Config) -> + LowerThanReduntantWitnessRewards = blockchain_test_reward_store:fetch( + lower_than_reduntant_test_witness_rewards + ), + LowerThanReduntantChallengeeRewards = blockchain_test_reward_store:fetch( + lower_than_reduntant_test_challengee_rewards + ), + + HigherThanReduntantWitnessRewards = blockchain_test_reward_store:fetch( + higher_than_reduntant_test_witness_rewards + ), + HigherThanReduntantChallengeeRewards = blockchain_test_reward_store:fetch( + higher_than_reduntant_test_challengee_rewards + ), + ct:pal("LowerThanReduntantWitnessRewards: ~p", [LowerThanReduntantWitnessRewards]), + ct:pal("HigherThanReduntantWitnessRewards: ~p", [HigherThanReduntantWitnessRewards]), + ct:pal("LowerThanReduntantChallengeeRewards: ~p", [LowerThanReduntantChallengeeRewards]), + ct:pal("HigherThanReduntantChallengeeRewards: ~p", [HigherThanReduntantChallengeeRewards]), + + true = + hd(maps:values(LowerThanReduntantWitnessRewards)) > + hd(maps:values(HigherThanReduntantWitnessRewards)), + + ok. + +compare_lower_test(_Config) -> + LowerThanReduntantWitnessRewards = blockchain_test_reward_store:fetch( + lower_than_reduntant_test_witness_rewards + ), + LowerThanReduntantChallengeeRewards = blockchain_test_reward_store:fetch( + lower_than_reduntant_test_challengee_rewards + ), + + LowerThanReduntantNoVarWitnessRewards = blockchain_test_reward_store:fetch( + lower_than_reduntant_no_var_test_witness_rewards + ), + LowerThanReduntantNoVarChallengeeRewards = blockchain_test_reward_store:fetch( + lower_than_reduntant_no_var_test_challengee_rewards + ), + + ct:pal("LowerThanReduntantWitnessRewards: ~p", [LowerThanReduntantWitnessRewards]), + ct:pal("LowerThanReduntantNoVarWitnessRewards: ~p", [LowerThanReduntantNoVarWitnessRewards]), + ct:pal("LowerThanReduntantChallengeeRewards: ~p", [LowerThanReduntantChallengeeRewards]), + ct:pal("LowerThanReduntantNoVarChallengeeRewards: ~p", [ + LowerThanReduntantNoVarChallengeeRewards + ]), + + %% we _know_ this is how it should be + StaticChallengees = [a, d, h], + StaticWitnesses = [i, j], + + %% Check1: only a, d, h get challengee rewards + true = + lists:sort(StaticChallengees) == lists:sort(maps:keys(LowerThanReduntantChallengeeRewards)), + true = + lists:sort(StaticChallengees) == + lists:sort(maps:keys(LowerThanReduntantNoVarChallengeeRewards)), + + %% Check2: only i, j get witness rewards + true = lists:sort(StaticWitnesses) == lists:sort(maps:keys(LowerThanReduntantWitnessRewards)), + true = + lists:sort(StaticWitnesses) == lists:sort(maps:keys(LowerThanReduntantNoVarWitnessRewards)), + + %% Check3: every single challengee reward for a hotspot in no_var_test is higher than with_var_test + true = lists:all( + fun(Gw) -> + Hip15Value = maps:get(Gw, LowerThanReduntantChallengeeRewards), + NonHip15Value = maps:get(Gw, LowerThanReduntantNoVarChallengeeRewards), + Hip15Value < NonHip15Value + end, + StaticChallengees + ), + + %% Check4: every single witness reward for a hotspot in no_var_test is higher than with_var_test + true = lists:all( + fun(Gw) -> + Hip15Value = maps:get(Gw, LowerThanReduntantWitnessRewards), + NonHip15Value = maps:get(Gw, LowerThanReduntantNoVarWitnessRewards), + Hip15Value < NonHip15Value + end, + StaticWitnesses + ), + + ok. + +%%-------------------------------------------------------------------- +%% HELPER +%%-------------------------------------------------------------------- + +run_test(Witnesses, Config) -> + ct:pal("Config: ~p", [Config]), + BaseDir = ?config(base_dir, Config), + ConsensusMembers = ?config(consensus_members, Config), + BaseDir = ?config(base_dir, Config), + Chain = ?config(chain, Config), + TCName = ?config(tc_name, Config), + Store = ?config(store, Config), + ct:pal("store: ~p", [Store]), + + Ledger = blockchain:ledger(Chain), + Vars = blockchain_ledger_v1:snapshot_vars(Ledger), + ct:pal("Vars: ~p", [Vars]), + + AG = blockchain_ledger_v1:active_gateways(Ledger), + + GatewayAddrs = lists:sort(maps:keys(AG)), + + AllGws = [a, b, c, d, e, f, g, h, i, j, k], + + %% For crosscheck + GatewayNameMap = lists:foldl( + fun({Letter, A}, Acc) -> + maps:put(blockchain_utils:addr2name(A), Letter, Acc) + end, + #{}, + lists:zip(AllGws, GatewayAddrs) + ), + + %% For crosscheck + GatewayLetterToAddrMap = lists:foldl( + fun({Letter, A}, Acc) -> + maps:put(Letter, A, Acc) + end, + #{}, + lists:zip(AllGws, GatewayAddrs) + ), + + Challenger = maps:get(k, GatewayLetterToAddrMap), + + GwA = maps:get(a, GatewayLetterToAddrMap), + GwD = maps:get(d, GatewayLetterToAddrMap), + GwH = maps:get(h, GatewayLetterToAddrMap), + + Rx1 = blockchain_poc_receipt_v1:new( + GwA, + 1000, + 10, + "first_rx", + p2p + ), + Rx2 = blockchain_poc_receipt_v1:new( + GwD, + 1000, + 10, + "second_rx", + radio + ), + Rx3 = blockchain_poc_receipt_v1:new( + GwH, + 1000, + 10, + "third_rx", + radio + ), + + ct:pal("Rx1: ~p", [Rx1]), + ct:pal("Rx2: ~p", [Rx2]), + ct:pal("Rx3: ~p", [Rx3]), + + ConstructedWitnesses = lists:foldl( + fun(W, Acc) -> + WitnessGw = maps:get(W, GatewayLetterToAddrMap), + Witness = blockchain_poc_witness_v1:new( + WitnessGw, + 1001, + 10, + crypto:strong_rand_bytes(32), + 9.8, + 915.2, + 10, + <<"data_rate">> + ), + [Witness | Acc] + end, + [], + Witnesses + ), + + ct:pal("ConstructedWitnesses: ~p", [ConstructedWitnesses]), + + %% We'll consider all the witnesses to be "good quality" for the sake of testing + meck:expect( + blockchain_txn_rewards_v1, + legit_witnesses, + fun(_, _, _, _, _, _) -> + ConstructedWitnesses + end + ), + meck:expect(blockchain_txn_poc_receipts_v1, absorb, fun(_, _) -> ok end), + meck:expect(blockchain_txn_poc_receipts_v1, valid_witnesses, fun(_, _, _) -> + ConstructedWitnesses + end), + meck:expect(blockchain_txn_poc_receipts_v1, good_quality_witnesses, fun(_, _) -> + ConstructedWitnesses + end), + meck:expect(blockchain_txn_poc_receipts_v1, get_channels, fun(_, _) -> + {ok, lists:seq(1, 11)} + end), + + P1 = blockchain_poc_path_element_v1:new(GwA, Rx1, []), + P2 = blockchain_poc_path_element_v1:new(GwD, Rx2, ConstructedWitnesses), + P3 = blockchain_poc_path_element_v1:new(GwH, Rx3, []), + + ct:pal("P1: ~p", [P1]), + ct:pal("P2: ~p", [P2]), + ct:pal("P3: ~p", [P3]), + + Txn = blockchain_txn_poc_receipts_v1:new( + Challenger, + <<"secret">>, + <<"onion_key_hash">>, + <<"block_hash">>, + [P1, P2, P3] + ), + ct:pal("Txn: ~p", [Txn]), + + %% Construct a block for the poc receipt txn WITHOUT validation + {ok, Block2} = test_utils:create_block(ConsensusMembers, [Txn], #{}, false), + ct:pal("Block2: ~p", [Block2]), + _ = blockchain_gossip_handler:add_block(Block2, Chain, self(), blockchain_swarm:swarm()), + ?assertEqual({ok, 2}, blockchain:height(Chain)), + + %% Empty block + {ok, Block3} = test_utils:create_block(ConsensusMembers, []), + ct:pal("Block3: ~p", [Block3]), + _ = blockchain_gossip_handler:add_block(Block3, Chain, self(), blockchain_swarm:swarm()), + ?assertEqual({ok, 3}, blockchain:height(Chain)), + + %% Calculate rewards by hand + Start = 1, + End = 3, + {ok, Rewards} = blockchain_txn_rewards_v1:calculate_rewards(Start, End, Chain), + + ChallengeesRewards = lists:filter( + fun(R) -> + blockchain_txn_reward_v1:type(R) == poc_challengees + end, + Rewards + ), + + WitnessRewards = lists:filter( + fun(R) -> + blockchain_txn_reward_v1:type(R) == poc_witnesses + end, + Rewards + ), + + ChallengeesRewardsMap = + lists:foldl( + fun(R, Acc) -> + maps:put( + maps:get( + blockchain_utils:addr2name(blockchain_txn_reward_v1:gateway(R)), + GatewayNameMap + ), + blockchain_txn_reward_v1:amount(R), + Acc + ) + end, + #{}, + ChallengeesRewards + ), + + WitnessRewardsMap = + lists:foldl( + fun(R, Acc) -> + maps:put( + maps:get( + blockchain_utils:addr2name(blockchain_txn_reward_v1:gateway(R)), + GatewayNameMap + ), + blockchain_txn_reward_v1:amount(R), + Acc + ) + end, + #{}, + WitnessRewards + ), + + %% Theoretically, gateways J, K should have higher witness rewards than B, C, E, F, G, I + ct:pal("Gateways: ~p", [GatewayNameMap]), + ct:pal("ChallengeesRewardsMap: ~p", [ChallengeesRewardsMap]), + ct:pal("WitnessRewardsMap: ~p", [WitnessRewardsMap]), + + ok = blockchain_test_reward_store:insert( + list_to_atom(atom_to_list(TCName) ++ "_witness_rewards"), + WitnessRewardsMap + ), + ok = blockchain_test_reward_store:insert( + list_to_atom(atom_to_list(TCName) ++ "_challengee_rewards"), + ChallengeesRewardsMap + ), + + ok. diff --git a/test/blockchain_test_reward_store.erl b/test/blockchain_test_reward_store.erl new file mode 100644 index 0000000000..0d3cb2a971 --- /dev/null +++ b/test/blockchain_test_reward_store.erl @@ -0,0 +1,56 @@ +-module(blockchain_test_reward_store). + +-behaviour(gen_server). + +-export([start/0]). +-export([fetch/1, insert/2, state/0, stop/0]). + +% our handlers +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3 +]). + +start() -> + gen_server:start({local, ?MODULE}, ?MODULE, [], []). + +insert(Key, Value) -> + gen_server:call(?MODULE, {insert, {Key, Value}}). + +fetch(Key) -> + gen_server:call(?MODULE, {fetch, Key}). + +state() -> + gen_server:call(?MODULE, state). + +stop() -> + gen_server:stop(?MODULE). + +init([]) -> + {ok, #{}}. + +handle_call({fetch, Key}, _From, State) -> + {reply, maps:get(Key, State, undefined), State}; +handle_call(state, _From, State) -> + {reply, State, State}; +handle_call({insert, {Key, Value}}, _From, State) -> + NewState = maps:put(Key, Value, State), + {reply, ok, NewState}; +handle_call(_Msg, _From, State) -> + {reply, ok, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. diff --git a/test/test_utils.erl b/test/test_utils.erl index 4f1ad22cb4..c521a51e83 100644 --- a/test/test_utils.erl +++ b/test/test_utils.erl @@ -9,7 +9,7 @@ init_chain/2, init_chain/3, init_chain/4, generate_keys/1, generate_keys/2, wait_until/1, wait_until/3, - create_block/2, create_block/3, + create_block/2, create_block/3, create_block/4, tmp_dir/0, tmp_dir/1, cleanup_tmp_dir/1, nonl/1, @@ -133,39 +133,52 @@ wait_until(Fun, Retry, Delay) when Retry > 0 -> end. create_block(ConsensusMembers, Txs) -> - create_block(ConsensusMembers, Txs, #{}). + %% Run validations by default + create_block(ConsensusMembers, Txs, #{}, true). create_block(ConsensusMembers, Txs, Override) -> + %% Run validations by default + create_block(ConsensusMembers, Txs, Override, true). + +create_block(ConsensusMembers, Txs, Override, RunValidation) -> Blockchain = blockchain_worker:blockchain(), - {ok, PrevHash} = blockchain:head_hash(Blockchain), + STxs = lists:sort(fun blockchain_txn:sort/2, Txs), + case RunValidation of + false -> + %% Just make a block without validation + {ok, make_block(Blockchain, ConsensusMembers, STxs, Override)}; + true -> + case blockchain_txn:validate(STxs, Blockchain) of + {_, []} -> + {ok, make_block(Blockchain, ConsensusMembers, STxs, Override)}; + {_, Invalid} -> + {error, {invalid_txns, Invalid}} + end + end. + +make_block(Blockchain, ConsensusMembers, STxs, Override) -> {ok, HeadBlock} = blockchain:head_block(Blockchain), + {ok, PrevHash} = blockchain:head_hash(Blockchain), Height = blockchain_block:height(HeadBlock) + 1, Time = blockchain_block:time(HeadBlock) + 1, - STxs = lists:sort(fun blockchain_txn:sort/2, Txs), - %% make sure all these txns are valid - case blockchain_txn:validate(STxs, Blockchain) of - {_, []} -> - lager:info("creating block ~p", [STxs]), - Default = #{prev_hash => PrevHash, - height => Height, - transactions => STxs, - signatures => [], - time => Time, - hbbft_round => 0, - election_epoch => 1, - epoch_start => 0, - seen_votes => [], - bba_completion => <<>> - }, - Block0 = blockchain_block_v1:new(maps:merge(Default, Override)), - BinBlock = blockchain_block:serialize(Block0), - Signatures = signatures(ConsensusMembers, BinBlock), - Block1 = blockchain_block:set_signatures(Block0, Signatures), - lager:info("block ~p", [Block1]), - {ok, Block1}; - {_, Invalid} -> - {error, {invalid_txns, Invalid}} - end. + lager:info("creating block ~p", [STxs]), + Default = #{prev_hash => PrevHash, + height => Height, + transactions => STxs, + signatures => [], + time => Time, + hbbft_round => 0, + election_epoch => 1, + epoch_start => 0, + seen_votes => [], + bba_completion => <<>> + }, + Block0 = blockchain_block_v1:new(maps:merge(Default, Override)), + BinBlock = blockchain_block:serialize(Block0), + Signatures = signatures(ConsensusMembers, BinBlock), + Block1 = blockchain_block:set_signatures(Block0, Signatures), + lager:info("block ~p", [Block1]), + Block1. signatures(ConsensusMembers, BinBlock) -> lists:foldl(