...
 
Commits (2)
FROM erlang:20
FROM erlang:20-alpine
WORKDIR /app
COPY . /app
# We create .erlang.mk/ as a workaround until https://github.com/ninenines/erlang.mk/pull/744 is merged.
RUN mkdir -p .erlang.mk && make rel
RUN apk add --no-cache make git curl build-base libtool autoconf automake
RUN make rel
EXPOSE 46658
......
......@@ -17,7 +17,7 @@ PROJECT_VERSION = 0.1.0
DEPS = abci_server dynarec erlsha2 gb_merkle_trees jiffy libsodium nist_beacon
dep_abci_server = git https://github.com/KrzysiekJ/abci_server.git v0.5.0
dep_dynarec = git https://github.com/dieswaytoofast/dynarec.git 1f477
dep_erlsha2 = git https://github.com/vinoski/erlsha2 2.2.1
dep_erlsha2 = git https://github.com/vinoski/erlsha2 e3434b33cfeea02609bbf877954d856d895b9e1d
dep_gb_merkle_trees = git https://github.com/KrzysiekJ/gb_merkle_trees.git 1687f8be1187cf0964e63012b175b286af69c21a
dep_jiffy = git https://github.com/davisp/jiffy.git 0.14.11
dep_libsodium = git https://github.com/potatosalad/erlang-libsodium.git 0.0.10
......
......@@ -18,7 +18,7 @@ For support and other ephemeral discussions, see [the #ercoin IRC channel on irc
## Development installation
1. Install [Tendermint](https://tendermint.com) (version 0.17.1).
1. Install [Tendermint](https://tendermint.com) (version 0.19.9).
2. Install [Erlang](https://www.erlang.org) (19 is the minimum version).
3. Clone the Ercoin’s repository and enter the created directory.
4. `dev/bootstrap.sh`
......@@ -40,7 +40,7 @@ If you want to create a custom initial state, functions exported from the `ercoi
To run the project using [Docker Compose](https://docs.docker.com/compose/):
1. `docker-compose build`
2. `docker run -it --rm -v ercoin_home:/tendermint tendermint/tendermint:0.17.1 init`
2. `docker run -it --rm -v ercoin_home:/tendermint tendermint/tendermint:0.19.9 init`
3. `docker run -it --rm -e ERCOIN_HOME=/ercoin -v ercoin_home:/ercoin ercoin_abci-server:latest /app/dev/bootstrap.sh`
4. `docker-compose up`
......@@ -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).
......
{
"app_hash" : "",
"app_state": "g1AAAAH9eNrL4EthYElJLElMZEz8kMiQwsBZmpeSmpaZl5qSyACCWbkMDAwKDARABlMiUwYLWOmDjzMu1RcFrP9bzufLqvV2kvGrcGH/m30q5xWLPatWLPoMVmTGfNJPRP5E7pXrptu3KBnLh5itNFmquz50p+mWd4EiK+5kMIOVWS169K38uaK9x7udUpfrjzKffeNo+OHvXIaFLd/ezZ1xwA+kiIeBsW0BkBb3KHvBANZVfZ3V90fMMltJdQNt1x9cTzee9GmzvnzwQSDbJLalia6boIYT41ABqOEgwAhig3UGvreyrfbZoXXgmbSY+IRT8czLWN+ZTD6S6V5f4TzbZJ9LBlMKA19qUXJ+Zl58al5JUX5BZQqDQFFqTmZiUk4qTAgYZowkOEXRATXIGdFpRrBRf8V0Ost665YFfjT5kfB55fqNh8WkgpRWLn+uZ3Q5rS9OETmGAVv/qfQ=",
"genesis_time" : "0001-01-01T00:00:00.000Z",
"chain_id" : "ercoin-test",
"validators" : [
{
"power" : 64,
"name" : "",
"pub_key" : {
"type" : "ed25519",
"data" : "e0f198d27f7250affd770e4d052aed9233ea57134fd98e24cf2173497aa8a2f3"
}
}
]
"genesis_time": "0001-01-01T00:00:00Z",
"chain_id": "ercoin-test",
"validators": [
{
"pub_key": {
"type": "AC26791624DE60",
"value": "4PGY0n9yUK/9dw5NBSrtkjPqVxNP2Y4kzyFzSXqoovM="
},
"power": 64,
"name": ""
}
],
"app_hash": "",
"app_state": "g1AAAAH9eNrL4EthYElJLElMZEz8kMiQwsBZmpeSmpaZl5qSyACCWbkMDAwKDARABlMiUwYLWOmDjzMu1RcFrP9bzufLqvV2kvGrcGH/m30q5xWLPatWLPoMVmTGfNJPRP5E7pXrptu3KBnLh5itNFmquz50p+mWd4EiK+5kMIOVWS169K38uaK9x7udUpfrjzKffeNo+OHvXIaFLd/ezZ1xwA+kiIeBsW0BkBb3KHvBANZVfZ3V90fMMltJdQNt1x9cTzee9GmzvnzwQSDbJLalia6boIYT41ABqOEgwAhig3UGvreyrfbZoXXgmbSY+IRT8czLWN+ZTD6S6V5f4TzbZJ9LBlMKA19qUXJ+Zl58al5JUX5BZQqDQFFqTmZiUk4qTAgYZowkOEXRATXIGdFpRrBRf8V0Ost665YFfjT5kfB55fqNh8WkgpRWLn+uZ3Q5rS9OETmGAVv/qfQ="
}
{
"address":"4447BBEBF8812823C80FCF301F51431FD7EAA93B",
"pub_key":
{
"type":"ed25519",
"data":"E0F198D27F7250AFFD770E4D052AED9233EA57134FD98E24CF2173497AA8A2F3"
},
"last_height":0,
"last_round":0,
"last_step":0,
"last_signature":null,
"priv_key":
{
"type":"ed25519",
"data":"B24619F57A29A8D3B1EB559E3734CF920313CFD6DD0444D0EB48A5F447EAF5F9E0F198D27F7250AFFD770E4D052AED9233EA57134FD98E24CF2173497AA8A2F3"
}
"address": "146DA449F1259751B2F6D69504D966E98A55F6A6",
"pub_key": {
"type": "AC26791624DE60",
"value": "4PGY0n9yUK/9dw5NBSrtkjPqVxNP2Y4kzyFzSXqoovM="
},
"last_height": 0,
"last_round": 0,
"last_step": 0,
"priv_key": {
"type": "954568A3288910",
"value": "skYZ9XopqNOx61WeNzTPkgMTz9bdBETQ60il9Efq9fng8ZjSf3JQr/13Dk0FKu2SM+pXE0/ZjiTPIXNJeqii8w=="
}
}
......@@ -9,7 +9,7 @@ services:
volumes:
- ercoin_home:/ercoin
tendermint:
image: tendermint/tendermint:0.17.1
image: tendermint/tendermint:0.19.9
ports:
- "46656:46656"
- "46657:46657"
......
......@@ -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(),
......
......@@ -63,6 +63,9 @@ hexstr_to_bin([X,Y|Tail], Acc) ->
{ok, [Byte], []} = io_lib:fread("~16u", [X,Y]),
hexstr_to_bin(Tail, <<Acc/binary, Byte>>).
%% In version 0.19.0 Tendermint changed the serialization format used for genesis.json and priv_validator.json.
%% This is not resembled below. To create files compatible with the newer format, use scripts/wire2amino.go from Tendermint.
%% @doc Convert data to a genesis.json file for Tendermint.
-spec data_to_genesis_json(data()) -> binary().
data_to_genesis_json(Genesis) ->
......
......@@ -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))).