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 c2a05428
Pipeline #26867379 passed with stages
in 15 minutes and 55 seconds
......@@ -14,8 +14,9 @@ PROJECT = ercoin
PROJECT_DESCRIPTION = A simple cryptocurrency using Tendermint
PROJECT_VERSION = 0.1.0
DEPS = abci_server dynarec enacl erlsha2 gb_merkle_trees jiffy nist_beacon
DEPS = abci_server datum dynarec enacl erlsha2 gb_merkle_trees jiffy nist_beacon
dep_abci_server = git https://github.com/KrzysiekJ/abci_server.git 2f38de3b21dfb463ec33f54f6c08b7ce63c27f56
dep_datum = git https://github.com/fogfish/datum.git 4.3.3
dep_dynarec = git https://github.com/dieswaytoofast/dynarec.git 1f477
dep_enacl = git https://github.com/jlouis/enacl.git f650c72b028e46dbbed35f94e33ebe1e8db5d7eb
dep_erlsha2 = git https://github.com/vinoski/erlsha2 e3434b33cfeea02609bbf877954d856d895b9e1d
......
......@@ -89,6 +89,7 @@ Fee: Per tx + per byte + per blocks.
* Transaction type (2, 1 byte).
* Locked until block (4 bytes).
* Address (32 bytes).
* Intended validator public key (32 bytes)
* Signature of all the previous fields.
Fee: per tx + per byte + per blocks.
......@@ -181,7 +182,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 public keys. 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.
......@@ -189,7 +190,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.
......@@ -198,9 +199,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
......@@ -216,9 +215,10 @@ Initial amount of coins will be distributed proportionally to the associated amo
* address — 32 bytes.
* length of the “locked until” field — 0 to 3, 1 byte. Use 0 unless the account is going to be locked.
* locked until — 0 to 3 bytes, depending on the previous field.
* intended validator public key — 32 bytes, present if the “locked until” field is greater than 0.
* signature of all the fields starting from “`Ercoin `”.
Transaction needs to be included in a BlackCoin block between height [] and []. All accounts participating in the initial distribution will be valid until block 16777215 (2²⁴ – 1). If an account has multiple associated outputs, then “locked until” will be the highest value picked from the outputs. Amount of ercoins will be proportional to the sum of blackcoins burnt in all outputs. The minimum required burnt amount per account is 10 BLK.
Transaction needs to be included in a BlackCoin block between height [] and []. All accounts participating in the initial distribution will be valid until block 16777215 (2²⁴ – 1). If an account has multiple associated outputs, then “locked until” and “intended validator public key” will be picked from the output with highest “locked until”, with order of inclusion used as a secondary method of comparison. Amount of ercoins will be proportional to the sum of blackcoins burnt in all outputs. The minimum required burnt amount per account is 10 BLK.
Initial validators will be drawn using ordinary rules, with timestamp for obtaining entropy being []. After drawing, future validators will be able to put vote transactions into BlackCoin blockchain, analogously to the genesis transaction described above, in blocks between height [] and []. Those transactions have to have “valid since” set to 0. Transactions will be applied in the order of inclusion.
......
......@@ -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.
......@@ -13,6 +13,8 @@
%% @reference <a href="https://hackage.haskell.org/package/burnt-explorer-1.0.0">burnt-explorer 1.0.0</a> — it is assumed that it is used to generate files containing burnt BlackCoin outputs.
-module(ercoin_genesis).
-compile({parse_transform, category}).
-export(
[binary_to_hex/1,
data_to_genesis_json/1,
......@@ -20,6 +22,11 @@
initial_data/5,
sk_to_priv_validator_json/1]).
-import(
datum_cat_option,
[fail/1,
unit/1]).
-include_lib("include/ercoin.hrl").
%% Bitcoin Script, some opcodes.
......@@ -32,7 +39,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, ValidatorAddress :: address() | none}}.
hex(N) when N < 10 ->
$0+N;
......@@ -100,53 +107,72 @@ sk_to_priv_validator_json(SK) ->
%% Functions for generating initial data below.
-spec payload_from_script_hex(string()) -> none | binary().
-spec payload_from_script_hex(string()) -> datum:option(binary()).
payload_from_script_hex(ScriptHex) ->
case hexstr_to_bin(ScriptHex) of
<<?OP_RETURN, ?OP_PUSHDATA1, Length, Payload2:Length/binary>> ->
Payload2;
unit(Payload2);
<<?OP_RETURN, ?OP_PUSHDATA2, Length:2/unit:8, Payload2:Length/binary>> ->
Payload2;
unit(Payload2);
<<?OP_RETURN, ?OP_PUSHDATA4, Length:4/unit:8, Payload2:Length/binary>> ->
Payload2;
unit(Payload2);
_ ->
none
fail("Script not recognized.")
end.
-spec line_to_score(string()) -> none | {ok, score()}.
-spec amount_str_to_satoshis(string()) -> non_neg_integer().
amount_str_to_satoshis(AmountStr) ->
{ok, [AmountFloat], []} = io_lib:fread("~f", AmountStr),
round(AmountFloat * ?SATOSHIS_IN_BLK).
-spec payload_amount_to_score(binary(), non_neg_integer()) -> datum:option(score()).
payload_amount_to_score(
<<"Ercoin "/utf8,
Address:32/binary,
LockedUntilLength,
LockedUntil:LockedUntilLength/unit:8,
Rest/binary>>=Payload,
Amount)
when LockedUntilLength =< 3 ->
MaybeScoreMsgLength =
case LockedUntil > 0 of
true ->
case Rest of
<<ValidatorAddress:32/binary, _/binary>> ->
unit(
{{Amount,
{Address, LockedUntil, ValidatorAddress}},
7 + 32 + 1 + LockedUntilLength + 32});
_ ->
fail("Validator address not present.")
end;
false ->
unit(
{{Amount, {Address, none, none}},
7 + 32 + 1 + LockedUntilLength})
end,
[option ||
ScoreMsgLength <- (fun () -> MaybeScoreMsgLength end)(),
Score =< element(1, ScoreMsgLength),
MsgLength =< element(2, ScoreMsgLength),
Msg =< binary:part(Payload, 0, MsgLength),
Sig =< binary:part(Payload, MsgLength, byte_size(Payload) - MsgLength),
cats:require(
ercoin_sig:verify_detached(Sig, Msg, Address),
Score,
"Invalid signature.")];
payload_amount_to_score(_, _) ->
fail("Unrecognized format.").
-spec line_to_score(string()) -> datum:option(score()).
line_to_score(Line) ->
[_, _, AmountStr, ScriptHex|_] = string:split(Line, " ", all),
case payload_from_script_hex(ScriptHex) of
none ->
none;
Payload ->
{ok, [AmountFloat], []} = io_lib:fread("~f", AmountStr),
Amount = round(AmountFloat * ?SATOSHIS_IN_BLK),
case Payload of
<<"Ercoin "/utf8,
Address:32/binary,
LockedUntilLength,
LockedUntil:LockedUntilLength/unit:8,
Signature/binary>> when LockedUntilLength =< 3 ->
SignedMsg = binary:part(Payload, 0, 7 + 32 + 1 + LockedUntilLength),
case ercoin_sig:verify_detached(Signature, SignedMsg, Address) of
true ->
{ok,
{Amount,
{Address,
case LockedUntil of
0 -> none;
_ -> LockedUntil
end}}};
false ->
none
end;
_ ->
none
end
end.
[option ||
Payload <- payload_from_script_hex(ScriptHex),
Amount =< amount_str_to_satoshis(AmountStr),
payload_amount_to_score(Payload, Amount)].
-spec extract_from_file_lines(file:io_device(), fun((string()) -> {ok, T} | none)) -> list(T).
-spec extract_from_file_lines(file:io_device(), fun((string()) -> datum:option(T))) -> list(T).
%% @doc Extract data from file using a specified function. Returns a list in reversed order.
extract_from_file_lines(File, F) ->
extract_from_file_lines(File, F, []).
......@@ -158,9 +184,9 @@ extract_from_file_lines(File, F, Acc) ->
Acc;
{ok, Line} ->
case F(Line) of
none ->
undefined ->
?FUNCTION_NAME(File, F, Acc);
{ok, Value} ->
Value ->
?FUNCTION_NAME(File, F, [Value|Acc])
end
end.
......@@ -171,10 +197,11 @@ scores_to_distribution(Scores, TotalCoins) ->
Distribution = 'hare-niemeyer':apportion(Scores, TotalCoins),
AccountsList =
lists:foldl(
fun ({{Address, LockedUntil}, Balance}, Acc) ->
fun ({{Address, LockedUntil, ValidatorPK}, Balance}, Acc) ->
Account =
#account{
address=Address,
validator_pk=ValidatorPK,
balance=Balance,
valid_until=ValidUntil,
locked_until=LockedUntil},
......@@ -196,16 +223,24 @@ 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) ->
merge_scores([Score={SatoshisBurnt, {Address, LockedUntil, ValidatorAddress}}|TailScores], Acc) ->
merge_scores(
TailScores,
maps:update_with(
Address,
fun ({OldSatoshisBurnt, {_, OldLockedUntil}}) ->
{OldSatoshisBurnt + SatoshisBurnt, {Address, max_on_maybe(LockedUntil, OldLockedUntil)}}
fun ({OldSatoshisBurnt, {_, OldLockedUntil, OldValidatorAddress}}) ->
NewValidatorAddress =
case LockedUntil > OldLockedUntil of
true ->
ValidatorAddress;
false ->
%% We process scores from newest outputs first, which should take precedence if lock times are equal.
OldValidatorAddress
end,
{OldSatoshisBurnt + SatoshisBurnt, {Address, max_on_maybe(LockedUntil, OldLockedUntil), NewValidatorAddress}}
end,
Score,
Acc)).
......@@ -233,18 +268,18 @@ initial_data_without_votes(Accounts, VotesEndTimestamp, StartingTimestamp) ->
Validators = ercoin_validators:draw(Data1),
Data1#data{validators=Validators}.
-spec line_to_vote_tx(string()) -> none | {ok, vote_tx()}.
-spec line_to_vote_tx(string()) -> datum:option(vote_tx()).
line_to_vote_tx(Line) ->
[_, _, _, ScriptHex|_] = string:split(Line, " ", all),
case payload_from_script_hex(ScriptHex) of
none ->
none;
undefined ->
fail("No payload.");
Payload ->
case ercoin_tx:deserialize(Payload) of
VoteTx=#vote_tx{} ->
{ok, VoteTx};
unit(VoteTx);
_ ->
none
fail("Not a vote tx.")
end
end.
......
......@@ -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/