Maintain a separate state for the purpose of CheckTx

This is required to allow block proposers to include valid sequences of transactions. CheckTx is now handled both when commiting and when gossiping, as responses to all CheckTxs need to be delivered for the client to issue a Commit request. Callback mode of ercoin_abci has been changed to faciliate the above changes.
parent 06107d2b
Pipeline #24317079 passed with stages
in 18 minutes 3 seconds
......@@ -12,5 +12,5 @@
}
],
"app_hash": "",
"app_state": "g1AAAAH9eNrL4EthYElJLElMZEz8kMiQwsBZmpeSmpaZl5qSyACCWbkMDAwKDARABlMiUwYLWOmDjzMu1RcFrP9bzufLqvV2kvGrcGH/m30q5xWLPatWLPoMVmTGfNJPRP5E7pXrptu3KBnLh5itNFmquz50p+mWd4EiK+5kMIOVWS169K38uaK9x7udUpfrjzKffeNo+OHvXIaFLd/ezZ1xwA+kiIeBsW0BkBb3KHvBANZVfZ3V90fMMltJdQNt1x9cTzee9GmzvnzwQSDbJLalia6boIYT41ABqOEgwAhig3UGvreyrfbZoXXgmbSY+IRT8czLWN+ZTD6S6V5f4TzbZJ9LBlMKA19qUXJ+Zl58al5JUX5BZQqDQFFqTmZiUk4qTAgYZowkOEXRATXIGdFpRrBRf8V0Ost665YFfjT5kfB55fqNh8WkgpRWLn+uZ3Q5rS9OETmGAVv/qfQ="
"app_state": "g1AAAAIJeNrL4E9hYElJLElMZEz8kMiQwsBZmpeSmpaZl5qSyACCWbkMDAwKDARABlMiUwYLWOmDjzMu1RcFrP9bzufLqvV2kvGrcGH/m30q5xWLPatWLPoMVmTGfNJPRP5E7pXrptu3KBnLh5itNFmquz50p+mWd4EiK+5kMIOVWS169K38uaK9x7udUpfrjzKffeNo+OHvXIaFLd/ezZ1xwA+kiIeBsW0BkBb3KHvBANZVfZ3V90fMMltJdQNt1x9cTzee9GmzvnzwQSDbJLalia6boIYT41ABqOEgwAhig3UGvreyrfbZoXXgmbSY+IRT8czLWN+ZTD6S6V5f4TzbZJ9LBlMKA19qUXJ+Zl58al5JUX5BZQqDQFFqTmZiUk4qTAgYZowkOEXRATXIGdFpRrBRf8V0Ost665YFfjT5kfB55fqNh8WkgpRWLn+uZ3Q5rS9OETmGkdkAbbquFA=="
}
......@@ -87,18 +87,24 @@
-type account() :: #account{}.
-record(
data,
%% Some default values are provided to prevent Dialyzer from complaining in tests.
{protocol=1 :: pos_integer(),
epoch_length :: 2..31536000,
epoch_length=604800 :: 2..31536000,
height=0 :: block_height() | 0,
last_epoch_end :: timestamp(),
timestamp :: timestamp(),
last_epoch_end=0 :: timestamp(),
timestamp=0 :: timestamp(),
%% Short deposit collects fees for current processing, long deposit collects fees for long term storage.
fee_deposit_short=0 :: fee(),
fee_deposit_long=0 :: fee(),
fresh_txs=?SETS:new() :: ?SETS:set(binary()),
fresh_txs_hash= <<0:256>> :: hash(),
accounts=gb_merkle_trees:empty() :: gb_merkle_trees:tree(),
entropy_fun :: {Module :: atom(), Function :: atom()},
entropy_fun={ercoin_entropy, simple_entropy} :: {Module :: atom(), Function :: atom()},
validators=gb_merkle_trees:empty() :: gb_merkle_trees:tree(),
future_validators :: undefined | {promise, docile_rpc:key(), hash()} | gb_merkle_trees:tree()}).
future_validators :: undefined | {promise, docile_rpc:key(), hash()} | gb_merkle_trees:tree(),
%% A copy of data used to maintain state for the purpose of checking transactions. It is important that checked transactions modify this state,
%% so that a block proposer can provide a valid sequence of transactions.
%%
%% We could handle the mempool connection using a separate process, but it would mean copying the data between processes on each commit (when mempool state is resetted), while when storing it in the same process we can benefit from term sharing.
mempool_data :: undefined | #data{}}).
-type data() :: #data{}.
......@@ -22,9 +22,7 @@
-export(
[callback_mode/0,
code_change/4,
uninitialized/3,
gossiping/3,
committing/3,
handle_event/4,
init/1,
terminate/3]).
......@@ -41,7 +39,7 @@ start_link() ->
gen_statem:start_link({local, ?MODULE}, ?MODULE, [], []).
callback_mode() ->
state_functions.
[handle_event_function, state_enter].
code_change(_OldVsn, OldState, OldData, _Extra) ->
{ok, OldState, OldData}.
......@@ -57,62 +55,10 @@ init(_Args) ->
terminate(_Reason, _State, _Data) ->
ok.
uninitialized({call, From}, #'RequestInitChain'{app_state_bytes=AppStateJSON}, none) ->
Data = #data{validators=Validators} = binary_to_term(base64:decode(jiffy:decode(AppStateJSON))),
TendermintValidators = [#'Validator'{pub_key=#'PubKey'{data=PK, type="ed25519"}, power=Power} || {PK, <<Power, _/binary>>} <- gb_merkle_trees:to_orddict(Validators)],
{next_state, gossiping, Data, {reply, From, #'ResponseInitChain'{validators=TendermintValidators}}};
uninitialized({call, From}, #'RequestInfo'{}, _) ->
Reply = #'ResponseInfo'{last_block_height=0, last_block_app_hash= <<>>},
{keep_state_and_data, {reply, From, Reply}}.
gossiping(internal, dump_data, Data) ->
ok = ercoin_persistence:dump_data_async(Data),
keep_state_and_data;
gossiping(
{call, From},
#'RequestBeginBlock'{
validators=SigningValidators,
header=#'Header'{time=NewTimestamp}},
Data=
#data{
height=Height,
epoch_length=EpochLength,
validators=Validators,
timestamp=OldTimestamp,
last_epoch_end=OldLastEpochEnd}) ->
Stage = Height rem EpochLength,
NewLastEpochEnd =
case Stage of
0 ->
OldTimestamp;
_ ->
OldLastEpochEnd
end,
NewValidators =
lists:foldl(
fun (#'SigningValidator'{signed_last_block=Signed, validator=#'Validator'{pub_key=#'PubKey'{data=PK}}}, Acc) ->
case Signed of
true ->
Acc;
false ->
<<Power, Absencies:3/unit:8, VoteBin/binary>> = gb_merkle_trees:lookup(PK, Validators),
NewAbsencies = Absencies + 1,
gb_merkle_trees:enter(PK, <<Power, NewAbsencies:3/unit:8, VoteBin/binary>>, Acc)
end
end,
Validators,
SigningValidators),
NewData =
Data#data{
validators=NewValidators,
timestamp=NewTimestamp,
last_epoch_end=NewLastEpochEnd},
{next_state, committing, NewData, {reply, From, #'ResponseBeginBlock'{}}};
gossiping({call, From}, Request, Data) ->
Response = gossiping(Request, Data),
{keep_state_and_data, {reply, From, Response}}.
committing({call, From}, #'RequestCommit'{}, Data=#data{height=Height, epoch_length=EpochLength}) ->
handle_event({call, From}, #'RequestCheckTx'{tx=MaybeTxBin}, _, Data) ->
{ErrorCode, NewMempoolData} = ercoin_tx:handle_bin(MaybeTxBin, Data#data.mempool_data),
{keep_state, Data#data{mempool_data=NewMempoolData}, {reply, From, #'ResponseCheckTx'{code=ErrorCode}}};
handle_event({call, From}, #'RequestCommit'{}, committing, Data=#data{height=Height, epoch_length=EpochLength}) ->
Response = #'ResponseCommit'{data=app_hash(Data)},
Reply = {reply, From, Response},
Actions =
......@@ -124,9 +70,10 @@ committing({call, From}, #'RequestCommit'{}, Data=#data{height=Height, epoch_len
[Reply]
end,
{next_state, gossiping, Data, Actions};
committing(
handle_event(
{call, From},
#'RequestEndBlock'{},
committing,
Data=
#data{
epoch_length=EpochLength,
......@@ -167,40 +114,79 @@ committing(
remove_old_and_unlock_accounts(
Data2#data{height=NewHeight, fresh_txs=NewFreshTxs}),
{keep_state, Data3, {reply, From, #'ResponseEndBlock'{validator_updates=Diffs}}};
committing({call, From}, Request, Data) when is_record(Request, 'RequestDeliverTx')->
{Response, NewData} = committing(Request, Data),
{keep_state, NewData, {reply, From, Response}};
committing({call, _}, _, _) ->
{keep_state_and_data, postpone}.
handle_event({call, From}, #'RequestDeliverTx'{tx=MaybeTxBin}, committing, Data)->
{ErrorCode, NewData} = ercoin_tx:handle_bin(MaybeTxBin, Data),
{keep_state, NewData, {reply, From, #'ResponseDeliverTx'{code=ErrorCode}}};
handle_event({call, _}, _, committing, _) ->
{keep_state_and_data, postpone};
handle_event(
{call, From},
#'RequestBeginBlock'{
validators=SigningValidators,
header=#'Header'{time=NewTimestamp}},
gossiping,
Data=
#data{
height=Height,
epoch_length=EpochLength,
validators=Validators,
timestamp=OldTimestamp,
last_epoch_end=OldLastEpochEnd}) ->
Stage = Height rem EpochLength,
NewLastEpochEnd =
case Stage of
0 ->
OldTimestamp;
_ ->
OldLastEpochEnd
end,
NewValidators =
lists:foldl(
fun (#'SigningValidator'{signed_last_block=Signed, validator=#'Validator'{pub_key=#'PubKey'{data=PK}}}, Acc) ->
case Signed of
true ->
Acc;
false ->
<<Power, Absencies:3/unit:8, VoteBin/binary>> = gb_merkle_trees:lookup(PK, Validators),
NewAbsencies = Absencies + 1,
gb_merkle_trees:enter(PK, <<Power, NewAbsencies:3/unit:8, VoteBin/binary>>, Acc)
end
end,
Validators,
SigningValidators),
NewData =
Data#data{
validators=NewValidators,
timestamp=NewTimestamp,
last_epoch_end=NewLastEpochEnd},
{next_state, committing, NewData, {reply, From, #'ResponseBeginBlock'{}}};
handle_event({call, From}, Query, gossiping, Data) when is_record(Query, 'RequestQuery') ->
{keep_state_and_data, {reply, From, ercoin_query:perform(Query, Data)}};
handle_event({call, From}, #'RequestInfo'{}, gossiping, Data) ->
Response =
#'ResponseInfo'{
last_block_app_hash=app_hash(Data),
last_block_height=Data#data.height},
{keep_state_and_data, {reply, From, Response}};
handle_event(internal, dump_data, gossiping, Data) ->
ok = ercoin_persistence:dump_data_async(Data),
keep_state_and_data;
handle_event({call, From}, #'RequestInitChain'{app_state_bytes=AppStateJSON}, uninitialized, none) ->
Data = #data{validators=Validators} = binary_to_term(base64:decode(jiffy:decode(AppStateJSON))),
TendermintValidators = [#'Validator'{pub_key=#'PubKey'{data=PK, type="ed25519"}, power=Power} || {PK, <<Power, _/binary>>} <- gb_merkle_trees:to_orddict(Validators)],
{next_state, gossiping, Data, {reply, From, #'ResponseInitChain'{validators=TendermintValidators}}};
handle_event({call, From}, #'RequestInfo'{}, uninitialized, _) ->
Reply = #'ResponseInfo'{last_block_height=0, last_block_app_hash= <<>>},
{keep_state_and_data, {reply, From, Reply}};
handle_event(enter, _, gossiping, Data) ->
NewMempoolData = Data#data{mempool_data=undefined},
{keep_state, Data#data{mempool_data=NewMempoolData}};
handle_event(enter, _, _, _) ->
keep_state_and_data.
handle_request(Request) ->
gen_statem:call(?MODULE, Request).
%% Internal functions.
-spec gossiping(abci_server:request(), data()) -> abci_server:response().
%% @doc A helper function used to return responses for gossiping requests.
gossiping(Query=#'RequestQuery'{}, Data) ->
ercoin_query:perform(Query, Data);
gossiping(#'RequestCheckTx'{tx=TxBin}, Data) ->
{Code, _} = ercoin_tx:unpack_binary(TxBin, Data),
#'ResponseCheckTx'{code=Code};
gossiping(#'RequestInfo'{}, Data) ->
#'ResponseInfo'{
last_block_app_hash=app_hash(Data),
last_block_height=height(Data)}.
-spec committing(abci_server:request(), data()) -> {abci_server:response(), data()}.
committing(#'RequestDeliverTx'{tx=TxBin}, Data) ->
case ercoin_tx:unpack_binary(TxBin, Data) of
{?OK, Tx} ->
Response = #'ResponseDeliverTx'{code=?OK},
NewData = ercoin_tx:apply(Tx, Data),
{Response, NewData};
{ErrorCode, _} ->
{#'ResponseDeliverTx'{code=ErrorCode}, Data}
end.
-spec calculate_diffs(gb_merkle_trees:tree(), gb_merkle_trees:tree()) -> list(#'Validator'{}).
calculate_diffs(New, Old) ->
KeysFun =
......@@ -218,10 +204,6 @@ calculate_diffs(New, Old) ->
end || {PK, Value} <- gb_merkle_trees:to_orddict(New) -- gb_merkle_trees:to_orddict(Old)],
NewEntries ++ DeletedEntries.
-spec height(data()) -> block_height() | 0.
height(#data{height=Height}) ->
Height.
-spec app_hash(data()) -> binary().
app_hash(
#data{
......
......@@ -13,6 +13,8 @@
-module(ercoin_persistence).
-behaviour(gen_server).
-include_lib("include/ercoin.hrl").
%% API.
-export([start_link/0]).
-export([current_data/0]).
......@@ -55,11 +57,11 @@ current_data() ->
end.
%% @doc Dump data asynchronously.
%% This function converts term to binary and schedules writing it to disc.
%% This function converts data to binary (with emptied mempool_data) and schedules writing it to disc.
-spec dump_data_async(term()) -> ok.
dump_data_async(Data) ->
%% We create a binary here to not copy large term between processes.
DataBin = term_to_binary(Data),
DataBin = term_to_binary(Data#data{mempool_data=undefined}),
gen_server:cast(?MODULE, {store_data_bin, DataBin}).
%% gen_server.
......
......@@ -23,6 +23,7 @@
from/1,
serialize/1,
unpack_binary/2,
handle_bin/2,
valid_since/1]).
-spec valid_since(tx()) -> block_height() | none.
......@@ -273,6 +274,15 @@ unpack_binary(MaybeTxBin, Data) ->
end
end.
-spec handle_bin(binary(), data()) -> {error_code(), data()}.
handle_bin(MaybeTxBin, Data) ->
case unpack_binary(MaybeTxBin, Data) of
{?OK, Tx} ->
{?OK, ercoin_tx:apply(Tx, Data)};
{Error, none} ->
{Error, Data}
end.
-spec apply(tx(), data()) -> data().
apply(Tx, Data=#data{fee_deposit_short=FeeDepositShort, fee_deposit_long=FeeDepositLong}) ->
From = ercoin_account:get(ercoin_tx:from(Tx), Data),
......
......@@ -20,7 +20,7 @@
-spec end_block(data()) -> data().
end_block(Data) ->
{keep_state, NewData, {reply, "from", #'ResponseEndBlock'{}}} =
ercoin_abci:committing({call, "from"}, #'RequestEndBlock'{}, Data),
ercoin_abci:handle_event({call, "from"}, #'RequestEndBlock'{}, committing, Data),
NewData.
-spec apply_diffs(list(#'Validator'{}), gb_merkle_trees:tree()) -> gb_merkle_trees:tree().
......@@ -53,10 +53,11 @@ prop_init_chain_sets_data_and_responds_with_validators() ->
data(),
begin
{next_state, gossiping, ResponseData, {reply, foo, #'ResponseInitChain'{validators=ResponseValidators}}} =
ercoin_abci:uninitialized(
ercoin_abci:handle_event(
{call, foo},
#'RequestInitChain'{
app_state_bytes=data_to_app_state_bytes(Data)},
uninitialized,
none),
ResponseData =:= Data andalso
ResponseValidators =:= [#'Validator'{power=Power, pub_key=#'PubKey'{data=PK, type="ed25519"}} || {PK, <<Power, _/binary>>} <- gb_merkle_trees:to_orddict(Validators)]
......@@ -67,9 +68,10 @@ prop_validators_at_end_block() ->
Data=#data{epoch_length=EpochLength, height=Height, validators=Validators, future_validators=FutureValidators},
data(),
begin
{keep_state, #data{validators=NewValidators},
{keep_state,
#data{validators=NewValidators},
{reply, "from", #'ResponseEndBlock'{validator_updates=Diffs}}} =
ercoin_abci:committing({call, "from"}, #'RequestEndBlock'{}, Data),
ercoin_abci:handle_event({call, "from"}, #'RequestEndBlock'{}, committing, Data),
NormalizationFun =
fun (Validators2) ->
gb_merkle_trees:foldr(
......@@ -227,35 +229,40 @@ prop_end_block_truncates_fresh_txs() ->
NewFreshTxs =:= ?SETS:filter(fun ({ValidSince, _Tx}) -> ValidSince >= NewHeight - Data#data.epoch_length + 2 end, FreshTxs)
end).
prop_tx_modifies_data() ->
prop_deliver_tx_handles_binary_in_regard_to_data() ->
?FORALL(
{Data, Tx},
ercoin_tx_gen:data_with_tx(),
{MaybeTxBin, Data},
oneof(
[fun ercoin_tx_gen:data_with_tx_bin/0,
fun ercoin_tx_gen:data_with_invalid_tx_bin/0]),
begin
{keep_state, NewData, {reply, "from", #'ResponseDeliverTx'{code=?OK}}} =
ercoin_abci:committing(
{keep_state, NewData, {reply, "from", #'ResponseDeliverTx'{code=ErrorCode}}} =
ercoin_abci:handle_event(
{call, "from"},
#'RequestDeliverTx'{
tx=ercoin_tx:serialize(Tx)},
Data),
NewData =:= ercoin_tx:apply(Tx, Data)
#'RequestDeliverTx'{
tx=MaybeTxBin},
committing,
Data),
{ErrorCode, NewData} =:= ercoin_tx:handle_bin(MaybeTxBin, Data)
end).
prop_binary_is_checked_as_tx() ->
prop_check_tx_handles_binary_in_regard_to_mempool_data() ->
?FORALL(
{Data, TxBin},
oneof(
[ercoin_tx_gen:data_with_tx_bin(),
ercoin_tx_gen:data_with_invalid_tx_bin()]),
{{MaybeTxBin, MempoolData},
StateName},
{oneof(
[fun ercoin_tx_gen:data_with_tx_bin/0,
fun ercoin_tx_gen:data_with_invalid_tx_bin/0]),
elements([gossiping, committing])},
begin
{keep_state_and_data, {reply, "from", #'ResponseCheckTx'{code=ResponseCode}}} =
ercoin_abci:gossiping(
{keep_state, #data{mempool_data=NewMempoolData}, {reply, "from", #'ResponseCheckTx'{code=ErrorCode}}} =
ercoin_abci:handle_event(
{call, "from"},
#'RequestCheckTx'{
tx=TxBin},
Data),
{ExpectedCode, _} = ercoin_tx:unpack_binary(TxBin, Data),
ExpectedCode =:= ResponseCode
#'RequestCheckTx'{
tx=MaybeTxBin},
StateName,
#data{mempool_data=MempoolData}),
{ErrorCode, NewMempoolData} =:= ercoin_tx:handle_bin(MaybeTxBin, MempoolData)
end).
prop_tx_puts_fee_into_fee_deposits() ->
......@@ -269,36 +276,23 @@ prop_tx_puts_fee_into_fee_deposits() ->
{keep_state,
#data{fee_deposit_short=NewFeeDepositShort, fee_deposit_long=NewFeeDepositLong},
{reply, "from", #'ResponseDeliverTx'{code=?OK}}} =
ercoin_abci:committing(
ercoin_abci:handle_event(
{call, "from"},
#'RequestDeliverTx'{
tx=ercoin_tx:serialize(Tx)},
committing,
Data),
NewFeeDepositShort =:= FeeDepositShort + FeeShort andalso
NewFeeDepositLong =:= FeeDepositLong + FeeLong
end).
prop_invalid_tx_does_not_modify_data() ->
?FORALL(
{Data, TxBin},
ercoin_tx_gen:data_with_invalid_tx_bin(),
begin
{keep_state, NewData, {reply, "from", #'ResponseDeliverTx'{}}} =
ercoin_abci:committing(
{call, "from"},
#'RequestDeliverTx'{
tx=TxBin},
Data),
Data =:= NewData
end).
prop_info_responds_with_app_hash_and_height() ->
prop_info_responds_with_app_hash_and_height_when_gossiping() ->
?FORALL(
Data=#data{height=Height},
data(),
begin
{keep_state_and_data, {reply, "from", #'ResponseInfo'{last_block_height=ResponseHeight, last_block_app_hash=AppHash}}} =
ercoin_abci:gossiping({call, "from"}, #'RequestInfo'{}, Data),
ercoin_abci:handle_event({call, "from"}, #'RequestInfo'{}, gossiping, Data),
ResponseHeight =:= Height andalso AppHash =:= ercoin_abci:app_hash(Data)
end).
......@@ -308,7 +302,7 @@ prop_commit_responds_with_app_hash() ->
data(),
begin
{next_state, gossiping, NewData, [{reply, "from", #'ResponseCommit'{data=ResponseData}}|_]} =
ercoin_abci:committing({call, "from"}, #'RequestCommit'{}, Data),
ercoin_abci:handle_event({call, "from"}, #'RequestCommit'{}, committing, Data),
ResponseData =:= ercoin_abci:app_hash(NewData)
end).
......@@ -329,7 +323,7 @@ prop_begin_block_increments_absencies() ->
Header})),
begin
{next_state, committing, #data{validators=ResponseValidators}, {reply, "from", #'ResponseBeginBlock'{}}} =
ercoin_abci:gossiping({call, "from"}, #'RequestBeginBlock'{validators=SigningValidators, header=Header}, Data),
ercoin_abci:handle_event({call, "from"}, #'RequestBeginBlock'{validators=SigningValidators, header=Header}, gossiping, Data),
lists:all(
fun (#'SigningValidator'{signed_last_block=Signed, validator=#'Validator'{pub_key=#'PubKey'{data=PK}}}) ->
<<_, Absencies:3/unit:8, _/binary>> = gb_merkle_trees:lookup(PK, Validators),
......@@ -362,7 +356,7 @@ prop_begin_block_updates_timestamps() ->
committing,
#data{timestamp=NewTimestamp, last_epoch_end=NewLastEpochEnd},
{reply, "from", #'ResponseBeginBlock'{}}} =
ercoin_abci:gossiping({call, "from"}, #'RequestBeginBlock'{header=Header}, Data),
ercoin_abci:handle_event({call, "from"}, #'RequestBeginBlock'{header=Header}, gossiping, Data),
NewTimestamp =:= Header#'Header'.time andalso NewLastEpochEnd =:=
case Stage of
0 ->
......@@ -379,6 +373,6 @@ prop_query() ->
begin
{keep_state_and_data,
{reply, "from", Response}} =
ercoin_abci:gossiping({call, "from"}, Query, Data),
ercoin_abci:handle_event({call, "from"}, Query, gossiping, Data),
ercoin_query:perform(Query, Data) =:= Response
end).
......@@ -73,6 +73,20 @@ prop_invalid_tx_bin_is_not_unpacked() ->
Code =/= ?OK andalso MaybeTx =:= none
end).
prop_handle_bin() ->
?FORALL(
{Data, MaybeTxBin},
oneof(
[fun ercoin_tx_gen:data_with_tx_bin/0,
fun ercoin_tx_gen:data_with_invalid_tx_bin/0]),
case ercoin_tx:unpack_binary(MaybeTxBin, Data) of
{?OK, Tx} ->
{?OK, ercoin_tx:apply(Tx, Data)} =:=
ercoin_tx:handle_bin(MaybeTxBin, Data);
{Error, none} ->
{Error, Data} =:= ercoin_tx:handle_bin(MaybeTxBin, Data)
end).
prop_vote_tx_changes_vote() ->
?FORALL(
{{Data=#data{height=Height, epoch_length=EpochLength}, _}, Tx},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment