Skip to content

Commit

Permalink
Add state gas cost for FATE (#2706)
Browse files Browse the repository at this point in the history
* Add state gas cost for FATE

* Calculate cost based on the actual gas for GA tests

* Used gas increased in test case
  • Loading branch information
gorillainduction committed Aug 27, 2019
1 parent 1227196 commit 43f1c27
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 69 deletions.
75 changes: 70 additions & 5 deletions apps/aecontract/test/aecontract_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@
, sophia_aens_transactions/1
, sophia_state_handling/1
, sophia_remote_state/1
, sophia_state_gas/1
, sophia_state_gas_arguments/1
, sophia_state_gas_store_size/1
, sophia_no_callobject_for_remote_calls/1
, sophia_operators/1
, sophia_bits/1
Expand Down Expand Up @@ -272,7 +273,7 @@ all() ->
]).

-define(FATE_TODO, [ {group, sophia_oracles_gas_ttl}
, sophia_state_gas
, sophia_state_gas_arguments
]).

groups() ->
Expand Down Expand Up @@ -357,7 +358,8 @@ groups() ->
sophia_aens_transactions,
sophia_state_handling,
sophia_remote_state,
sophia_state_gas,
sophia_state_gas_arguments,
sophia_state_gas_store_size,
sophia_no_callobject_for_remote_calls,
sophia_operators,
sophia_bits,
Expand Down Expand Up @@ -4104,7 +4106,9 @@ sophia_maps_gc(_Cfg) ->

Prune = fun(M) -> maps:without(maps:keys(InitA), M) end,

Ct = ?call(create_contract, Acc, maps_gc, {InitA, InitB}, #{fee => 2000000 * aec_test_utils:min_gas_price()}),
Ct = ?call(create_contract, Acc, maps_gc, {InitA, InitB},
#{fee => 2000000 * aec_test_utils:min_gas_price(),
gas => 200000}),

StateT = {tuple, [{map, string, string}, {map, string, string}]},

Expand Down Expand Up @@ -5292,7 +5296,7 @@ sophia_state_handling(_Cfg) ->

ok.

sophia_state_gas(_Cfg) ->
sophia_state_gas_arguments(_Cfg) ->
state(aect_test_utils:new_state()),
Acc = ?call(new_account, 20000000 * aec_test_utils:min_gas_price()),
ContractName =
Expand Down Expand Up @@ -5339,6 +5343,67 @@ sophia_state_gas(_Cfg) ->
?assertMatch(true, Gas9 > Gas8),
ok.

