Commit a8e8b7a9 authored by Krzysztof Jurewicz's avatar Krzysztof Jurewicz

Let locked accounts choose other addresses as validators

This change means that compromising validator’s key won’t necessarily
mean loss of funds. While it results in a lower incentive to protect
validators’ keys, it also results in a lower incentive to crack
validators (as there will be little funds to steal). Locked accounts
will still have an incentive to make validators secure, as compromised
validators can lower the currency value.

This change also means that stakeholders will be able to easily
delegate the duty of running a validator to other entity. This opens a
possibility of validators being backed by multiple stakeholders.

A possible next step is lowering the entropy of validators’ drawing.
parent 742a920c
Pipeline #24700725 passed with stages
in 16 minutes and 26 seconds
......@@ -88,6 +88,7 @@ Fee: Per tx + per byte + per blocks.
* Transaction type (2, 1 byte).
* Locked until block (4 bytes).
* Address (32 bytes).
* Intended validator address (32 bytes)
* Signature of all the previous fields.
Fee: per tx + per byte + per blocks.
......@@ -180,7 +181,7 @@ Validators are responsible for providing replay protection.
Transfer transactions modify accounts’ balances. They may not be performed from a locked account, but they can transfer funds to a locked account.
Lock transactions lock accounts and declare them as ready to become a validator. They may be performed by locked accounts.
Lock transactions lock accounts and specify intended validator addresses. They may be performed by locked accounts. Note that while it is technically possible for a complex address to become a validator, only an address being a public key will be able to sign blocks.
Vote transactions can be performed by validators to vote for fee sizes and protocol changes.
......@@ -188,7 +189,7 @@ Account transaction create accounts or extend their validity.
Every transaction except a vote transaction deducts a fee from the account from which it is performed. Fees are calculated from validators’ votes. They are put into long fee deposit (fees per block) and short fee deposit (fees per tx and per byte).
At the end of every epoch, short fee deposit and part of long fee deposit are divided between those validators that have less than 2/3 of absencies in block signatures, proportionally to their voting power. If the epoch lasted longer than expected, a ratio proportional to the relative delay is put into long fee deposit instead of being granted to validators.
At the end of every epoch, short fee deposit and part of long fee deposit are divided between validators proportionally to their voting power. If the epoch lasted longer than expected, a ratio proportional to the relative delay is put into long fee deposit instead of being granted to validators. If a validator doesn’t have an account or has at least 1⁄3 absencies in the passing epoch, its reward is destroyed.
When new epoch is reached, validator set is replaced by a new one which has been drawn previously. When quarter of epoch (rounded down) is reached, data is locked for the purpose of drawing new validators. When three quarters of epoch (rounded down) are reached, result of drawing is yielded into application state. From that point future validators are able to send fee votes.
......@@ -197,9 +198,7 @@ Drawing of validators is performed randomly among locked accounts, with handicap
* account balance;
* time for which an account will be locked, counting from the start of the next epoch.
If a validator proposes an invalid tx in a block, it pays a fee like it was a transaction tx.
If balance of a validator reaches 0, it is removed from the validator set.
If a validator proposes an invalid tx in a block, it is removed from the validator set.
## Initial data
......
......@@ -12,6 +12,7 @@
-define(FEE_DEPOSIT_LONG_RATIO, 13140). %% Inversed ratio of long fee deposit which is dispatched.
-define(MAX_BLOCK_HEIGHT, 4294967295). %% 2^32 - 1
-define(MAX_EPOCH_LENGTH, 31536000). %% 1 year, assuming 1 second blocks.
-define(HASH(X), crypto:hash(sha256, X)).
-define(SETS, ordsets).
......@@ -27,7 +28,9 @@
-type pk() :: <<_:256>>.
-type signature() :: <<_:512>>.
-type hash() :: <<_:256>>.
-type address() :: pk() | hash().
-type block_height() :: 1..?MAX_BLOCK_HEIGHT.
-type epoch_length() :: 2..?MAX_EPOCH_LENGTH.
-type balance() :: 0..18446744073709551615.
-type fee() :: 0..18446744073709551615.
-type tx_value() :: 0..18446744073709551615.
......@@ -56,6 +59,7 @@
lock_tx,
{address :: pk(),
locked_until :: block_height(),
validator_pk :: pk(),
signature :: signature()}).
-type lock_tx() :: #lock_tx{}.
-record(
......@@ -83,13 +87,14 @@
{address :: pk(),
balance=0 :: balance(),
valid_until :: block_height(),
locked_until=none :: block_height() | none}).
locked_until=none :: block_height() | none,
validator_pk=none :: pk() | none}). %% TODO: Replace incorrect usages of pk() with address().
-type account() :: #account{}.
-record(
data,
%% Some default values are provided to prevent Dialyzer from complaining in tests.
{protocol=1 :: pos_integer(),
epoch_length=604800 :: 2..31536000,
epoch_length=604800 :: epoch_length(),
height=0 :: block_height() | 0,
last_epoch_end=0 :: timestamp(),
timestamp=0 :: timestamp(),
......
......@@ -7,7 +7,6 @@
[account/0,
data/0,
data_sks/0,
data_sks/1,
data_with_account/0,
data_sks_and_account/0,
data_sks_and_account/1,
......
......@@ -258,13 +258,22 @@ grant_fee_deposits(
ReturnedDeposit = max(0, min(TakenDeposit, TakenDeposit * (RealEpochLength - ExpectedRealEpochLength) div ExpectedRealEpochLength)),
DisposedDeposit = TakenDeposit - ReturnedDeposit,
SharesValidators =
[{VP, PK} || {PK, <<VP, Absencies:3/unit:8, _/binary>>} <- gb_merkle_trees:to_orddict(Validators), Absencies < EpochLength div 3],
[{VP, PK} || {PK, <<VP, _/binary>>} <- gb_merkle_trees:to_orddict(Validators)],
ValidatorsRewards = 'hare-niemeyer':apportion(SharesValidators, DisposedDeposit),
Data1 =
lists:foldl(
fun ({PK, Reward}, DataPrime) ->
#account{balance=Balance} = Account = ercoin_account:get(PK, Data),
ercoin_account:put(Account#account{balance=Balance + Reward}, DataPrime)
fun ({PK, Reward}, DataAcc) ->
case ercoin_validators:absencies(PK, Validators) < EpochLength 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),
......
......@@ -32,13 +32,14 @@ serialize(
address=Address,
balance=Balance,
valid_until=ValidUntil,
locked_until=LockedUntil}) ->
locked_until=LockedUntil,
validator_pk=ValidatorPK}) ->
{Address,
case LockedUntil of
none ->
<<ValidUntil:4/unit:8, Balance:8/unit:8>>;
_ ->
<<ValidUntil:4/unit:8, Balance:8/unit:8, LockedUntil:4/unit:8>>
<<ValidUntil:4/unit:8, Balance:8/unit:8, LockedUntil:4/unit:8, ValidatorPK/binary>>
end}.
-spec deserialize({Key :: binary(), Value :: binary()}) -> account().
......@@ -52,12 +53,13 @@ deserialize({Address, AccountBin}) ->
case Rest of
<<>> ->
BaseAccount;
<<LockedUntil:4/unit:8>> ->
<<LockedUntil:4/unit:8, ValidatorPK:32/binary>> ->
BaseAccount#account{
locked_until=LockedUntil}
locked_until=LockedUntil,
validator_pk=ValidatorPK}
end.
-spec get(pk(), data()) -> account() | none.
-spec get(address(), data()) -> account() | none.
get(Address, #data{accounts=Accounts}) ->
case gb_merkle_trees:lookup(Address, Accounts) of
none ->
......@@ -72,7 +74,7 @@ put(Account, Data=#data{accounts=Accounts}) ->
NewAccounts = gb_merkle_trees:enter(Address, AccountBin, Accounts),
Data#data{accounts=NewAccounts}.
-spec lookup_balance(pk(), data()) -> balance().
-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.
......@@ -32,7 +32,7 @@
-define(BLK_DUST_THRESHOLD, 10 * ?SATOSHIS_IN_BLK). %% In satoshis.
%% Score extracted from burnt output.
-type score() :: {SatoshisBurnt :: non_neg_integer(), {pk(), LockedUntil :: block_height() | none}}.
-type score() :: {SatoshisBurnt :: non_neg_integer(), {address(), LockedUntil :: block_height() | none}}.
hex(N) when N < 10 ->
$0+N;
......@@ -199,7 +199,7 @@ max_on_maybe(Int1, Int2) ->
merge_scores(Scores) ->
merge_scores(Scores, #{}).
-spec merge_scores(list(score()), #{pk() => score()}) -> list(score()).
-spec merge_scores(list(score()), #{address() => score()}) -> list(score()).
merge_scores([], Acc) ->
maps:values(Acc);
merge_scores([Score={SatoshisBurnt, {Address, LockedUntil}}|TailScores], Acc) ->
......
......@@ -36,7 +36,7 @@ valid_since(#burn_tx{valid_since=ValidSince}) ->
valid_since(_) ->
none.
-spec from(tx()) -> pk().
-spec from(tx()) -> address().
from(#transfer_tx{from=From}) ->
From;
from(#account_tx{from=From}) ->
......@@ -70,8 +70,9 @@ serialize(
#lock_tx{
locked_until=LockedUntil,
address=Address,
validator_pk=ValidatorPK,
signature=Signature}) ->
<<2, LockedUntil:4/unit:8, Address/binary, Signature/binary>>;
<<2, LockedUntil:4/unit:8, Address/binary, ValidatorPK/binary, Signature/binary>>;
serialize(
#vote_tx{
vote=
......@@ -110,10 +111,11 @@ deserialize(TxBin) ->
from=From,
to=To,
signature=Signature};
<<2, LockedUntil:4/unit:8, Address:32/binary, Signature/binary>> ->
<<2, LockedUntil:4/unit:8, Address:32/binary, ValidatorPK:32/binary, Signature/binary>> ->
#lock_tx{
locked_until=LockedUntil,
address=Address,
validator_pk=ValidatorPK,
signature=Signature};
<<3, ValidSince:4/unit:8, Address:32/binary, FeePerTx:8/unit:8, FeePer256B:8/unit:8, FeePerAccountDay:8/unit:8, Protocol, Signature/binary>> ->
#vote_tx{
......@@ -156,17 +158,22 @@ error_code(Tx, Data=#data{height=Height, epoch_length=EpochLength}) ->
ValidSince = ercoin_tx:valid_since(Tx),
case ValidSince =:= none orelse ValidSince =< Height + 1 andalso ValidSince > Height - EpochLength + 1 of
true ->
case ercoin_account:get(ercoin_tx:from(Tx), Data) of
none ->
?NOT_FOUND;
From ->
error_code_1(Tx, Data, From)
case is_record(Tx, vote_tx) of
true ->
error_code_1(Tx, Data, none);
false ->
case ercoin_account:get(ercoin_tx:from(Tx), Data) of
none ->
?NOT_FOUND;
From ->
error_code_1(Tx, Data, From)
end
end;
_ ->
?INVALID_TIMESTAMP
end.
-spec error_code_1(tx(), data(), account()) -> error_code().
-spec error_code_1(tx(), data(), account() | none) -> error_code().
error_code_1(Tx=#transfer_tx{to=To, value=Value}, Data, #account{locked_until=LockedUntil, balance=Balance}) ->
case in_fresh_txs(Tx, Data) of
false ->
......@@ -285,15 +292,23 @@ handle_bin(MaybeTxBin, Data) ->
-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),
FeeShort = ercoin_fee:short(Tx, Data),
NewFeeDepositShort = FeeDepositShort + FeeShort,
FeeLong = ercoin_fee:long(Tx, Data),
NewFeeDepositLong = FeeDepositLong + FeeLong,
NewFrom = From#account{balance=From#account.balance - FeeShort - FeeLong},
apply_1(
Tx,
ercoin_account:put(NewFrom, Data#data{fee_deposit_short=NewFeeDepositShort, fee_deposit_long=NewFeeDepositLong})).
TotalPaid = FeeLong + FeeShort,
case TotalPaid of
0 ->
apply_1(Tx, Data);
_ ->
From = ercoin_account:get(ercoin_tx:from(Tx), Data),
NewFeeDepositShort = FeeDepositShort + FeeShort,
NewFeeDepositLong = FeeDepositLong + FeeLong,
NewFrom = From#account{balance=From#account.balance - TotalPaid},
apply_1(
Tx,
ercoin_account:put(
NewFrom,
Data#data{fee_deposit_short=NewFeeDepositShort, fee_deposit_long=NewFeeDepositLong}))
end.
-spec apply_1(tx(), data()) -> data().
apply_1(Tx=#transfer_tx{to=To, from=From, value=Value}, Data=#data{}) ->
......@@ -315,9 +330,9 @@ apply_1(#account_tx{valid_until=ValidUntil, to=ToAddress}, Data) ->
address=ToAddress}
end,
ercoin_account:put(NewAccount, Data);
apply_1(#lock_tx{locked_until=LockedUntil, address=Address}, Data) ->
apply_1(#lock_tx{locked_until=LockedUntil, address=Address, validator_pk=ValidatorPK}, Data) ->
Account = ercoin_account:get(Address, Data),
ercoin_account:put(Account#account{locked_until=LockedUntil}, Data);
ercoin_account:put(Account#account{locked_until=LockedUntil, validator_pk=ValidatorPK}, Data);
apply_1(Tx=#vote_tx{vote=Vote, address=Address}, Data) ->
Data1 = ercoin_vote:put(Address, Vote, Data),
add_to_fresh_txs(Tx, Data1);
......
......@@ -15,7 +15,8 @@
-include_lib("include/ercoin.hrl").
-export(
[draw/1]).
[absencies/2,
draw/1]).
-spec voting_resolution(data()) -> pos_integer().
voting_resolution(_) ->
......@@ -32,16 +33,16 @@ draw(Data=#data{accounts=Accounts, height=Height, epoch_length=EpochLength}) ->
{PointsAddresses, TotalPoints} =
gb_merkle_trees:foldr(
fun (AccountSerialized, {PointsAddresses1, PointsSum}) ->
#account{address=Address, locked_until=LockedUntil, balance=Balance} = ercoin_account:deserialize(AccountSerialized),
#account{locked_until=LockedUntil, validator_pk=ValidatorPK, balance=Balance} = ercoin_account:deserialize(AccountSerialized),
case LockedUntil =/= none andalso LockedUntil > Height of
false ->
{PointsAddresses1, PointsSum};
true ->
NextEpochStart = EpochLength * (Height div EpochLength + 1),
Handicap = max(0, (LockedUntil - NextEpochStart + 1) * Balance),
RandomInt = binary:decode_unsigned(binary:part(?HASH(<<0, Entropy/binary, Address/binary>>), 0, 8)),
RandomInt = binary:decode_unsigned(binary:part(?HASH(<<0, Entropy/binary, ValidatorPK/binary>>), 0, 8)),
Points = RandomInt * Handicap,
{[{Points, Address}|PointsAddresses1], PointsSum + Points}
{[{Points, ValidatorPK}|PointsAddresses1], PointsSum + Points}
end
end,
{[], 0},
......@@ -56,3 +57,7 @@ draw(Data=#data{accounts=Accounts, height=Height, epoch_length=EpochLength}) ->
end} ||
{Address, VotePower} <- 'hare-niemeyer':apportion(PointsAddresses, Resolution, TotalPoints)]).
-spec absencies(address(), gb_merkle_trees:tree()) -> non_neg_integer().
absencies(Address, Validators) ->
<<_, Absencies:3/unit:8, _/binary>> = gb_merkle_trees:lookup(Address, Validators),
Absencies.
......@@ -57,7 +57,7 @@ deserialize(<<ValidSince:4/unit:8, FeePerTx:8/unit:8, FeePer256B:8/unit:8, FeePe
deserialize(<<>>) ->
none.
-spec put(pk(), vote(), data()) -> data().
-spec put(address(), vote(), data()) -> data().
put(Address, Vote, Data=#data{validators=Validators, future_validators=FutureValidators, height=Height, epoch_length=EpochLength}) ->
VoteBin = ercoin_vote:serialize(Vote),
NewValidators =
......@@ -82,11 +82,11 @@ put(Address, Vote, Data=#data{validators=Validators, future_validators=FutureVal
Data#data{validators=NewValidators, future_validators=NewFutureValidators}.
-spec get(pk(), data()) -> vote() | none.
-spec get(address(), data()) -> vote() | none.
get(Address, Data) ->
get(Address, Data, validators).
-spec get(pk(), data(), atom()) -> vote() | none.
-spec get(address(), data(), atom()) -> vote() | none.
get(Address, Data, Field) ->
<<_:1/unit:8, _:3/unit:8, VoteBin/binary>> = gb_merkle_trees:lookup(Address, get_value(Field, Data)),
ercoin_vote:deserialize(VoteBin).
......@@ -13,13 +13,17 @@
(defmodule merkle_proofs
(export (deserialize 1)
(serialize 1)
(fold 1)))
(fold 1)
)
(export_type (path 0)
(proof 0)
))
(deftype merkle-path (list (tuple (UNION 'left 'right) (binary))))
(deftype merkle-proof (tuple (binary) (merkle-path)))
(deftype path (list (tuple (UNION 'left 'right) (binary))))
(deftype proof (tuple (binary) (path)))
(defspec (deserialize 1)
(((binary)) (UNION (merkle-proof) 'none)))
(((binary)) (UNION (proof) 'none)))
(defun deserialize
(((binary (leaf binary (size 32)) (path-bin binary)))
(case (deserialize-path path-bin)
......@@ -29,7 +33,7 @@
((_) 'none))
(defspec (deserialize-path 1)
(((bitstring)) (UNION (merkle-path) 'none)))
(((bitstring)) (UNION (path) 'none)))
(defun deserialize-path
((padding) (when (< (bit_size padding) 8))
(let ((padding-size (bit_size padding)))
......@@ -50,14 +54,14 @@
'none))
(defspec (serialize 1)
(((merkle-proof)) (binary)))
(((proof)) (binary)))
(defun serialize
(((tuple leaf path))
(let* ((path-bin (serialize-path path))
(padding-size (rem (- 8 (rem (bit_size path-bin) 8)) 8)))
(binary (leaf binary) (path-bin bitstring) (0 (size padding-size))))))
(defspec (serialize-path 1) (((merkle-path)) (bitstring)))
(defspec (serialize-path 1) (((path)) (bitstring)))
(defun serialize-path
(((list))
(binary))
......@@ -69,7 +73,7 @@
(binary (direction-bit (size 1)) (hash binary) (steps-bin bitstring)))))
(defspec (fold 1)
(((merkle-proof)) (binary)))
(((proof)) (binary)))
(defun fold
"Compute root hash from a Merkle proof."
(((tuple val (list)))
......
......@@ -136,18 +136,6 @@ prop_end_block_expires_accounts() ->
ercoin_data:money_supply(NewData) =< ercoin_data:money_supply(Data) - Account#account.balance
end).
%% TODO: Give this more attention, ensure that the right thing is tested.
prop_after_end_block_all_validators_have_accounts() ->
?FORALL(
{Data, Account},
data_with_account(),
begin
ExpiringAccount = Account#account{valid_until=Data#data.height},
ExpiringData = ercoin_account:put(ExpiringAccount, Data),
NewData = end_block(ExpiringData),
ercoin_account:get(Account#account.address, NewData) =:= none
end).
prop_end_block_does_not_expire_valid_accounts() ->
?FORALL(
{{Data, Account}, ValidFor},
......@@ -181,23 +169,34 @@ prop_fee_deposit_granting() ->
%% If there were delays in creating blocks, then we want to lower the reward, so there is no incentive to delay blocks.
ExpectedReturnedDeposit = max(0, min(ExpectedTakenDeposit, ExpectedTakenDeposit * (RealEpochLength - ExpectedRealEpochLength) div ExpectedRealEpochLength)),
ExpectedDisposedDeposit = ExpectedTakenDeposit - ExpectedReturnedDeposit,
%% If a validator has at least 1/3 of absencies, it should not get any reward.
SharesValidators = [{VP, PK} || {PK, <<VP, Absencies:3/unit:8, _/binary>>} <- gb_merkle_trees:to_orddict(Validators), Absencies < EpochLength div 3],
SharesValidators = [{VP, PK} || {PK, <<VP, _/binary>>} <- gb_merkle_trees:to_orddict(Validators)],
ValidatorsExpectedRewards = 'hare-niemeyer':apportion(SharesValidators, ExpectedDisposedDeposit),
NewFeeDepositShort =:= 0 andalso NewFeeDepositLong >= FeeDepositLong - FeeDepositLong div ?FEE_DEPOSIT_LONG_RATIO + ExpectedReturnedDeposit andalso
lists:all(
fun ({PK, Reward}) ->
{BalancesOK, DestroyedSum} =
lists:foldl(
fun ({PK, Reward}, {BalancesOKAcc, DestroyedSumAcc}) ->
case ercoin_account:get(PK, NewData) of
#account{balance=BalanceNew} ->
#account{balance=BalanceOld} = ercoin_account:get(PK, Data),
BalanceNew - BalanceOld =:= Reward;
case ercoin_validators:absencies(PK, Validators) < EpochLength div 3 of
true ->
{BalancesOKAcc andalso BalanceNew =:= BalanceOld + Reward,
DestroyedSumAcc};
false ->
{BalancesOKAcc andalso BalanceNew =:= BalanceOld,
DestroyedSumAcc + Reward}
end;
none ->
true
{BalancesOKAcc, DestroyedSumAcc + Reward}
end
end,
ValidatorsExpectedRewards);
{true, 0},
ValidatorsExpectedRewards),
NewFeeDepositShort =:= 0 andalso
NewFeeDepositLong =:= FeeDepositLong - FeeDepositLong div ?FEE_DEPOSIT_LONG_RATIO + ExpectedReturnedDeposit andalso
BalancesOK andalso
ercoin_data:money_supply(NewData) =< ercoin_data:money_supply(Data) - DestroyedSum;
_ ->
NewFeeDepositShort =:= FeeDepositShort andalso NewFeeDepositLong >= FeeDepositLong
NewFeeDepositShort =:= FeeDepositShort andalso NewFeeDepositLong =:= FeeDepositLong
end
end).
......@@ -246,6 +245,8 @@ prop_deliver_tx_handles_binary_in_regard_to_data() ->
{ErrorCode, NewData} =:= ercoin_tx:handle_bin(MaybeTxBin, Data)
end).
%% TODO: Slash validators that propose invalid transactions.
prop_check_tx_handles_binary_in_regard_to_mempool_data() ->
?FORALL(
{{MaybeTxBin, MempoolData},
......
This diff is collapsed.
......@@ -28,7 +28,7 @@
-type secret() :: {SK :: <<_:512>>, {PK :: <<_:256>>, Path :: list({left | right, <<_:256>>}) | none}}.
-spec delete_account(pk(), data()) -> data().
-spec delete_account(address(), data()) -> data().
delete_account(Address, Data=#data{accounts=Accounts}) ->
NewAccounts = gb_merkle_trees:delete(Address, Accounts),
Data#data{accounts=NewAccounts}.
......@@ -64,8 +64,8 @@ to_sign(
<<0, ValidSince:4/unit:8, From/binary, To/binary, Value:8/unit:8, MessageLength, Message/binary>>;
to_sign(#account_tx{valid_until=ValidUntil, from=From, to=To}) ->
<<1, ValidUntil:4/unit:8, From/binary, To/binary>>;
to_sign(#lock_tx{address=Address, locked_until=LockedUntil}) ->
<<2, LockedUntil:4/unit:8, Address/binary>>;
to_sign(#lock_tx{address=Address, locked_until=LockedUntil, validator_pk=ValidatorPK}) ->
<<2, LockedUntil:4/unit:8, Address/binary, ValidatorPK/binary>>;
to_sign(
#vote_tx{
address=Address,
......@@ -120,14 +120,27 @@ vote_tx(Data, Address, SKs) ->
data_sks_and_vote_tx() ->
?LET(
{{Data, SKs}, Account},
data_sks_and_account(validator_current_or_future),
{{Data, SKs}, vote_tx(Data, Account#account.address, SKs)}).
{Data, SKs},
data_sks(),
?LET(
{Address, _},
elements(
gb_merkle_trees:to_orddict(Data#data.validators) ++
case Data#data.future_validators of
undefined -> [];
{promise, _, _} -> [];
FutureValidators -> gb_merkle_trees:to_orddict(FutureValidators)
end),
{{Data, SKs}, vote_tx(Data, Address, SKs)})).
data_sks_and_lock_tx() ->
?LET(
{{{Data=#data{height=Height}, SKs}, Account=#account{address=Address, locked_until=LockedUntil, valid_until=ValidUntil}}, LockedFor},
{data_sks_and_account(), pos_integer()},
{{{Data=#data{height=Height}, SKs}, Account=#account{address=Address, locked_until=LockedUntil, valid_until=ValidUntil}},
LockedFor,
ValidatorPK},
{data_sks_and_account(),
pos_integer(),
ercoin_gen:address()},
begin
NewLockedUntil =
case LockedUntil of
......@@ -142,6 +155,7 @@ data_sks_and_lock_tx() ->
#lock_tx{
address=Address,
locked_until=NewLockedUntil,
validator_pk=ValidatorPK,
signature= <<0:512>>},
SKs),
{{make_sufficient_balance(ercoin_account:put(NewAccount, Data), Tx), SKs}, Tx}
......@@ -215,7 +229,7 @@ account_tx({Data, SKs}, From, ToAddress) ->
data_sks_and_transfer_tx() ->
?LET(
{Data, SKs},
data_sks(true),
data_sks(),
?LET(
{From, To},
{ercoin_gen:account_from(Data, unlocked), ercoin_gen:account_from(Data)},
......@@ -227,7 +241,7 @@ data_sks_and_transfer_tx() ->
data_sks_and_burn_tx() ->
?LET(
{Data, SKs},
data_sks(true),
data_sks(),
?LET(
From,
ercoin_gen:account_from(Data, unlocked),
......@@ -303,11 +317,16 @@ data_with_invalid_tx_bin() ->
end),
%% Lock not longer than existing.
?LET(
{{{Data, _}, Tx}, ShorterFor},
{data_sks_and_lock_tx(), non_neg_integer()},
{{{Data, _}, Tx}, ShorterFor, ValidatorPK},
{data_sks_and_lock_tx(), non_neg_integer(), ercoin_gen:address()},
begin
Account = ercoin_account:get(ercoin_tx:from(Tx), Data),
{ercoin_account:put(Account#account{locked_until=Tx#lock_tx.locked_until + ShorterFor}, Data), ercoin_tx:serialize(Tx)}
{ercoin_account:put(
Account#account{
locked_until=Tx#lock_tx.locked_until + ShorterFor,
validator_pk=ValidatorPK},
Data),
ercoin_tx:serialize(Tx)}
end),
%% Validity not longer than existing.
?LET(
......@@ -323,15 +342,21 @@ data_with_invalid_tx_bin() ->
end),
%% Account tx: From locked and To other than From.
?LET(
{{{Data, _}, Tx}, LockedFor},
{{{Data, _}, Tx}, LockedFor, ValidatorPK},
{?SUCHTHAT(
{_, #account_tx{from=From, to=To}},
data_sks_and_account_tx(),
From =/= To),
pos_integer()},
pos_integer(),
ercoin_gen:address()},
begin
Account = ercoin_account:get(ercoin_tx:from(Tx), Data),
{ercoin_account:put(Account#account{locked_until=Data#data.height + LockedFor}, Data), ercoin_tx:serialize(Tx)}
{ercoin_account:put(
Account#account{
locked_until=Data#data.height + LockedFor,
validator_pk=ValidatorPK},
Data),
ercoin_tx:serialize(Tx)}
end),
%% Lock tx: Locked until longer than valid until.
?LET(
......@@ -352,7 +377,10 @@ data_with_invalid_tx_bin() ->
%% From not existing.
?LET(
{Data, ValidTx},
data_with_tx(),
?SUCHTHAT(
{_, Tx},
data_with_tx(),
not is_record(Tx, vote_tx)),
{delete_account(ercoin_tx:from(ValidTx), Data), ercoin_tx:serialize(ValidTx)}),
%% Vote tx: From not a validator and not a future validator.
?LET(
......@@ -386,14 +414,21 @@ data_with_invalid_tx_bin() ->
%% Transfer/burn tx: from locked.
?LET(
{{{Data, _}, Tx},
LockedFor},
LockedFor,
ValidatorPK},
{oneof(
[fun data_sks_and_transfer_tx/0,
fun data_sks_and_burn_tx/0]),
non_neg_integer()},
non_neg_integer(),
ercoin_gen:address()},
begin
Account = ercoin_account:get(ercoin_tx:from(Tx), Data),
NewData = ercoin_account:put(Account#account{locked_until=Data#data.height + LockedFor}, Data),
NewData =
ercoin_account:put(
Account#account{
locked_until=Data#data.height + LockedFor,
validator_pk=ValidatorPK},
Data),
{NewData, ercoin_tx:serialize(Tx)}
end),
%% Tx not valid yet.
......@@ -456,18 +491,20 @@ data_with_invalid_tx_bin() ->
]).
data_sks_and_tx() ->
oneof(
[fun data_sks_and_transfer_tx/0,
fun data_sks_and_lock_tx/0,
fun data_sks_and_account_tx/0,
fun data_sks_and_vote_tx/0,
fun data_sks_and_burn_tx/0]).
noshrink(
oneof(
[fun data_sks_and_transfer_tx/0,
fun data_sks_and_lock_tx/0,
fun data_sks_and_account_tx/0,
fun data_sks_and_vote_tx/0,
fun data_sks_and_burn_tx/0])).
data_with_tx() ->
?LET(
{{Data, _}, Tx},
data_sks_and_tx(),
{Data, Tx}).
noshrink(
?LET(
{{Data, _}, Tx},
data_sks_and_tx(),
{Data, Tx})).
data_with_tx_bin() ->
?LET(
......
......@@ -127,11 +127,15 @@ prop_burn_tx_destroys_money() ->
ercoin_account:lookup_balance(Address, ercoin_tx:apply(Tx, Data)) =:=
ercoin_account:lookup_balance(Address, Data) - Value).
prop_lock_tx_locks_account_or_extends_lock() ->
prop_lock_tx_locks_account_or_extends_lock_and_sets_validator_pk() ->
?FORALL(
{{Data, _}, Tx=#lock_tx{address=Address, locked_until=NewLockedUntil}},
{{Data, _}, Tx=#lock_tx{address=Address, locked_until=NewLockedUntil, validator_pk=NewValidatorPK}},
data_sks_and_lock_tx(),
NewLockedUntil =:= get_value(locked_until, ercoin_account:get(Address, ercoin_tx:apply(Tx, Data)))).
begin
#account{locked_until=ReturnedNewLockedUntil, validator_pk=ReturnedNewValidatorPK} =
ercoin_account:get(Address, ercoin_tx:apply(Tx, Data)),
ReturnedNewLockedUntil =:= NewLockedUntil andalso ReturnedNewValidatorPK =:= NewValidatorPK
end).
prop_account_tx_extends_validity() ->
?FORALL(
......
......@@ -14,21 +14,26 @@
-include_lib("include/ercoin_test.hrl").
prop_new_validators_will_have_accounts_when_epoch_starts() ->
prop_drawn_validators_are_backed_by_voters() ->
?FORALL(
Data=#data{height=Height, epoch_length=EpochLength},
data(),
begin
NewValidators = ercoin_validators:draw(Data),
Addresses =
gb_merkle_trees:foldr(
fun ({Address, _}, Acc) -> [Address|Acc] end,
[],
NewValidators),
NextEpochStart = EpochLength * (Height div EpochLength + 1),
lists:all(
fun (Address) ->
#account{valid_until=ValidUntil} = ercoin_account:get(Address, Data),
ValidUntil >= EpochLength * (Height div EpochLength + 1)
fun ({ValidatorAddress, _}) ->
gb_merkle_trees:foldr(