Skip to content

Commit

Permalink
Merge pull request #62 from helium/xandkar/ignore-oversize-txn-batches
Browse files Browse the repository at this point in the history
Ignore oversize transaction batches
  • Loading branch information
xandkar authored Mar 4, 2021
2 parents 866c7b7 + ee2c80f commit 480ff1d
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 16 deletions.
74 changes: 60 additions & 14 deletions src/hbbft.erl
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,17 @@
is_serialized/1]).

-ifdef(TEST).
-export([get_encrypted_key/2]).
-export([
encode_list/1,
get_encrypted_key/2,
abstraction_breaking_set_acs_results/2,
abstraction_breaking_set_enc_keys/2
]).
-endif.

-type acs_results() :: [{non_neg_integer(), binary()}].
-type enc_keys() :: #{non_neg_integer() => tpke_pubkey:ciphertext()}.

-record(hbbft_data, {
batch_size :: pos_integer(),
secret_key :: undefined | tpke_privkey:privkey(),
Expand All @@ -38,8 +46,8 @@
acs_init = false :: boolean(),
sent_txns = false :: boolean(),
sent_sig = false :: boolean(),
acs_results = [] :: [{non_neg_integer(), binary()}],
enc_keys = #{} :: #{non_neg_integer() => tpke_pubkey:ciphertext()}, %% will only ever hold verified ciphertexts
acs_results = [] :: acs_results(),
enc_keys = #{} :: enc_keys(), %% will only ever hold verified ciphertexts
dec_shares = #{} :: #{{non_neg_integer(), non_neg_integer()} => {boolean() | undefined, {non_neg_integer(), erlang_pbc:element()}}},
decrypted = #{} :: #{non_neg_integer() => [binary()]},
sig_shares = #{} :: #{non_neg_integer() => {non_neg_integer(), erlang_pbc:element()}},
Expand Down Expand Up @@ -110,6 +118,20 @@ init(SK, N, F, J, BatchSize, MaxBuf, StampFun, Round, Buf) ->
max_buf=MaxBuf,
stampfun=StampFun}.

-ifdef(TEST).
-spec abstraction_breaking_set_acs_results(State, acs_results()) ->
State
when State :: hbbft_data().
abstraction_breaking_set_acs_results(State, AcsResults) ->
State#hbbft_data{acs_results=AcsResults}.

-spec abstraction_breaking_set_enc_keys(State, enc_keys()) ->
State
when State :: hbbft_data().
abstraction_breaking_set_enc_keys(State, EncKeys) ->
State#hbbft_data{enc_keys=EncKeys}.
-endif.


-spec get_stamp_fun(hbbft_data()) -> {atom(), atom(), list()} | undefined.
get_stamp_fun(#hbbft_data{stampfun=S}) ->
Expand All @@ -131,7 +153,7 @@ start_on_demand(Data = #hbbft_data{buf=Buf, j=J, n=N, secret_key=SK, batch_size=
{M, F, A} -> erlang:apply(M, F, A)
end,
true = is_binary(Stamp),
EncX = encrypt(tpke_privkey:public_key(SK), encode_list([Stamp|Proposed], [])),
EncX = encrypt(tpke_privkey:public_key(SK), encode_list([Stamp|Proposed])),
%% time to kick off a round
{NewACSState, {send, ACSResponse}} = hbbft_acs:input(Data#hbbft_data.acs, EncX),
%% add this to acs set in data and send out the ACS response(s)
Expand Down Expand Up @@ -216,14 +238,22 @@ buf(_Data=#hbbft_data{buf = Buf}) ->
buf(Buf, Data) ->
Data#hbbft_data{buf=Buf}.

-spec handle_msg(hbbft_data(), non_neg_integer(), acs_msg() | dec_msg() | sign_msg()) -> {hbbft_data(), ok |
defer |
{send, [hbbft_utils:multicast(dec_msg() |
sign_msg()) |
rbc_wrapped_output() |
bba_wrapped_output()]} |
{result, {transactions, list(), [binary()]}} |
{result, {signature, binary()}}} | ignore.
-spec handle_msg(State, J :: non_neg_integer(), Msg) ->
{State, Next} | ignore
when State :: hbbft_data(),
Msg :: acs_msg() | dec_msg() | sign_msg(),
Next
:: ok
| defer
| {send, [NextMsg]}
| {result, Result},
NextMsg
:: hbbft_utils:multicast(dec_msg() | sign_msg())
| rbc_wrapped_output()
| bba_wrapped_output(),
Result
:: {signature, binary()}
| {transactions, list(), [binary()]}.
handle_msg(Data = #hbbft_data{round=R}, _J, {{acs, R2}, _ACSMsg}) when R2 > R ->
%% ACS requested we defer this message for now
{Data, defer};
Expand Down Expand Up @@ -346,7 +376,20 @@ handle_msg(Data = #hbbft_data{round=R}, J, {dec, R, I, Share}) ->
check_completion(Data#hbbft_data{dec_shares=NewShares, decrypted=NewDecrypted,
failed_decrypt=[I|Data#hbbft_data.failed_decrypt]});
Decrypted ->
#hbbft_data{batch_size=B, n=N} = Data,
case decode_list(Decrypted, []) of
[_Stamp | Transactions]
when length(Transactions) > (B div N) ->
% Batch exceeds agreed-upon size.
% Ignoring this proposal.
check_completion(
Data#hbbft_data{
dec_shares =
NewShares,
decrypted =
maps:put(I, [], Data#hbbft_data.decrypted)
}
);
[Stamp | Transactions] ->
NewDecrypted = maps:put(I, Transactions, Data#hbbft_data.decrypted),
Stamps = [{I, Stamp} | Data#hbbft_data.stamps],
Expand Down Expand Up @@ -399,6 +442,7 @@ handle_msg(Data = #hbbft_data{round=R, thingtosign=ThingToSign}, J, {sign, R, Bi
ignore
end;
handle_msg(_Data, _J, _Msg) ->
% TODO Consider either crashing or returning {ok, _} | {error, _} result.
ignore.

-spec maybe_start_acs(hbbft_data()) -> {hbbft_data(), ok | {send, [rbc_wrapped_output()]}}.
Expand All @@ -416,7 +460,7 @@ maybe_start_acs(Data = #hbbft_data{n=N, j=J, secret_key=SK, batch_size=BatchSize
{M, F, A} -> erlang:apply(M, F, A)
end,
true = is_binary(Stamp),
EncX = encrypt(tpke_privkey:public_key(SK), encode_list([Stamp|Proposed], [])),
EncX = encrypt(tpke_privkey:public_key(SK), encode_list([Stamp|Proposed])),
%% time to kick off a round
{NewACSState, {send, ACSResponse}} = hbbft_acs:input(Data#hbbft_data.acs, EncX),
%% add this to acs set in data and send out the ACS response(s)
Expand Down Expand Up @@ -638,6 +682,9 @@ combine_shares(F, SK, SharesForThisBundle, EncKey) ->
undefined
end.

encode_list(L) ->
encode_list(L, []).

encode_list([], Acc) ->
list_to_binary(lists:reverse(Acc));
encode_list([H|T], Acc) ->
Expand All @@ -655,4 +702,3 @@ decode_list(<<Length:24/integer-unsigned-little, Entry:Length/binary, Tail/binar
decode_list(Tail, [Entry|Acc]);
decode_list(_, _Acc) ->
{error, bad_chunk_encoding}.

97 changes: 95 additions & 2 deletions test/hbbft_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
start_on_demand_test/1,
one_actor_wrong_key_test/1,
one_actor_corrupted_key_test/1,
one_actor_oversized_batch_test/1,
batch_size_limit_minimal_test/1,
initial_fakecast_test/1
]).

Expand All @@ -29,6 +31,8 @@ all() ->
start_on_demand_test,
one_actor_wrong_key_test,
one_actor_corrupted_key_test,
one_actor_oversized_batch_test,
batch_size_limit_minimal_test,
initial_fakecast_test
].

Expand Down Expand Up @@ -343,6 +347,95 @@ one_actor_corrupted_key_test(Config) ->
ct:log("chain contains ~p distinct transactions~n", [length(BlockTxns)]),
ok.

one_actor_oversized_batch_test(Config) ->
N = proplists:get_value(n, Config),
F = proplists:get_value(f, Config),
B = proplists:get_value(batchsize, Config),
PK = proplists:get_value(pubkey, Config),
[SK1 | SKs] = proplists:get_value(privatekeys, Config),

BadWorker = % with an oversized batch parameter
ok(hbbft_worker:start_link(N, F, 0, tpke_privkey:serialize(SK1), B + 1, false)),
GoodWorkers =
[ok(hbbft_worker:start_link(N, F, I + 1, tpke_privkey:serialize(SKi), B, false))
|| {I, SKi} <- enumerate(SKs)
],

% Each node needs at least B/N transactions.
GoodTxns = [list_to_binary("GOOD_" ++ integer_to_list(I)) || I <- lists:seq(1, B * N)],
BadTxns = [list_to_binary("BAD_" ++ integer_to_list(I)) || I <- lists:seq(1, B)],

% Submit transactions:
lists:foreach(
fun (T) -> ok = hbbft_worker:submit_transaction(T, BadWorker) end,
BadTxns),
lists:foreach(
fun (T) ->
Destinations = random_n(rand:uniform(N), GoodWorkers),
[ok = hbbft_worker:submit_transaction(T, D) || D <- Destinations]
end,
GoodTxns),

% Wait for all the worker's mailboxes to settle:
ok = wait_for_chains([BadWorker | GoodWorkers], 2),

% Wait for the chains to converge:
Chains = get_common_chain(GoodWorkers),
1 = sets:size(Chains),
[Chain] = sets:to_list(Chains),
CommittedTxns =
lists:flatten([hbbft_worker:block_transactions(Block) || Block <- Chain]),

% Transactions are cryptographically linked:
?assertMatch(true, hbbft_worker:verify_chain(Chain, PK)),

% Transactions are unique:
?assertMatch([], CommittedTxns -- lists:usort(CommittedTxns)),

% Finally, quod erat demonstrandum - that
% only the expected transactions were committed:
?assertMatch([], CommittedTxns -- GoodTxns),
ok.

batch_size_limit_minimal_test(_) ->
% Same test goal as one_actor_oversized_batch_test, but
% the absolute minimal to test the state transition.
N = 1,
F = 0,
BatchSize = 1,
{ok, Dealer} = dealer:new(N, F + 1, 'SS512'),
{ok, {PK, [SK | _]}} = dealer:deal(Dealer),

% Protocol begins.
ProtocolInstanceId = 0,
State_0 = hbbft:init(SK, N, F, ProtocolInstanceId, BatchSize, infinity),

% Transactions submitted. One more than max batch size.
Buf = [list_to_binary(integer_to_list(Txn)) || Txn <- lists:seq(1, BatchSize + 1)],

% Pretending ACS happened here.
Stamp = <<"trust-me-im-a-stamp">>,
Enc = hbbft:encrypt(PK, hbbft:encode_list([Stamp | Buf])),
{ok, EncKey} = hbbft:get_encrypted_key(SK, Enc),
AcsInstanceId = 0, % E?
State_1 = hbbft:abstraction_breaking_set_acs_results(State_0, [{AcsInstanceId, Enc}]),
State_2 = hbbft:abstraction_breaking_set_enc_keys(State_1, #{AcsInstanceId => EncKey}),

% Decoding transactions from ACS, which we expect to be rejectected.
{State_3, Result} =
hbbft:handle_msg(
State_2,
ProtocolInstanceId,
{
dec,
hbbft:round(State_2),
AcsInstanceId,
hbbft_utils:share_to_binary(tpke_privkey:decrypt_share(SK, EncKey))
}
),
?assertMatch({result, {transactions, [], []}}, Result),
?assertMatch(#{sent_txns := true}, hbbft:status(State_3)),
ok.

-record(state,
{
Expand Down Expand Up @@ -418,6 +511,8 @@ initial_fakecast_test(Config) ->

%% helper functions

ok({ok, X}) -> X.

enumerate(List) ->
lists:zip(lists:seq(0, length(List) - 1), List).

Expand Down Expand Up @@ -465,5 +560,3 @@ get_common_chain(Workers) ->

%% chains are stored in reverse, so we have to reverse them to get the first N blocks and then re-reverse them to restore expected ordering
sets:from_list(lists:map(fun(C) -> lists:reverse(lists:sublist(lists:reverse(C), ShortestChainLen)) end, AllChains)).


0 comments on commit 480ff1d

Please sign in to comment.