sophia_state_gas_store_size(_Cfg) ->
?skipRest(not ?IS_FATE_SOPHIA(vm_version()), only_valid_for_fate),
state(aect_test_utils:new_state()),
Acc = ?call(new_account, 20000000 * aec_test_utils:min_gas_price()),
Ct0 = ?call(create_contract, Acc, remote_state, {}, #{ amount => 100000 }),
Ct1 = ?call(create_contract, Acc, state_handling, {?cid(Ct0), 1}, #{ amount => 100000 }),

UnitT = {tuple, []},

%% Test that gas usage varies when the state changes size
%% Baseline
{{}, Gas0} = ?call(call_contract, Acc, Ct1, update_m, UnitT, {#{1 => 1}}, #{ return_gas_used => true }),

%% Same state size
{{}, Gas1} = ?call(call_contract, Acc, Ct1, update_m, UnitT, {#{2 => 2}}, #{ return_gas_used => true }),
?assertEqual(Gas0, Gas1),

%% Smaller state size
{{}, Gas2} = ?call(call_contract, Acc, Ct1, update_m, UnitT, {#{}}, #{ return_gas_used => true }),
?assertEqual({true, Gas0, Gas2}, {Gas0 > Gas2, Gas0, Gas2}),

%% Bigger state size
{{}, Gas3} = ?call(call_contract, Acc, Ct1, update_m, UnitT, {#{1 => 1, 2 => 2}}, #{ return_gas_used => true }),
?assertEqual({true, Gas0, Gas3}, {Gas0 < Gas3, Gas0, Gas3}),

%% Create a map that is big enough to end up as a store map.
BigMap = maps:from_list([{X, X} || X <- lists:seq(1, 1000)]),
{{}, _} = ?call(call_contract, Acc, Ct1, update_m, UnitT, {BigMap}, #{ return_gas_used => true }),

%% Updating one key in the store map should cost the same even if the old key didn't exist.
{{}, Gas4} = ?call(call_contract, Acc, Ct1, update_mk, UnitT, {1, 2}, #{ return_gas_used => true }),
{{}, Gas5} = ?call(call_contract, Acc, Ct1, update_mk, UnitT, {2, 1}, #{ return_gas_used => true }),
{{}, Gas6} = ?call(call_contract, Acc, Ct1, update_mk, UnitT, {0, 1}, #{ return_gas_used => true }),
?assertEqual(Gas4, Gas5),
?assertEqual(Gas5, Gas6),

%% Updating one key in the store map should cost more if the value takes up more space
{{}, Gas7} = ?call(call_contract, Acc, Ct1, update_mk, UnitT, {0, 257}, #{ return_gas_used => true }),
?assertEqual({true, Gas6, Gas7}, {Gas6 < Gas7, Gas6, Gas7}),

%% Updating one key in the store map should cost more if the key takes up more space
{{}, Gas8} = ?call(call_contract, Acc, Ct1, update_mk, UnitT, {257, 0}, #{ return_gas_used => true }),
?assertEqual({true, Gas6, Gas8}, {Gas6 < Gas8, Gas6, Gas8}),

%% Test that gas usage varies when the state changes size for the integer component
%% Baseline
{{}, Gas9} = ?call(call_contract, Acc, Ct1, update_i, UnitT, {256}, #{ return_gas_used => true }),

%% Same state size
{{}, Gas10} = ?call(call_contract, Acc, Ct1, update_i, UnitT, {257}, #{ return_gas_used => true }),
?assertEqual(Gas9, Gas10),

%% Smaller state size
{{}, Gas11} = ?call(call_contract, Acc, Ct1, update_i, UnitT, {0}, #{ return_gas_used => true }),
?assertEqual({true, Gas10, Gas11}, {Gas10 > Gas11, Gas10, Gas11}),

%% Bigger state size
{{}, Gas12} = ?call(call_contract, Acc, Ct1, update_i, UnitT, {1024}, #{ return_gas_used => true }),
?assertEqual({true, Gas10, Gas12}, {Gas10 < Gas12, Gas10, Gas12}),

ok.

%%%===================================================================
%%% Store
Expand Down
10 changes: 8 additions & 2 deletions apps/aefate/src/aefa_engine_state.erl
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,15 @@ new(Gas, Value, Spec, Stores, APIState, CodeCache) ->
, trace = []
}.

-spec finalize(state()) -> state().
-spec finalize(state()) -> {ok, state()} | {error, out_of_gas}.
finalize(#es{chain_api = API, stores = Stores} = ES) ->
ES#es{chain_api = aefa_stores:finalize(API, Stores)}.
Gas = gas(ES),
case aefa_stores:finalize(API, Gas, Stores) of
{ok, Stores1, GasLeft} ->
{ok, ES#es{chain_api = Stores1, gas = GasLeft}};
{error, out_of_gas} ->
{error, out_of_gas}
end.

%%%===================================================================
%%% API
Expand Down
5 changes: 4 additions & 1 deletion apps/aefate/src/aefa_fate.erl
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,10 @@ execute(EngineState) ->
loop(Instructions, EngineState) ->
case step(Instructions, EngineState) of
{stop, FinalState} ->
aefa_engine_state:finalize(FinalState);
case aefa_engine_state:finalize(FinalState) of
{ok, ES} -> ES;
{error, What} -> abort(What, FinalState)
end;
{jump, BB, NewState} ->
{NewInstructions, State2} = jump(BB, NewState),
loop(NewInstructions, State2)
Expand Down
82 changes: 57 additions & 25 deletions apps/aefate/src/aefa_stores.erl
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
%%% altered. Both things are expensive as it involves going out to the
%%% underlying merkle trees.
%%%
%%% Use finalize/2 to push the stores back to the chain when the
%%% Use finalize/3 to push the stores back to the chain when the
%%% fate execution is done.
%%%
%%% @end
Expand All @@ -22,7 +22,7 @@

-include_lib("aebytecode/include/aeb_fate_data.hrl").

-export([ finalize/2
-export([ finalize/3
, find_value/3
, has_contract/2
, initial_contract_store/0
Expand Down Expand Up @@ -277,11 +277,19 @@ map_raw_key(RawId, KeyBin) ->
%%%===================================================================
%%% Write through cache to stores

-spec finalize(aefa_chain_api:state(), store()) -> aefa_chain_api:state().
finalize(API, #store{cache = Cache} = _S) ->
-spec finalize(aefa_chain_api:state(), non_neg_integer(), store()) ->
{ok, aefa_chain_api:state(), non_neg_integer()}
| {error, out_of_gas}.
finalize(API, GasLeft, #store{cache = Cache} = _S) ->
?DEBUG_STORE(_S),
Stores = maps:fold(fun finalize_entry/3, [], Cache),
finalize_stores(Stores, API).
try maps:fold(fun finalize_entry/3, {[], GasLeft}, Cache) of
{Stores, GasLeft1} ->
API1 = finalize_stores(Stores, API),
{ok, API1, GasLeft1}
catch
throw:out_of_gas ->
{error, out_of_gas}
end.

finalize_stores([{Pubkey, Store}|Left], API) ->
API1 = aefa_chain_api:set_contract_store(Pubkey, Store, API),
Expand All @@ -291,7 +299,7 @@ finalize_stores([], API) ->

finalize_entry(_Pubkey, #cache_entry{dirty = false}, Acc) ->
Acc;
finalize_entry(Pubkey, Cache = #cache_entry{store = Store}, Acc) ->
finalize_entry(Pubkey, Cache = #cache_entry{store = Store}, {Writes, GasLeft}) ->
{ok, Metadata} = find_meta_data_no_cache(Cache), %% Last access so no need to cache here
%% Compute which updates need to be performed (see store_update() type
%% below). This also takes care of updating the metadata with new reference
Expand All @@ -301,7 +309,8 @@ finalize_entry(Pubkey, Cache = #cache_entry{store = Store}, Acc) ->
?DEBUG_PRINT("Updates\n ~p\n", [Updates]),

%% Performing the updates writes the necessary changes to the MP trees.
[{Pubkey, perform_store_updates(Updates, Metadata1, Store)} | Acc].
{Store1, GasLeft1} = perform_store_updates(Updates, Metadata1, GasLeft, Store),
{[{Pubkey, Store1} | Writes], GasLeft1}.

%%%===================================================================
%%% Store updates
Expand Down Expand Up @@ -353,15 +362,31 @@ compute_store_updates(Metadata, #cache_entry{terms = TermCache, store = Store})
Compare = fun(A, B) -> Order(element(1, A)) =< Order(element(1, B)) end,
{Metadata2, lists:sort(Compare, Updates)}.

perform_store_updates(Updates, Meta, Store) ->
{Meta1, Store1} = lists:foldl(fun perform_store_update/2, {Meta, Store}, Updates),
perform_store_updates([Update|Left], Meta, GasLeft, Store) ->
?DEBUG_PRINT("Update: ~p\n", [Update]),
{Meta1, Bytes, Store1} = perform_store_update(Update, {Meta, Store}),
GasLeft1 = spend_size_gas(GasLeft, Bytes),
perform_store_updates(Left, Meta1, GasLeft1, Store1);
perform_store_updates([], Meta, GasLeft, Store) ->
%% Save the updated metadata at the end
push_term(?META_STORE_POS, Meta1, Store1).
{Store1, Bytes} = push_term(?META_STORE_POS, Meta, Store),
GasLeft1 = spend_size_gas(GasLeft, Bytes),
{Store1, GasLeft1}.

spend_size_gas(GasLeft, Bytes) ->
?DEBUG_PRINT("GasLeft: ~w Bytes: ~w\n", [GasLeft, Bytes]),
case GasLeft - Bytes * aec_governance:byte_gas() of
TooLittle when TooLittle < 0 ->
throw(out_of_gas);
Enough ->
Enough
end.

-spec perform_store_update(store_update(), {store_meta(), aect_contracts_store:store()}) ->
{store_meta(), aect_contracts_store:store()}.
{store_meta(), non_neg_integer(), aect_contracts_store:store()}.
perform_store_update({push_term, StorePos, FateVal}, {Meta, Store}) ->
{Meta, push_term(StorePos, FateVal, Store)};
{Store1, Bytes} = push_term(StorePos, FateVal, Store),
{Meta, Bytes, Store1};
perform_store_update({copy_map, MapId, Map}, S) ->
copy_map(MapId, Map, S);
perform_store_update({update_map, MapId, Map}, S) ->
Expand All @@ -372,7 +397,9 @@ perform_store_update({gc_map, MapId}, S) ->
%% Write to a store register
push_term(Pos, FateVal, Store) ->
Val = aeb_fate_encoding:serialize(FateVal),
aect_contracts_store:put(store_key(Pos), Val, Store).
Key = store_key(Pos),
Bytes = byte_size(Key) + byte_size(Val),
{aect_contracts_store:put(Key, Val, Store), Bytes}.

%% Allocate a new map.
copy_map(MapId, Map, {Meta, Store}) when ?IS_FATE_MAP(Map) ->
Expand All @@ -384,10 +411,10 @@ copy_map(MapId, Map, {Meta, Store}) when ?IS_FATE_MAP(Map) ->
Meta1 = put_map_meta(MapId, ?METADATA(RawId, RefCount, Size), Meta),
%% Write the data
BinData = cache_to_bin_data(?FATE_MAP_VALUE(Map)), %% A map value is a special case of a cache (no tombstones)
Store1 = write_bin_data(RawId, BinData, Store),
{Store1, Bytes} = write_bin_data(RawId, BinData, Store),
%% and the subtree node that allows us to call aect_contracts_store:subtree
Store2 = aect_contracts_store:put(map_data_key(RawId), <<0>>, Store1),
{Meta1, Store2};
{Meta1, Bytes, Store2};
copy_map(MapId, ?FATE_STORE_MAP(Cache, OldId), {Meta, Store}) ->
%% In case of a modified store map we need to copy all the entries for the
%% old map and then update with the new data (Cache).
Expand All @@ -399,7 +426,7 @@ copy_map(MapId, ?FATE_STORE_MAP(Cache, OldId), {Meta, Store}) ->
Size = OldSize + size_delta(OldMap, NewData),
Meta1 = put_map_meta(MapId, ?METADATA(RawId, RefCount, Size), Meta),
%% First copy the old data, then update with the new
Store1 = write_bin_data(RawId, maps:to_list(OldMap) ++ NewData, Store),
{Store1, Bytes} = write_bin_data(RawId, maps:to_list(OldMap) ++ NewData, Store),
Store2 = aect_contracts_store:put(map_data_key(RawId), <<0>>, Store1),

%% We also need to update the refcounts for nested maps. We already added
Expand All @@ -409,7 +436,7 @@ copy_map(MapId, ?FATE_STORE_MAP(Cache, OldId), {Meta, Store}) ->
CopiedBin = maps:without([K || {K, _} <- NewData], OldMap),
RefCounts = aeb_fate_maps:refcount([ aeb_fate_encoding:deserialize(Val) || Val <- maps:values(CopiedBin) ]),
Meta2 = update_refcounts(RefCounts, Meta1),
{Meta2, Store2}.
{Meta2, Bytes, Store2}.

%% In-place update of an existing store map. This happens, for instance, when
%% you update a map in the state throwing away the old copy of the it.
Expand All @@ -418,7 +445,7 @@ update_map(MapId, ?FATE_STORE_MAP(Cache, OldId), {Meta, Store}) ->
?METADATA(RawId, _RefCount, OldSize) = get_map_meta(OldId, Meta),
NewData = cache_to_bin_data(Cache),
Size = OldSize + size_delta(RawId, Store, NewData),
Store1 = write_bin_data(RawId, NewData, Store),
{Store1, Bytes} = write_bin_data(RawId, NewData, Store),
Meta1 = put_map_meta(MapId, ?METADATA(RawId, RefCount, Size),
remove_map_meta(OldId, Meta)),

Expand All @@ -430,15 +457,15 @@ update_map(MapId, ?FATE_STORE_MAP(Cache, OldId), {Meta, Store}) ->
Count)
end, aeb_fate_maps:refcount_zero(), NewData),
Meta2 = update_refcounts(RefCounts, Meta1),
{Meta2, Store1}.
{Meta2, Bytes, Store1}.

gc_map(RawId, {Meta, Store}) ->
%% Only the RawId here, we already removed the MapId from the metadata.
Data = aect_contracts_store:subtree(map_data_key(RawId), Store),
Store1 = maps:fold(fun(Key, _, S) -> aect_contracts_store:remove(map_raw_key(RawId, Key), S) end,
aect_contracts_store:remove(map_data_key(RawId), Store), Data),

{Meta, Store1}.
{Meta, _Bytes = 0, Store1}.

-type bin_data() :: [{binary(), binary() | ?FATE_MAP_TOMBSTONE}].
-type map_cache() :: #{fate_val() => fate_val() | ?FATE_MAP_TOMBSTONE}.
Expand All @@ -453,12 +480,17 @@ cache_to_bin_data(Cache) ->
{KeyBin, ValBin}
end || {K, V} <- maps:to_list(Cache) ].

-spec write_bin_data(raw_id(), bin_data(), aect_contracts_store:store()) -> aect_contracts_store:store().
-spec write_bin_data(raw_id(), bin_data(), aect_contracts_store:store()) ->
{aect_contracts_store:store(), non_neg_integer()}.
write_bin_data(RawId, BinData, Store) ->
lists:foldl(
fun({K, ?FATE_MAP_TOMBSTONE}, S) -> aect_contracts_store:remove(map_raw_key(RawId, K), S);
({K, V}, S) -> aect_contracts_store:put(map_raw_key(RawId, K), V, S)
end, Store, BinData).
fun({K, ?FATE_MAP_TOMBSTONE}, {S, B}) ->
{aect_contracts_store:remove(map_raw_key(RawId, K), S),
B};
({K, V}, {S, B}) ->
{aect_contracts_store:put(map_raw_key(RawId, K), V, S),
B + byte_size(K) + byte_size(V)}
end, {Store, 0}, BinData).

%% Compute the change in size updating an old map with new entries.
-spec size_delta(#{binary() => binary()}, bin_data()) -> integer().
Expand Down
3 changes: 2 additions & 1 deletion apps/aehttp/src/aehttp_dispatch_ext.erl
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,8 @@ handle_request_('GetTransactionInfoByHash', Params, _Config) ->
fun(#{info := Info, tx_type := ga_meta_tx}) ->
#{<<"ga_info">> => aega_call:serialize_for_client(Info)};
(#{info := Info, tx_type := TxType}) when TxType =:= contract_create_tx;
TxType =:= contract_call_tx ->
TxType =:= contract_call_tx;
TxType =:= ga_attach_tx ->
#{<<"call_info">> => aect_call:serialize_for_client(Info)};
(#{info := Info, tx_type := _}) ->
%% info is assumed to be a binary
Expand Down
3 changes: 2 additions & 1 deletion apps/aehttp/src/aehttp_helpers.erl
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,8 @@ get_info_object_signed_tx(BlockHash, STx) ->
Tx = aetx_sign:tx(STx),
case aetx:specialize_type(Tx) of
{TxType, _} when TxType =:= contract_create_tx;
TxType =:= contract_call_tx ->
TxType =:= contract_call_tx;
TxType =:= ga_attach_tx ->
{CB, CTx} = aetx:specialize_callback(Tx),
Contract = CB:contract_pubkey(CTx),
CallId = CB:call_id(CTx),
Expand Down
2 changes: 1 addition & 1 deletion apps/aehttp/test/aehttp_contracts_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -1163,7 +1163,7 @@ remote_gas_test_contract(Config) ->
call_get(APub, APriv, EncC2Pub, Contract, 1),
force_fun_calls(Node),
Balance2 = get_balance(APub),
?assertMatchVM(1610855, 1600036, (Balance1 - Balance2) div ?DEFAULT_GAS_PRICE),
?assertMatchVM(1610855, 1600196, (Balance1 - Balance2) div ?DEFAULT_GAS_PRICE),

%% Test remote call with limited gas (3) that fails (out of gas).
[] = call_func(APub, APriv, EncC1Pub, Contract, "call", [EncC2Pub, "2", "3"], error),
Expand Down

0 comments on commit 43f1c27

Please sign in to comment.