Don’t rely on block height for measuring time

This is a big change that comes from the fact that time between blocks
is not enforced by consensus in Tendermint. Besides making data
structures more natural and the usage simpler, it allows to abandon
creating blocks every n seconds regardless of whether there are
transactions present or not. For this to be possible, Tendermint needs
to stop creating blocks when the application hash changes. See
https://github.com/tendermint/tendermint/issues/1909 for details.

A testnet generator is introduced to faciliate generating of initial
data, which is now highly dependent on time.

A module which encompasses epoch-specific logic has been created.
parent b07cd82f
Pipeline #36006423 passed with stages
in 20 minutes and 34 seconds
......@@ -63,4 +63,28 @@ no-bad-mix-app-files:
filter-genesis-txs:
@$(SHELL_ERL) -pa $(SHELL_PATHS) -noshell -eval 'ercoin_genesis:filter_genesis_txs(), halt()'
.PHONY: filter-genesis-txs no-bad-mix-app-files
ERCOIN_HOME ?= $(HOME)/.ercoin
ERCOIN_CONFIG = $(ERCOIN_HOME)/config
# The order of dependencies is important: the public key of validator may be used to generate genesis data.
# The presence of priv_validator.json is however not required, so we cannot make it a rigid dependency.
testnet: $(ERCOIN_CONFIG)/priv_validator.json $(ERCOIN_CONFIG)/config.toml.patched $(ERCOIN_CONFIG)/genesis.json
$(ERCOIN_CONFIG)/genesis.json: $(ERCOIN_CONFIG)/genesis.json.custom
$(ERCOIN_CONFIG)/genesis.json.custom:
$(verbose) mkdir -p $(ERCOIN_CONFIG)
$(verbose) $(SHELL_ERL) -pa $(SHELL_PATHS) -noshell -eval\
'file:write_file("$(ERCOIN_CONFIG)/genesis.json", ercoin_genesis:data_to_genesis_json(ercoin_genesis:testnet_data($(data_opts)))), halt()'
$(gen_verbose) touch $@
$(ERCOIN_CONFIG) $(ERCOIN_CONFIG)/priv_validator.json $(ERCOIN_CONFIG)/config.toml:
$(gen_verbose) tendermint init --home $(ERCOIN_HOME)
$(ERCOIN_CONFIG)/config.toml.patched: $(ERCOIN_CONFIG)/config.toml
$(verbose) sed -i s'/timeout_commit = .*/timeout_commit = 3000/' $<
$(verbose) sed -i s'/create_empty_blocks = .*/create_empty_blocks = false/' $<
$(verbose) sed -i s'/create_empty_blocks_interval = .*/create_empty_blocks_interval = 900/' $<
$(gen_verbose) touch $@
.PHONY: filter-genesis-txs no-bad-mix-app-files testnet
This diff is collapsed.
This is a simple initial configuration suitable for development purposes. The initial state consists of two accounts:
• the initial validator with balance 1 (see priv_validator.json for private key);
• an unlocked account with much higher balance. The private key for this account, written in Base58, is 5kVpzPsANrSuuZMC98BkP78nZ5mPv3SopTK9yt8KXWZmDTpTFBn8MvfUjMorpC7YQWwXjpig1TS1wxJNTxE2BzHj
#!/bin/sh
# Licensed under the Apache License, Version 2.0 (the “License”);
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an “AS IS” BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
BOOTSTRAP_DIR=$(dirname $(readlink -f $0))
ERCOIN_HOME=${ERCOIN_HOME:-$HOME/.ercoin}
if [ -d $ERCOIN_HOME/config ]
then
echo "Config directory found, skipping tendermint init…"
else
tendermint init --home $ERCOIN_HOME
fi
cp $BOOTSTRAP_DIR/genesis.json $ERCOIN_HOME/config/
cp $BOOTSTRAP_DIR/priv_validator.json $ERCOIN_HOME/config/
echo "Printing file $BOOTSTRAP_DIR/README…"
cat $BOOTSTRAP_DIR/README
{
"app_hash" : "",
"app_state" : "g1AAAAIneNrL4E9hYElJLElMZEz8kMiQwsBZmpeSmpaZl5qSyACCWbkMDAwKDARABlMiUwYLWOmDjzMu1RcFrP9bzufLqvV2kvGrcGH/m30q5xWLPatWLPoMViTKlS4tYBKywfaFtsoE9lNnBGrDy5QObWVV5ykTKejScs1gBiuzWvToW/lzRXuPdzulLtcfZT77xtHww9+5DAtbvr2bO+OAH0gRDwNj2wIgLe5R9oIBrKv6Oqvvj5hltpLqBtquP7iebjzp02Z9+eCDQLZJbEsTXTdBDSfGoQZQw0GAEcQmyneuZ5irP3bfS1BzXrRiimHwCTOmVl2Lmadqrhd2Xq2NdcrLYEph4EstSs7PzItPzSspyi+oBAoUZ+YW5KTCBIAhykiCQxVFUCOEEZ1mBBu1ziTpr0y97e8MzxdhG5zvfEnxWyKefH6itJCwx/9wr5y/yPGPzAYAzZ265Q==",
"genesis_time" : "2018-09-03T18:55:36Z",
"chain_id" : "ercoin-test",
"validators" : [
{
"power" : "20",
"name" : "",
"pub_key" : {
"type" : "tendermint/PubKeyEd25519",
"value" : "4PGY0n9yUK/9dw5NBSrtkjPqVxNP2Y4kzyFzSXqoovM="
}
}
]
}
{
"address": "146DA449F1259751B2F6D69504D966E98A55F6A6",
"last_height": "0",
"last_round": "0",
"last_step": 0,
"priv_key": {
"type": "tendermint/PrivKeyEd25519",
"value": "skYZ9XopqNOx61WeNzTPkgMTz9bdBETQ60il9Efq9fng8ZjSf3JQr/13Dk0FKu2SM+pXE0/ZjiTPIXNJeqii8w=="
}
}
......@@ -35,8 +35,6 @@
-type balance() :: 0..18446744073709551615.
-type fee() :: 0..18446744073709551615.
-type tx_value() :: 0..18446744073709551615.
%% @type timestamp() = 0..18446744073709551615. POSIX timestamp in miliseconds.
-type timestamp() :: 0..18446744073709551615.
-type voting_power() :: non_neg_integer().
-type message() :: binary().
-record(
......@@ -85,10 +83,10 @@
-type tx() :: transfer_tx() | account_tx() | lock_tx() | vote_tx() | burn_tx().
-record(
account,
{address :: pk(),
{address= <<0:256>> :: pk(),
balance=0 :: balance(),
valid_until :: block_height(),
locked_until=none :: block_height() | none,
valid_until=1 :: ercoin_timestamp:timestamp(),
locked_until=none :: ercoin_timestamp:timestamp() | none,
validator_pk=none :: pk() | none}). %% TODO: Replace incorrect usages of pk() with address().
-type account() :: #account{}.
-record(
......@@ -96,9 +94,10 @@
%% Some default values are provided to prevent Dialyzer from complaining in tests.
{protocol=1 :: pos_integer(),
epoch_length=604800 :: epoch_length(),
epoch_stage=beginning :: ercoin_epoch:stage(),
height=0 :: block_height() | 0,
last_epoch_end=0 :: timestamp(),
timestamp=0 :: timestamp(),
last_epoch_end=(ercoin_timestamp:now() - ?BLOCK_LENGTH) :: ercoin_timestamp:timestamp(),
timestamp=ercoin_timestamp:now() :: ercoin_timestamp: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(),
......@@ -106,6 +105,7 @@
fresh_txs_hash= <<0:256>> :: hash(),
accounts=gb_merkle_trees:empty() :: gb_merkle_trees:tree(),
entropy_fun={ercoin_entropy, simple_entropy} :: {Module :: atom(), Function :: atom()},
%% After BeginBlock, these are validators for the next block.
validators=gb_merkle_trees:empty() :: 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,
......
......@@ -9,6 +9,7 @@
data/0,
data_sks/0,
data_with_account/0,
data_with_begin_block/0,
data_sks_and_account/0,
data_sks_and_account/1,
vote/1,
......
This diff is collapsed.
......@@ -12,16 +12,26 @@
-module(ercoin_account).
-export_type(
[accounts/0]).
-export(
[deserialize/1,
[accounts_from_option_maps/1,
construct/1,
deserialize/1,
foldr/3,
get/2,
is_locked/1,
lookup_balance/2,
put/2,
serialize/1]).
-compile({parse_transform, dynarec}).
-include_lib("include/ercoin.hrl").
-type accounts() :: gb_merkle_trees:tree().
-spec is_locked(account()) -> boolean().
is_locked(#account{locked_until=LockedUntil}) ->
LockedUntil =/= none.
......@@ -59,8 +69,10 @@ deserialize({Address, AccountBin}) ->
validator_pk=ValidatorPK}
end.
-spec get(address(), data()) -> account() | none.
-spec get(address(), data() | accounts()) -> account() | none.
get(Address, #data{accounts=Accounts}) ->
get(Address, Accounts);
get(Address, Accounts) ->
case gb_merkle_trees:lookup(Address, Accounts) of
none ->
none;
......@@ -68,13 +80,65 @@ get(Address, #data{accounts=Accounts}) ->
ercoin_account:deserialize({Address, AccountBin})
end.
-spec put(account(), data()) -> data().
-spec put(account(), data() | accounts()) -> data() | accounts().
put(Account, Data=#data{accounts=Accounts}) ->
NewAccounts = ?MODULE:put(Account, Accounts),
Data#data{accounts=NewAccounts};
put(Account, AccountsTree) ->
{Address, AccountBin} = ercoin_account:serialize(Account),
NewAccounts = gb_merkle_trees:enter(Address, AccountBin, Accounts),
Data#data{accounts=NewAccounts}.
gb_merkle_trees:enter(Address, AccountBin, AccountsTree).
-spec lookup_balance(address(), data()) -> balance().
lookup_balance(Address, #data{accounts=AccountsTree}) ->
#account{balance=Balance} = ercoin_account:deserialize({Address, gb_merkle_trees:lookup(Address, AccountsTree)}),
Balance.
-spec foldr(fun ((account(), Acc) -> Acc), Acc, accounts()) -> Acc.
foldr(F, Acc0, Accounts) ->
gb_merkle_trees:foldr(
fun (AccountBin, Acc) ->
Account = deserialize(AccountBin),
F(Account, Acc)
end,
Acc0,
Accounts).
-spec maybe_decode_b64(list() | binary()) -> binary().
maybe_decode_b64(B64) when is_list(B64) orelse byte_size(B64) rem 32 =/= 0 ->
base64:decode(B64);
maybe_decode_b64(Bin) ->
Bin.
-spec construct(map()) -> account().
%% Construct an account from a provided option map.
%% Options generally correspond to account fields, with some special cases implemented (see the code for details).
construct(OptionMap) ->
Now = ercoin_timestamp:now(),
maps:fold(
fun (Key, Value, AccountAcc) ->
case Key of
valid_for ->
set_value(valid_until, Now + Value, AccountAcc);
locked_for ->
set_value(locked_until, Now + Value, AccountAcc);
address ->
set_value(address, maybe_decode_b64(Value), AccountAcc);
validator_pk ->
set_value(validator_pk, maybe_decode_b64(Value), AccountAcc);
_ ->
set_value(Key, Value, AccountAcc)
end
end,
#account{},
OptionMap).
-spec accounts_from_option_maps(list(map())) -> accounts().
%% @doc Construct an account tree from provided list of constructor options.
%% @see construct/1
accounts_from_option_maps(OptMaps) ->
lists:foldl(
fun (OptMap, TreeAcc) ->
?MODULE:put(construct(OptMap), TreeAcc)
end,
gb_merkle_trees:empty(),
OptMaps).
%% Licensed under the Apache License, Version 2.0 (the “License”);
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an “AS IS” BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
-module(ercoin_data).
-export(
[app_hash/1,
construct/1,
grant_fee_deposits/1,
money_supply/1,
remove_old_and_unlock_accounts/1,
shift_to_timestamp/2]).
-compile({parse_transform, dynarec}).
-include_lib("include/ercoin.hrl").
-spec app_hash(data()) -> binary().
app_hash(
#data{
protocol=Protocol,
accounts=Accounts,
validators=Validators,
future_validators=FutureValidators,
fresh_txs_hash=FreshTxsHash,
fee_deposit_short=FeeDepositShort,
fee_deposit_long=FeeDepositLong,
epoch_stage=EpochStage,
last_epoch_end=LastEpochEnd,
timestamp=Timestamp}) ->
ValidatorsHash = gb_merkle_trees:root_hash(Validators),
AccountsHash =
case gb_merkle_trees:root_hash(Accounts) of
undefined ->
%% This means that there are no accounts anymore, which is an uninteresting case, but implementing this eases testing.
<<0:256>>;
AccountsHash2 ->
AccountsHash2
end,
FutureValidatorsHash =
case FutureValidators of
undefined ->
<<0:256>>;
{promise, _, FutureValidatorsHash2} ->
FutureValidatorsHash2;
_ ->
gb_merkle_trees:root_hash(FutureValidators)
end,
OtherDataHash =
?HASH(
<<Protocol,
Timestamp:4/unit:8,
LastEpochEnd:4/unit:8,
(ercoin_epoch:stage_no(EpochStage)),
FeeDepositShort:8/unit:8,
FeeDepositLong:8/unit:8,
ValidatorsHash/binary,
FutureValidatorsHash/binary,
FreshTxsHash/binary>>),
?HASH(<<AccountsHash/binary, OtherDataHash/binary>>).
-spec grant_fee_deposits(data()) -> data().
%% @doc Dispose deposits, assuming that the time for it has come.
grant_fee_deposits(
Data=#data{
validators=Validators,
fee_deposit_long=DepositLong,
fee_deposit_short=DepositShort,
timestamp=Timestamp,
last_epoch_end=LastEpochEnd}) ->
DisposedDepositLong = DepositLong div ?FEE_DEPOSIT_LONG_RATIO,
DisposedDeposit = DepositShort + DisposedDepositLong,
RealEpochLength = Timestamp - LastEpochEnd,
SharesValidators =
[{VP, PK} || {PK, <<VP, _/binary>>} <- gb_merkle_trees:to_orddict(Validators)],
ValidatorsRewards = 'hare-niemeyer':apportion(SharesValidators, DisposedDeposit),
Data1 =
lists:foldl(
fun ({PK, Reward}, DataAcc) ->
case ercoin_validators:absencies(PK, Validators) < RealEpochLength div 3 of
true ->
case ercoin_account:get(PK, Data) of
#account{balance=Balance} = Account ->
ercoin_account:put(Account#account{balance=Balance + Reward}, DataAcc);
none ->
DataAcc
end;
false ->
DataAcc
end
end,
Data,
ValidatorsRewards),
Data1#data{fee_deposit_short=0, fee_deposit_long=DepositLong - DisposedDepositLong}.
-spec shift_to_timestamp(ercoin_timestamp:timestamp(), data()) -> data().
shift_to_timestamp(Timestamp, Data) ->
remove_old_and_unlock_accounts(Data#data{timestamp=Timestamp}).
-spec remove_old_and_unlock_accounts(data()) -> data().
remove_old_and_unlock_accounts(Data=#data{accounts=AccountsTree, timestamp=Timestamp}) ->
{NewAccountsTree, _} = remove_old_and_unlock_accounts_from_tree(AccountsTree, Timestamp),
Data#data{accounts=NewAccountsTree}.
-spec remove_old_and_unlock_accounts_from_tree(gb_merkle_trees:tree(), ercoin_timestamp:timestamp()) -> {gb_merkle_trees:tree(), non_neg_integer()}.
%% @doc Remove old and unlock accounts, returning a new tree and total balance of destroyed accounts.
remove_old_and_unlock_accounts_from_tree(AccountsTree, Timestamp) ->
gb_merkle_trees:foldr(
fun ({Address, AccountBin}, {TreeAcc, BalanceAcc}) ->
case ercoin_account:deserialize({Address, AccountBin}) of
#account{valid_until=ValidUntil, balance=Balance} when ValidUntil < Timestamp ->
{gb_merkle_trees:delete(Address, TreeAcc), BalanceAcc + Balance};
Account=#account{locked_until=LockedUntil} when LockedUntil < Timestamp ->
{_, NewAccountBin} = ercoin_account:serialize(Account#account{locked_until=none}),
{gb_merkle_trees:enter(Address, NewAccountBin, TreeAcc), BalanceAcc};
#account{} ->
{TreeAcc, BalanceAcc}
end
end,
{AccountsTree, 0},
AccountsTree).
-spec money_supply(data()) -> non_neg_integer().
money_supply(#data{accounts=Accounts, fee_deposit_short=FeeDepositShort, fee_deposit_long=FeeDepositLong}) ->
AccountsBalance =
gb_merkle_trees:foldr(
fun (AccountSerialized, Sum) ->
#account{balance=Balance} = ercoin_account:deserialize(AccountSerialized),
Balance + Sum
end,
0,
Accounts),
AccountsBalance + FeeDepositShort + FeeDepositLong.
-spec construct(map()) -> data().
%% @doc Construct data from provided option map.
%% Options are not mandatory and generally correspond to data fields, with special case of <code>accounts_opts</code> which needs to be a list of account construction options.
%% @see ercoin_account:construct/1
construct(Opts) ->
Data1 =
maps:fold(
fun (Key, Value, DataAcc) ->
case Key of
accounts_opts ->
Accounts = ercoin_account:accounts_from_option_maps(Value),
ercoin_data:set_value(accounts, Accounts, DataAcc);
_ ->
ercoin_data:set_value(Key, Value, DataAcc)
end
end,
#data{},
Opts),
case gb_merkle_trees:size(Data1#data.validators) of
0 ->
Data1#data{validators=ercoin_validators:draw(Data1)};
_ ->
Data1
end.
......@@ -29,4 +29,4 @@ reliable_entropy(#data{timestamp=Timestamp, epoch_length=EpochLength}) ->
%% @doc Obtain pseudoentropy for data, possibly for development purposes.
%% Will be cheap and quick to generate but don’t expect it to be secure.
simple_entropy(#data{height=Height, timestamp=Timestamp}) ->
?HASH(<<Height, Timestamp:8/unit:8>>).
?HASH(<<Height:4/unit:8, Timestamp:4/unit:8>>).
%% Licensed under the Apache License, Version 2.0 (the “License”);
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an “AS IS” BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
-module(ercoin_epoch).
-export_type(
[stage/0]).
-export(
[all_stages/0,
stage/1,
stage_no/1,
stage_gte/2,
progress/3]).
-include_lib("include/ercoin.hrl").
-type stage() :: beginning | drawing_frozen | drawing_yielded | drawing_to_be_announced | drawing_announced.
-type action() :: fun((data()) -> data()).
-define(
STAGES,
[beginning,
drawing_frozen,
drawing_yielded,
drawing_to_be_announced,
drawing_announced]).
-spec all_stages() -> list(stage()).
all_stages() ->
?STAGES.
-spec stage(data()) -> stage().
stage(#data{epoch_stage=Stage}) ->
Stage.
-spec stage_no(data() | stage()) -> pos_integer().
stage_no(#data{epoch_stage=Stage}) ->
?FUNCTION_NAME(Stage);
stage_no(Stage) ->
list_index(Stage, ?STAGES).
-spec list_index(term(), list(term())) -> pos_integer().
list_index(El, List) ->
list_index(El, List, 1).
-spec list_index(term(), list(term()), pos_integer()) -> pos_integer().
list_index(El, [El|_], I) ->
I;
list_index(El, [_|ListTail], I) ->
list_index(El, ListTail, I+1).
-spec stage_gte(data() | stage(), stage()) -> boolean().
stage_gte(#data{epoch_stage=Stage1}, Stage2) ->
stage_gte(Stage1, Stage2);
stage_gte(Stage1, Stage2) ->
stage_no(Stage1) >= stage_no(Stage2).
-spec should_we_step(data()) -> boolean().
should_we_step(
#data{
epoch_stage=Stage,
epoch_length=Length,
last_epoch_end=LastEpochEnd,
timestamp=Timestamp}) ->
Duration = Timestamp - LastEpochEnd,
case Stage of
beginning ->
Duration >= Length div 4;
drawing_frozen ->
Duration >= Length * 3 div 4;
drawing_yielded ->
Duration >= Length - ?BLOCK_LENGTH * 2;
drawing_to_be_announced ->
true;
drawing_announced ->
true
end.
-spec execute_actions(list(action()), data()) -> data().
execute_actions([], Data) ->
Data;
execute_actions([Action|ActionsTail], Data) ->
execute_actions(ActionsTail, apply(Action, [Data])).
-spec progress(data(), non_neg_integer(), list({{address(), binary()}, boolean()})) -> data().
progress(Data=#data{epoch_stage=Stage}, TimeSinceLastBlock, ValidatorsWithPresence) ->
Data1 = increase_absencies(Data, TimeSinceLastBlock, ValidatorsWithPresence),
case should_we_step(Data1) of
true ->
NewStage = next_stage(Stage),
Data2 = Data1#data{epoch_stage=NewStage},
execute_actions(stage_actions(NewStage), Data2);
false ->
Data1
end.
-spec next_stage(stage()) -> stage().
next_stage(drawing_announced) ->
beginning;
next_stage(Stage) ->
next_stage(Stage, ?STAGES).
-spec next_stage(stage(), list(stage())) -> stage().
next_stage(Stage, [Stage, NextStage|_]) ->
NextStage;
next_stage(Stage, [_|TailStages]) ->
next_stage(Stage, TailStages);
next_stage(_, []) ->
beginning.
-spec update_last_epoch_end(data()) -> data().
update_last_epoch_end(Data=#data{timestamp=Timestamp}) ->
Data#data{last_epoch_end=Timestamp}.
-spec stage_actions(stage()) -> list(action()).
stage_actions(beginning) ->
[];
stage_actions(drawing_frozen) ->
[fun ercoin_validators:begin_drawing/1];
stage_actions(drawing_yielded) ->
[fun ercoin_validators:yield_drawing/1];
stage_actions(drawing_to_be_announced) ->
[];
stage_actions(drawing_announced) ->
[fun ercoin_data:grant_fee_deposits/1,
%% While we update last_epoch_end before the new epoch has already begun, we’ll use this field only later.
fun update_last_epoch_end/1,
fun ercoin_validators:set_future_as_current/1].
-spec increase_absencies(data(), non_neg_integer(), list({{address(), binary()}, boolean()})) -> data().
increase_absencies(Data=#data{validators=Validators}, TimestampDiff, ValidatorsWithPresence) ->
NewValidators =
lists:foldl(
fun ({_, true}, Acc) ->
Acc;
({{Address, <<Power, Absencies:3/unit:8, VoteBin/binary>>}, false}, Acc) ->
NewAbsencies = Absencies + TimestampDiff,
gb_merkle_trees:enter(Address, <<Power, NewAbsencies:3/unit:8, VoteBin/binary>>, Acc)
end,
Validators,
ValidatorsWithPresence),
Data#data{validators=NewValidators}.
......@@ -45,22 +45,22 @@ total(Tx, Data) ->
short(Tx, Data) + long(Tx, Data).
-spec long(tx(), data()) -> fee().
long(#account_tx{valid_until=ValidUntil, to=Address}, Data=#data{height=Height}) ->
long(#account_tx{valid_until=ValidUntil, to=Address}, Data=#data{timestamp=Timestamp}) ->
ExtensionLength =
case ercoin_account:get(Address, Data) of
none ->
ValidUntil - Height;
ValidUntil - Timestamp;
#account{valid_until=OldValidUntil} ->
ValidUntil - OldValidUntil
end,
#{fee_per_account_day := FPerAccountDay} = fees(Data),
div_ceil(ExtensionLength * FPerAccountDay, 3600 * 24);
long(#lock_tx{locked_until=LockedUntil, address=Address}, Data=#data{height=Height}) ->
long(#lock_tx{locked_until=LockedUntil, address=Address}, Data=#data{timestamp=Timestamp}) ->
#account{locked_until=OldLockedUntil} = ercoin_account:get(Address, Data),
ExtensionLength =
case OldLockedUntil of
none ->
LockedUntil - Height;
LockedUntil - Timestamp;
_ ->
LockedUntil - OldLockedUntil
end,
......
This diff is collapsed.
......@@ -19,6 +19,7 @@
-export([start_link/0]).
-export([current_data/0]).
-export([dump_data_async/1]).
-export([data_dir/0]).
%% gen_server.
-export([init/1]).
......
......@@ -12,6 +12,9 @@
-module(ercoin_timestamp).
-export_type(
[timestamp/0]).
-export(
[from_iso/1,
from_protobuf/1,
......@@ -19,30 +22,32 @@
to_iso/1,
to_protobuf/1]).
-include_lib("include/ercoin.hrl").
-include_lib("abci_server/include/abci.hrl").
%% @type timestamp() = 0..4294967295. POSIX timestamp in seconds.
-type timestamp() :: 0..4294967295.
-spec to_iso(timestamp()) -> binary().
to_iso(Timestamp) ->
'Elixir.DateTime':to_iso8601(
'Elixir.DateTime':'from_unix!'(Timestamp, millisecond),
'Elixir.DateTime':'from_unix!'(Timestamp, second),
extended).
-spec now() -> timestamp().
now() ->
'Elixir.DateTime':to_unix('Elixir.DateTime':utc_now(), millisecond).
'Elixir.DateTime':to_unix('Elixir.DateTime':utc_now(), second).
-spec from_iso(binary()) -> timestamp().
from_iso(ISO) ->
{ok, DateTime, 0} = 'Elixir.DateTime':'from_iso8601'(ISO),
'Elixir.DateTime':to_unix(DateTime, millisecond).
'Elixir.DateTime':to_unix(DateTime, second).
-spec from_protobuf(#'google.protobuf.Timestamp'{}) -> timestamp().
from_protobuf(#'google.protobuf.Timestamp'{seconds=Secs, nanos=Nanos}) ->
Secs * 1000 + Nanos div 1000000.
from_protobuf(#'google.protobuf.Timestamp'{seconds=Secs}) ->
Secs.
-spec to_protobuf(timestamp()) -> #'google.protobuf.Timestamp'{}.
to_protobuf(Timestamp) ->
#'google.protobuf.Timestamp'{
seconds=Timestamp div 1000,
nanos=(Timestamp rem 1000) * 1000000}.
seconds=Timestamp,
nanos=0}.
......@@ -28,7 +28,7 @@
valid_since/1,
value/1]).
-spec valid_since(tx()) -> block_height() | none.
-spec valid_since(tx()) -> ercoin_timestamp:timestamp() | none.
valid_since(#transfer_tx{valid_since=ValidSince}) ->
ValidSince;
valid_since(#vote_tx{vote=#vote{valid_since=ValidSince}}) ->
......@@ -166,9 +166,9 @@ deserialize(TxBin) ->
end.
-spec error_code(tx(), data()) -> error_code().
error_code(Tx, Data=#data{height=Height, epoch_length=EpochLength}) ->
error_code(Tx, Data=#data{timestamp=Timestamp, epoch_length=EpochLength}) ->
ValidSince = ercoin_tx:valid_since(Tx),
case ValidSince =:= none orelse ValidSince =< Height + 1 andalso ValidSince > Height - EpochLength + 1 of
case ValidSince =:= none orelse ValidSince =< Timestamp andalso ValidSince > Timestamp - EpochLength of
true ->
case is_record(Tx, vote_tx) of
true ->
......@@ -208,8 +208,8 @@ error_code_1(Tx=#transfer_tx{to=To}, Data, #account{locked_until=LockedUntil}) -
true ->
?ALREADY_EXECUTED
end;
error_code_1(#account_tx{valid_until=ValidUntil, to=To, from=From}, Data=#data{height=Height}, FromAccount) ->
case ValidUntil > Height of
error_code_1(#account_tx{valid_until=ValidUntil, to=To, from=From}, Data=#data{timestamp=Timestamp}, FromAccount) ->