diff --git a/src/hbbft.erl b/src/hbbft.erl index ac7a875..54e1813 100644 --- a/src/hbbft.erl +++ b/src/hbbft.erl @@ -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(), @@ -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()}}, @@ -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}) -> @@ -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) @@ -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}; @@ -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], @@ -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()]}}. @@ -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) @@ -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) -> @@ -655,4 +702,3 @@ decode_list(< {error, bad_chunk_encoding}. - diff --git a/test/hbbft_SUITE.erl b/test/hbbft_SUITE.erl index a4dfcd6..34b52cf 100644 --- a/test/hbbft_SUITE.erl +++ b/test/hbbft_SUITE.erl @@ -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 ]). @@ -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 ]. @@ -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, { @@ -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). @@ -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)). - -