...
 
Commits (5)
stages:
- test
- build
- test
- release
image: erlang:20
image: docker:latest
services:
- docker:dind
variables:
GIT_DEPTH: "3"
CONTAINER_REF_IMAGE: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_NAME}
CONTAINER_LATEST_IMAGE: ${CI_REGISTRY_IMAGE}:latest
test:
stage: test
before_script:
# Workaround for https://github.com/ninenines/erlang.mk/issues/501
- make plt || test $? -eq 2
script:
- make check
- make rel
build:
stage: build
image: docker:latest
script:
- docker build -t ${CONTAINER_REF_IMAGE} .
- docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
- docker push ${CONTAINER_REF_IMAGE}
services:
- docker:dind
tags:
- docker
test:
stage: test
script:
- docker run --rm -t ${CONTAINER_REF_IMAGE} make check
tags:
- docker
release:
stage: release
image: docker:latest
script:
- docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY}
- docker pull ${CONTAINER_REF_IMAGE}
- docker tag ${CONTAINER_REF_IMAGE} ${CONTAINER_LATEST_IMAGE}
- docker push ${CONTAINER_LATEST_IMAGE}
services:
- docker:dind
tags:
- docker
only:
......
FROM erlang:20-alpine
FROM erlang:21-alpine
WORKDIR /app
COPY . /app
RUN apk add --no-cache make git curl build-base libtool autoconf automake
RUN apk add --no-cache make git curl build-base libtool autoconf automake libsodium-dev
RUN make rel
......
......@@ -14,13 +14,14 @@ PROJECT = ercoin
PROJECT_DESCRIPTION = A simple cryptocurrency using Tendermint
PROJECT_VERSION = 0.1.0
DEPS = abci_server dynarec erlsha2 gb_merkle_trees jiffy libsodium 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
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
dep_nist_beacon = git https://gitlab.com/KrzysiekJ/nist_beacon v0.1.1
LOCAL_DEPS = crypto
......@@ -36,10 +37,9 @@ DEP_PLUGINS = lfe.mk
# Whitespace to be used when creating files from templates.
SP = 4
plt: test-deps
include erlang.mk
$(DIALYZER_PLT): test-deps
DIALYZER_DIRS += -r $(TEST_DIR)
# eunit is mentioned explicitly as a workaround until https://github.com/ninenines/erlang.mk/issues/770 is fixed.
PLT_APPS = $(TEST_DEPS) eunit
......
......@@ -20,10 +20,11 @@ For support and other ephemeral discussions, see [the #ercoin IRC channel on irc
1. Install [Tendermint](https://tendermint.com) (version 0.20.0).
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`
5. `make run`
6. In another window, run `tendermint node --home ~/.ercoin/`
3. Install [libsodium](https://libsodium.org) (when using a package manager, you may need to install a separate package containing development files).
4. Clone the Ercoin’s repository and enter the created directory.
5. `dev/bootstrap.sh`
6. `make run`
7. In another window, run `tendermint node --home ~/.ercoin/`
You can play with the node using [`ercoin_wallet`](https://gitlab.com/Ercoin/ercoin_wallet).
......@@ -88,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 address (32 bytes)
* Signature of all the previous fields.
Fee: per tx + per byte + per blocks.
......@@ -180,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 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 +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.
......@@ -197,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
......@@ -215,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 address — 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 “validator primary 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.
......
This diff was suppressed by a .gitattributes entry.
......@@ -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;
......@@ -64,8 +71,7 @@ hexstr_to_bin([X,Y|Tail], Acc) ->
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.
%% This is not resembled below. To create file 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) ->
......@@ -85,71 +91,88 @@ data_to_genesis_json(Genesis) ->
{<<"validators">>, Validators}]},
jiffy:encode(ToEncode, Options).
%% This doesn’t seem to work.
sk_to_priv_validator_json(SK) ->
PK = libsodium_crypto_sign_ed25519:sk_to_pk(SK),
Options = [pretty],
<<_:32/binary, PK:32/binary>> = SK,
Address = binary:part(crypto:hash(sha256, PK), 0, 20),
ToEncode =
{[{<<"address">>, binary_to_hex(crypto:hash(ripemd160, PK))},
{[{<<"address">>, binary_to_hex(Address)},
{<<"last_height">>, 0},
{<<"last_round">>, 0},
{<<"last_signature">>, null},
{<<"last_signbytes">>, <<"">>},
{<<"last_step">>, 0},
%% Outdated format below.
{<<"priv_key">>, [1, binary_to_hex(SK)]},
{<<"pub_key">>, [1, binary_to_hex(PK)]}]},
{<<"priv_key">>,
[{<<"type">>, <<"954568A3288910">>},
{<<"value">>, base64:encode(SK)}]}]},
jiffy:encode(ToEncode, Options).
%% 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, []).
......@@ -161,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.
......@@ -174,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},
......@@ -199,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)).
......@@ -236,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.
......
......@@ -38,9 +38,9 @@ verify_detached(_, _, _) ->
-spec verify_ed25519(Sig :: binary(), Msg :: binary(), PK :: binary()) -> boolean().
verify_ed25519(Signature, Msg, PK) ->
case libsodium_crypto_sign_ed25519:verify_detached(Signature, Msg, PK) of
0 ->
case enacl:sign_verify_detached(Signature, Msg, PK) of
{ok, _} ->
true;
-1 ->
{error, failed_verification} ->
false
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/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,
......@@ -86,7 +86,7 @@ to_sign(
-spec sign_detached(binary(), secret()) -> binary().
sign_detached(Msg, {SK, Proof}) ->
RawSig = libsodium_crypto_sign_ed25519:detached(Msg, SK),
RawSig = enacl:sign_detached(Msg, SK),
ProofBin =
case Proof of
none ->
......@@ -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(
fun
(_, true) ->
true;
(AccountSerialized, false) ->
Account = ercoin_account:deserialize(AccountSerialized),
Account#account.validator_pk =:= ValidatorAddress andalso
Account#account.locked_until > NextEpochStart andalso
Account#account.balance > 0
end,
false,
Data#data.accounts)
end,
Addresses)
gb_merkle_trees:to_orddict(ercoin_validators:draw(Data)))
end).