diff --git a/src/blockchain_election.erl b/src/blockchain_election.erl index d73c38e11b..9d95ff3479 100644 --- a/src/blockchain_election.erl +++ b/src/blockchain_election.erl @@ -18,6 +18,8 @@ new_group(Ledger, Hash, Size, Delay) -> case blockchain_ledger_v1:config(?election_version, Ledger) of + {ok, N} when N >= 4 -> + new_group_v4(Ledger, Hash, Size, Delay); {ok, N} when N >= 3 -> new_group_v3(Ledger, Hash, Size, Delay); {ok, N} when N == 2 -> @@ -116,6 +118,39 @@ new_group_v3(Ledger, Hash, Size, Delay) -> Rem = OldGroup0 -- select(OldGroup, OldGroup, min(Remove, length(New)), RemovePct, []), Rem ++ New. +new_group_v4(Ledger, Hash, Size, Delay) -> + {ok, OldGroup0} = blockchain_ledger_v1:consensus_members(Ledger), + + {ok, SelectPct} = blockchain_ledger_v1:config(?election_selection_pct, Ledger), + {ok, RemovePct} = blockchain_ledger_v1:config(?election_removal_pct, Ledger), + {ok, ClusterRes} = blockchain_ledger_v1:config(?election_cluster_res, Ledger), + %% a version of the filter that just gives everyone the same score + Gateways0 = noscore_gateways_filter(ClusterRes, Ledger), + + OldLen = length(OldGroup0), + {Remove, Replace} = determine_sizes(Size, OldLen, Delay, Ledger), + + %% remove dupes, sort + {OldGroupDeduped, Gateways1} = dedup(OldGroup0, Gateways0, Ledger), + + %% adjust for bbas and seen votes + OldGroupAdjusted = adjust_old_group(OldGroupDeduped, Ledger), + + %% get the locations of the current consensus group at a particular h3 resolution + Locations = locations(OldGroup0, Gateways0), + + %% deterministically set the random seed + blockchain_utils:rand_from_hash(Hash), + %% random shuffle of all gateways + Gateways = blockchain_utils:shuffle(Gateways1), + New = select(Gateways, Gateways, min(Replace, length(Gateways)), SelectPct, [], Locations), + + %% sort low to high to prioritize low scoring and down gateways + %% for removal from the group + OldGroup = lists:sort(OldGroupAdjusted), + Rem = OldGroup0 -- select(OldGroup, OldGroup, min(Remove, length(New)), RemovePct, []), + Rem ++ New. + %% all blocks other than the first block in an election epoch contain %% information about the nodes that consensus members saw during the %% course of the block, and also information about which BBAs were @@ -297,8 +332,59 @@ score_dedup(OldGroup0, Gateways0, Ledger) -> true -> OldGw = case Missing of - %% make sure that non-functioning - %% nodes sort first regardless of score + %% sub 5 to make sure that non-functioning nodes sort first regardless + %% of score, making them most likely to be deselected + true -> + {Score - 5, Loc, Addr}; + _ -> + {Score, Loc, Addr} + end, + {[OldGw | Old], Candidates}; + _ -> + case Missing of + %% don't bother to add to the candidate list + true -> + Acc; + _ -> + {Old, [{Score, Loc, Addr} | Candidates]} + end + end + end, + {[], []}, + Gateways0). + +%% we do some duplication here because score is expensive to calculate, so write alternate code +%% paths to avoid it + +noscore_gateways_filter(ClusterRes, Ledger) -> + {ok, Height} = blockchain_ledger_v1:current_height(Ledger), + blockchain_ledger_v1:cf_fold( + active_gateways, + fun({Addr, BinGw}, Acc) -> + Gw = blockchain_ledger_gateway_v2:deserialize(BinGw), + Last0 = last(blockchain_ledger_gateway_v2:last_poc_challenge(Gw)), + Last = Height - Last0, + Loc = location(ClusterRes, Gw), + %% instead of getting the score, start at 1.0 for all spots + %% we need something like a score for sorting the existing consensus group members + %% for performance grading + maps:put(Addr, {Last, Loc, 1.0}, Acc) + end, + #{}, + Ledger). + +dedup(OldGroup0, Gateways0, Ledger) -> + PoCInterval = blockchain_utils:challenge_interval(Ledger), + + maps:fold( + fun(Addr, {Last, Loc, Score}, {Old, Candidates} = Acc) -> + Missing = Last > 3 * PoCInterval, + case lists:member(Addr, OldGroup0) of + true -> + OldGw = + case Missing of + %% sub 5 to make sure that non-functioning nodes sort first regardless + %% of score, making them most likely to be deselected true -> {Score - 5, Loc, Addr}; _ -> diff --git a/src/transactions/v1/blockchain_txn_vars_v1.erl b/src/transactions/v1/blockchain_txn_vars_v1.erl index 702635dc25..fed4f76b1f 100644 --- a/src/transactions/v1/blockchain_txn_vars_v1.erl +++ b/src/transactions/v1/blockchain_txn_vars_v1.erl @@ -720,6 +720,7 @@ validate_var(?election_version, Value) -> undefined -> ok; 2 -> ok; 3 -> ok; + 4 -> ok; _ -> throw({error, {invalid_election_version, Value}}) end; diff --git a/test/blockchain_simple_SUITE.erl b/test/blockchain_simple_SUITE.erl index 334a15f82c..508b6be512 100644 --- a/test/blockchain_simple_SUITE.erl +++ b/test/blockchain_simple_SUITE.erl @@ -30,6 +30,7 @@ epoch_reward_test/1, election_test/1, election_v3_test/1, + election_v4_test/1, chain_vars_test/1, chain_vars_set_unset_test/1, token_burn_test/1, @@ -78,6 +79,7 @@ all() -> epoch_reward_test, election_test, election_v3_test, + election_v4_test, chain_vars_test, chain_vars_set_unset_test, token_burn_test, @@ -107,6 +109,10 @@ init_per_testcase(TestCase, Config) -> #{election_version => 3, election_bba_penalty => 0.01, election_seen_penalty => 0.03}; + election_v4_test -> + #{election_version => 4, + election_bba_penalty => 0.01, + election_seen_penalty => 0.03}; _ -> #{allow_zero_amount => false, max_open_sc => 2, @@ -1622,6 +1628,145 @@ election_v3_test(Config) -> ?assertEqual(ControlScore, SevenScore), ok. +election_v4_test(Config) -> + BaseDir = ?config(base_dir, Config), + + ConsensusMembers = ?config(consensus_members, Config), + GenesisMembers = ?config(genesis_members, Config), + BaseDir = ?config(base_dir, Config), + %% Chain = ?config(chain, Config), + Chain = blockchain_worker:blockchain(), + N = 7, + + %% make sure our generated alpha & beta values are the same each time + rand:seed(exs1024s, {1, 2, 234098723564079}), + Ledger = blockchain:ledger(Chain), + + %% add random alpha and beta to gateways + Ledger1 = blockchain_ledger_v1:new_context(Ledger), + + [begin + {ok, I} = blockchain_gateway_cache:get(Addr, Ledger1), + Alpha = 1.0 + rand:uniform(20), + Beta = 1.0 + rand:uniform(4), + I2 = blockchain_ledger_gateway_v2:set_alpha_beta_delta(Alpha, Beta, 1, I), + blockchain_ledger_v1:update_gateway(I2, Addr, Ledger1) + end + || {Addr, _} <- GenesisMembers], + ok = blockchain_ledger_v1:commit_context(Ledger1), + + %% we need to add some blocks here. they have to have seen + %% values and bbas. + %% index 5 will be entirely absent + %% index 6 will be talking (seen) but missing from bbas (maybe + %% byzantine, maybe just missing/slow on too many packets to + %% finish anything) + %% index 7 will be bba-present, but only partially seen, and + %% should not be penalized + + %% it's possible to test unseen but bba-present here, but that seems impossible? + + SeenA = maps:from_list([{I, case I of 5 -> false; 7 -> true; _ -> true end} + || I <- lists:seq(1, N)]), + SeenB = maps:from_list([{I, case I of 5 -> false; 7 -> false; _ -> true end} + || I <- lists:seq(1, N)]), + Seen0 = lists:duplicate(4, SeenA) ++ lists:duplicate(2, SeenB), + + BBA0 = maps:from_list([{I, case I of 5 -> false; 6 -> false; _ -> true end} + || I <- lists:seq(1, N)]), + + {_, Seen} = + lists:foldl(fun(S, {I, Acc})-> + V = blockchain_utils:map_to_bitvector(S), + {I + 1, [{I, V} | Acc]} + end, + {1, []}, + Seen0), + BBA = blockchain_utils:map_to_bitvector(BBA0), + + %% maybe these should vary more? + + BlockCt = 50, + + lists:foreach( + fun(_) -> + {ok, Block} = test_utils:create_block(ConsensusMembers, [], #{seen_votes => Seen, + bba_completion => BBA}), + _ = blockchain_gossip_handler:add_block(Block, Chain, self(), blockchain_swarm:swarm()) + end, + lists:seq(1, BlockCt) + ), + + {ok, OldGroup} = blockchain_ledger_v1:consensus_members(Ledger), + ct:pal("old ~p", [OldGroup]), + + %% generate new group of the same length + New = blockchain_election:new_group(Ledger, crypto:hash(sha256, "foo"), N, 0), + New1 = blockchain_election:new_group(Ledger, crypto:hash(sha256, "foo"), N, 1000), + + ct:pal("new ~p new1 ~p", [New, New1]), + + ?assertEqual(N, length(New)), + ?assertEqual(N, length(New1)), + + ?assertNotEqual(OldGroup, New), + ?assertNotEqual(OldGroup, New1), + ?assertNotEqual(New, New1), + + Scored = + [begin + {ok, I} = blockchain_gateway_cache:get(Addr, Ledger), + {_, _, Score} = blockchain_ledger_gateway_v2:score(Addr, I, 1, Ledger), + {Score, Addr} + end + || Addr <- New], + + %% it should be really unlikely that they're sorted by score now + ?assertNotEqual(lists:reverse(lists:sort(Scored)), Scored), + + ScoredOldGroup = + [begin + {ok, I} = blockchain_gateway_cache:get(Addr, Ledger), + %% this is at the wrong res but it shouldn't matter? + Loc = blockchain_ledger_gateway_v2:location(I), + {_, _, Score} = blockchain_ledger_gateway_v2:score(Addr, I, 1, Ledger), + {Score, Loc, Addr} + end + || Addr <- OldGroup], + + + ct:pal("scored ~p", [Scored]), + + %% no dupes + ?assertEqual(lists:usort(Scored), lists:sort(Scored)), + + ?assertEqual(1, length(New -- OldGroup)), + + Adjusted = blockchain_election:adjust_old_group(ScoredOldGroup, Ledger), + + ct:pal("adjusted ~p", [Adjusted]), + + {FiveScore, _, _} = lists:nth(5, Adjusted), + {SixScore, _, _} = lists:nth(6, Adjusted), + {SevenScore, _, _} = lists:nth(7, Adjusted), + + {ok, BBAPenalty} = blockchain_ledger_v1:config(?election_bba_penalty, Ledger), + {ok, SeenPenalty} = blockchain_ledger_v1:config(?election_seen_penalty, Ledger), + + %% five should have taken both hits + FiveTarget = normalize_float(element(1, lists:nth(5, ScoredOldGroup)) - + normalize_float((BlockCt * BBAPenalty + BlockCt * SeenPenalty))), + ?assertEqual(FiveTarget, FiveScore), + + %% six should have taken only the BBA hit + SixTarget = normalize_float(element(1, lists:nth(6, ScoredOldGroup)) + - (BlockCt * BBAPenalty)), + ?assertEqual(SixTarget, SixScore), + + %% seven should not have been penalized + ?assertEqual(element(1, lists:nth(7, ScoredOldGroup)), SevenScore), + + ok. chain_vars_test(Config) -> ConsensusMembers = ?config(consensus_members, Config),