Add a transaction for burning coins

This also removes some generators for the sake of simplicity and
generality.
parent 46d4fdaa
Pipeline #23625622 passed with stages
in 17 minutes and 34 seconds
......@@ -109,6 +109,18 @@ Fee: none
* Price of storing an account for 1 day (8 bytes).
* Protocol version (1 byte).
### Burn transaction format
* Transaction type (4, 1 byte).
* Valid Since (4 bytes) — earliest block height at which tx can be included.
* Address (32 bytes).
* Value (8 bytes).
* Message length (1 byte).
* Message.
* Signature of all the previous fields.
Fee: per tx + per byte
### Signature format
* An Ed25519 signature (64 bytes).
......
......@@ -44,6 +44,14 @@
message :: message(),
signature :: signature()}).
-type transfer_tx() :: #transfer_tx{}.
-record(
burn_tx,
{valid_since :: block_height(),
address :: pk(),
value :: tx_value(),
message :: message(),
signature :: signature()}).
-type burn_tx() :: #burn_tx{}.
-record(
lock_tx,
{address :: pk(),
......@@ -69,7 +77,7 @@
address :: pk(),
signature :: signature()}).
-type vote_tx() :: #vote_tx{}.
-type tx() :: transfer_tx() | account_tx() | lock_tx() | vote_tx().
-type tx() :: transfer_tx() | account_tx() | lock_tx() | vote_tx() | burn_tx().
-record(
account,
{address :: pk(),
......
......@@ -30,6 +30,8 @@ valid_since(#transfer_tx{valid_since=ValidSince}) ->
ValidSince;
valid_since(#vote_tx{vote=#vote{valid_since=ValidSince}}) ->
ValidSince;
valid_since(#burn_tx{valid_since=ValidSince}) ->
ValidSince;
valid_since(_) ->
none.
......@@ -41,6 +43,8 @@ from(#account_tx{from=From}) ->
from(#lock_tx{address=Address}) ->
Address;
from(#vote_tx{address=Address}) ->
Address;
from(#burn_tx{address=Address}) ->
Address.
-spec serialize(tx()) -> binary().
......@@ -76,7 +80,16 @@ serialize(
address=Address,
signature=Signature}) ->
ChoicesBin = ercoin_vote:choices_serialize(Choices),
<<3, ValidSince:4/unit:8, Address/binary, ChoicesBin/binary, Signature/binary>>.
<<3, ValidSince:4/unit:8, Address/binary, ChoicesBin/binary, Signature/binary>>;
serialize(
#burn_tx{
valid_since=ValidSince,
address=Address,
value=Value,
message=Message,
signature=Signature}) ->
MsgLength = byte_size(Message),
<<4, ValidSince:4/unit:8, Address/binary, Value:8/unit:8, MsgLength, Message/binary, Signature/binary>>.
-spec deserialize(binary()) -> tx() | none.
deserialize(TxBin) ->
......@@ -113,6 +126,13 @@ deserialize(TxBin) ->
fee_per_account_day => FeePerAccountDay,
protocol => Protocol}},
signature=Signature};
<<4, ValidSince:4/unit:8, Address:32/binary, Value:8/unit:8, MsgLength, Msg:MsgLength/binary, Signature/binary>> ->
#burn_tx{
valid_since=ValidSince,
address=Address,
value=Value,
message=Msg,
signature=Signature};
_ ->
none
end,
......@@ -147,7 +167,7 @@ error_code(Tx, Data=#data{height=Height, epoch_length=EpochLength}) ->
-spec error_code_1(tx(), data(), account()) -> error_code().
error_code_1(Tx=#transfer_tx{to=To, value=Value}, Data, #account{locked_until=LockedUntil, balance=Balance}) ->
case ?SETS:is_element({ercoin_tx:valid_since(Tx), ?HASH(ercoin_tx:serialize(Tx))}, Data#data.fresh_txs) of
case in_fresh_txs(Tx, Data) of
false ->
case ercoin_account:get(To, Data) of
none ->
......@@ -193,8 +213,8 @@ error_code_1(#lock_tx{address=Address, locked_until=LockedUntil}, Data, _) ->
false ->
?FORBIDDEN
end;
error_code_1(Tx=#vote_tx{address=Address}, #data{fresh_txs=FreshTxs, validators=Validators, future_validators=FutureValidators}, _) ->
case ?SETS:is_element({ercoin_tx:valid_since(Tx), ?HASH(ercoin_tx:serialize(Tx))}, FreshTxs) of
error_code_1(Tx=#vote_tx{address=Address}, Data=#data{validators=Validators, future_validators=FutureValidators}, _) ->
case in_fresh_txs(Tx, Data) of
false ->
case gb_merkle_trees:lookup(Address, Validators) of
none ->
......@@ -216,8 +236,29 @@ error_code_1(Tx=#vote_tx{address=Address}, #data{fresh_txs=FreshTxs, validators=
end;
_ ->
?ALREADY_EXECUTED
end;
error_code_1(Tx=#burn_tx{value=Value}, Data, #account{balance=Balance, locked_until=LockedUntil}) ->
case in_fresh_txs(Tx, Data) of
false ->
case LockedUntil of
none ->
case Balance < ercoin_fee:total(Tx, Data) + Value of
false ->
?OK;
true ->
?INSUFFICIENT_FUNDS
end;
_ ->
?FORBIDDEN
end;
true ->
?ALREADY_EXECUTED
end.
-spec in_fresh_txs(tx(), data()) -> boolean().
in_fresh_txs(Tx, Data) ->
?SETS:is_element({ercoin_tx:valid_since(Tx), ?HASH(ercoin_tx:serialize(Tx))}, Data#data.fresh_txs).
-spec unpack_binary(binary(), data()) -> {error_code(), tx() | none}.
unpack_binary(MaybeTxBin, Data) ->
case deserialize(MaybeTxBin) of
......@@ -245,16 +286,14 @@ apply(Tx, Data=#data{fee_deposit_short=FeeDepositShort, fee_deposit_long=FeeDepo
ercoin_account:put(NewFrom, Data#data{fee_deposit_short=NewFeeDepositShort, fee_deposit_long=NewFeeDepositLong})).
-spec apply_1(tx(), data()) -> data().
apply_1(Tx=#transfer_tx{to=To, from=From, value=Value}, Data=#data{fresh_txs=FreshTxs, fresh_txs_hash=FreshTxsHash}) ->
TxBin = ercoin_tx:serialize(Tx),
NewFreshTxs = ?SETS:add_element({ercoin_tx:valid_since(Tx), ?HASH(TxBin)}, FreshTxs),
NewFreshTxsHash = ?HASH(<<FreshTxsHash/binary, TxBin/binary>>),
apply_1(Tx=#transfer_tx{to=To, from=From, value=Value}, Data=#data{}) ->
FromAccount = ercoin_account:get(From, Data),
NewFromAccount = FromAccount#account{balance=FromAccount#account.balance - Value},
Data1 = ercoin_account:put(NewFromAccount, Data#data{fresh_txs=NewFreshTxs, fresh_txs_hash=NewFreshTxsHash}),
Data1 = ercoin_account:put(NewFromAccount, Data),
ToAccount = ercoin_account:get(To, Data1),
NewToAccount = ToAccount#account{balance=ToAccount#account.balance + Value},
ercoin_account:put(NewToAccount, Data1);
Data2 = ercoin_account:put(NewToAccount, Data1),
add_to_fresh_txs(Tx, Data2);
apply_1(#account_tx{valid_until=ValidUntil, to=ToAddress}, Data) ->
NewAccount =
case ercoin_account:get(ToAddress, Data) of
......@@ -269,9 +308,18 @@ apply_1(#account_tx{valid_until=ValidUntil, to=ToAddress}, Data) ->
apply_1(#lock_tx{locked_until=LockedUntil, address=Address}, Data) ->
Account = ercoin_account:get(Address, Data),
ercoin_account:put(Account#account{locked_until=LockedUntil}, Data);
apply_1(Tx=#vote_tx{vote=Vote, address=Address}, Data=#data{fresh_txs=FreshTxs, fresh_txs_hash=FreshTxsHash}) ->
apply_1(Tx=#vote_tx{vote=Vote, address=Address}, Data) ->
Data1 = ercoin_vote:put(Address, Vote, Data),
add_to_fresh_txs(Tx, Data1);
apply_1(#burn_tx{address=Address, value=Value}, Data) ->
Account = ercoin_account:get(Address, Data),
NewAccount = Account#account{balance=Account#account.balance - Value},
ercoin_account:put(NewAccount, Data).
-spec add_to_fresh_txs(tx(), data()) -> data().
%% @doc Add a transaction to fresh_txs and to fresh_txs_hash.
add_to_fresh_txs(Tx, Data=#data{fresh_txs=FreshTxs, fresh_txs_hash=FreshTxsHash}) ->
TxBin = ercoin_tx:serialize(Tx),
NewFreshTxsHash = ?HASH(<<FreshTxsHash/binary, TxBin/binary>>),
NewFreshTxs = ?SETS:add_element({ercoin_tx:valid_since(Tx), ?HASH(TxBin)}, FreshTxs),
ercoin_vote:put(Address, Vote, Data#data{fresh_txs=NewFreshTxs, fresh_txs_hash=NewFreshTxsHash}).
NewFreshTxsHash = ?HASH(<<FreshTxsHash/binary, TxBin/binary>>),
Data#data{fresh_txs=NewFreshTxs, fresh_txs_hash=NewFreshTxsHash}.
This diff is collapsed.
......@@ -20,13 +20,12 @@
ercoin_tx_gen,
[data_with_tx/0,
data_with_tx_bin/0,
data_with_vote_tx/0,
data_with_transfer_tx/0,
data_with_invalid_tx_bin/0,
data_sks_and_account_tx/0,
data_sks_and_burn_tx/0,
data_sks_and_transfer_tx/0,
data_sks_and_lock_tx/0,
tx/0]).
data_sks_and_vote_tx/0]).
money_supply(#data{accounts=Accounts, fee_deposit_short=FeeDepositShort, fee_deposit_long=FeeDepositLong}) ->
AccountsBalance =
......@@ -65,18 +64,30 @@ tx_serialization_test_() ->
?_assertEqual(?SAMPLE_TRANSFER_TX, ercoin_tx:deserialize(?SAMPLE_TRANSFER_TX_SERIALIZED))].
prop_tx_serialization() ->
?FORALL(Tx, tx(), ercoin_tx:deserialize(ercoin_tx:serialize(Tx)) =:= Tx).
?FORALL({_, Tx}, data_with_tx(), ercoin_tx:deserialize(ercoin_tx:serialize(Tx)) =:= Tx).
prop_valid_tx_is_positively_checked() ->
prop_valid_tx_bin_is_unpacked() ->
?FORALL(
{Data, Tx},
data_with_tx(),
?OK =:= ercoin_tx:error_code(Tx, Data)).
begin
{Code, Tx} = ercoin_tx:unpack_binary(ercoin_tx:serialize(Tx), Data),
Code =:= ?OK andalso Tx =/= none
end).
prop_invalid_tx_bin_is_not_unpacked() ->
?FORALL(
{Data, InvalidTxBin},
data_with_invalid_tx_bin(),
begin
{Code, MaybeTx} = ercoin_tx:unpack_binary(InvalidTxBin, Data),
Code =/= ?OK andalso MaybeTx =:= none
end).
prop_vote_tx_changes_vote() ->
?FORALL(
{Data=#data{height=Height, epoch_length=EpochLength}, Tx},
data_with_vote_tx(),
{{Data=#data{height=Height, epoch_length=EpochLength}, _}, Tx},
data_sks_and_vote_tx(),
begin
NewData = ercoin_tx:apply(Tx, Data),
case gb_merkle_trees:lookup(ercoin_tx:from(Tx), NewData#data.validators) of
......@@ -100,12 +111,19 @@ prop_vote_tx_changes_vote() ->
prop_transfer_tx_adds_balance_to_destination() ->
?FORALL(
{Data, Tx=#transfer_tx{from=From, to=To, value=Value}},
data_with_transfer_tx(),
{{Data, _}, Tx=#transfer_tx{from=From, to=To, value=Value}},
data_sks_and_transfer_tx(),
?IMPLIES(
From =/= To,
ercoin_account:lookup_balance(To, Data) + Value =:= ercoin_account:lookup_balance(To, ercoin_tx:apply(Tx, Data)))).
prop_burn_tx_destroys_money() ->
?FORALL(
{{Data, _}, Tx=#burn_tx{address=Address, value=Value}},
data_sks_and_burn_tx(),
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() ->
?FORALL(
{{Data, _}, Tx=#lock_tx{address=Address, locked_until=NewLockedUntil}},
......@@ -120,10 +138,10 @@ prop_account_tx_extends_validity() ->
prop_transfer_tx_and_vote_tx_are_added_to_fresh_txs_and_fresh_txs_hash() ->
?FORALL(
{Data=#data{fresh_txs_hash=FreshTxsHash}, Tx},
{{Data=#data{fresh_txs_hash=FreshTxsHash}, _}, Tx},
oneof(
[data_with_transfer_tx(),
data_with_vote_tx()]),
[data_sks_and_transfer_tx(),
data_sks_and_vote_tx()]),
begin
TxBin = ercoin_tx:serialize(Tx),
#data{fresh_txs=NewFreshTxs, fresh_txs_hash=NewFreshTxsHash} = ercoin_tx:apply(Tx, Data),
......@@ -131,8 +149,10 @@ prop_transfer_tx_and_vote_tx_are_added_to_fresh_txs_and_fresh_txs_hash() ->
NewFreshTxsHash =:= ?HASH(<<FreshTxsHash/binary, TxBin/binary>>)
end).
prop_tx_does_not_change_money_supply() ->
prop_non_burn_tx_does_not_change_money_supply() ->
?FORALL(
{Data, Tx},
data_with_tx(),
money_supply(ercoin_tx:apply(Tx, Data)) =:= money_supply(Data)).
?IMPLIES(
not is_record(Tx, burn_tx),
money_supply(ercoin_tx:apply(Tx, Data)) =:= money_supply(Data))).
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment