Skip to content
This repository has been archived by the owner on Mar 5, 2024. It is now read-only.

HIP 16 implementation #676

Merged
merged 3 commits into from
Nov 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 88 additions & 2 deletions src/blockchain_election.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
evanmcc marked this conversation as resolved.
Show resolved Hide resolved
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};
evanmcc marked this conversation as resolved.
Show resolved Hide resolved
_ ->
Expand Down
1 change: 1 addition & 0 deletions src/transactions/v1/blockchain_txn_vars_v1.erl
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@ validate_var(?election_version, Value) ->
undefined -> ok;
2 -> ok;
3 -> ok;
4 -> ok;
_ ->
throw({error, {invalid_election_version, Value}})
end;
Expand Down
145 changes: 145 additions & 0 deletions test/blockchain_simple_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down