Merge branch 'pharpend/develop' of github.com:aeternity/Vanillae into pharpend/develop
This commit is contained in:
@@ -16,10 +16,6 @@ Vanillae is just a knife.
|
|||||||
|
|
||||||
## Flagship Tools
|
## Flagship Tools
|
||||||
|
|
||||||
- Jaeck Russell
|
|
||||||
|
|
||||||
A minimal browser wallet extension. Not included in this repository yet.
|
|
||||||
|
|
||||||
- [Sidekick](./sidekick/)
|
- [Sidekick](./sidekick/)
|
||||||
|
|
||||||
A library to talk to a browser wallet extension from the perspective of a
|
A library to talk to a browser wallet extension from the perspective of a
|
||||||
@@ -49,25 +45,6 @@ out.
|
|||||||
consist of what data the waellet expects from the aepp, and what data the
|
consist of what data the waellet expects from the aepp, and what data the
|
||||||
aepp expects from the waellet.
|
aepp expects from the waellet.
|
||||||
|
|
||||||
- [Parasite](./libs/parasite/)
|
|
||||||
|
|
||||||
**NOT FOR PRODUCTION USE**
|
|
||||||
|
|
||||||
This is a library for talking to Aeternity HTTP nodes from the perspective
|
|
||||||
of a page script. Used in example/documentation code for things that your
|
|
||||||
backend should do.
|
|
||||||
|
|
||||||
This may eventually be polished and repackaged as a production quality
|
|
||||||
library.
|
|
||||||
|
|
||||||
- libjr
|
|
||||||
|
|
||||||
This is a library to talk to a page script ("aepp") from the perspective of
|
|
||||||
the wallet ("waellet"). It essentially is sidekick from the perspective of
|
|
||||||
the wallet.
|
|
||||||
|
|
||||||
Not yet included in this repository.
|
|
||||||
|
|
||||||
|
|
||||||
## Utilities
|
## Utilities
|
||||||
|
|
||||||
|
|||||||
@@ -40,31 +40,39 @@
|
|||||||
% AE node JSON query interface functions
|
% AE node JSON query interface functions
|
||||||
-export([top_height/0, top_block/0,
|
-export([top_height/0, top_block/0,
|
||||||
kb_current/0, kb_current_hash/0, kb_current_height/0,
|
kb_current/0, kb_current_hash/0, kb_current_height/0,
|
||||||
% kb_pending/0,
|
kb_pending/0,
|
||||||
kb_by_hash/1, kb_by_height/1,
|
kb_by_hash/1, kb_by_height/1,
|
||||||
% kb_insert/1,
|
% kb_insert/1,
|
||||||
mb_header/1, mb_txs/1, mb_tx_index/2, mb_tx_count/1,
|
mb_header/1, mb_txs/1, mb_tx_index/2, mb_tx_count/1,
|
||||||
gen_current/0, gen_by_id/1, gen_by_height/1,
|
gen_current/0, gen_by_id/1, gen_by_height/1,
|
||||||
acc/1, acc_at_height/2, acc_at_block_id/2,
|
acc/1, acc_at_height/2, acc_at_block_id/2,
|
||||||
% acc_pending_txs/1,
|
acc_pending_txs/1,
|
||||||
next_nonce/1,
|
next_nonce/1,
|
||||||
dry_run/1, dry_run/2,
|
dry_run/1, dry_run/2, dry_run/3,
|
||||||
tx/1, tx_info/1,
|
tx/1, tx_info/1,
|
||||||
post_tx/1,
|
post_tx/1,
|
||||||
contract/1, contract_code/1,
|
contract/1, contract_code/1,
|
||||||
% contract_poi/1,
|
contract_poi/1,
|
||||||
% oracle/1, oracle_queries/1, oracle_queries_by_id/2,
|
% oracle/1, oracle_queries/1, oracle_queries_by_id/2,
|
||||||
name/1,
|
name/1,
|
||||||
% channel/1,
|
% channel/1,
|
||||||
peer_pubkey/0,
|
peer_pubkey/0,
|
||||||
status/0]).
|
status/0,
|
||||||
% status_chainends/0]).
|
status_chainends/0]).
|
||||||
|
|
||||||
% AE contract call and serialization interface functions
|
% AE contract call and serialization interface functions
|
||||||
-export([read_aci/1,
|
-export([read_aci/1,
|
||||||
|
min_gas/0,
|
||||||
|
min_gas_price/0,
|
||||||
|
min_fee/0,
|
||||||
|
contract_create/3,
|
||||||
|
contract_create/8,
|
||||||
prepare_contract/1,
|
prepare_contract/1,
|
||||||
|
contract_call/5,
|
||||||
contract_call/6,
|
contract_call/6,
|
||||||
contract_call/10]).
|
contract_call/10,
|
||||||
|
verify_signature/3]).
|
||||||
|
|
||||||
|
|
||||||
% OTP Application Interface
|
% OTP Application Interface
|
||||||
%-export([start/0, stop/0]).
|
%-export([start/0, stop/0]).
|
||||||
@@ -73,7 +81,7 @@
|
|||||||
|
|
||||||
%%% Types
|
%%% Types
|
||||||
|
|
||||||
-export_type([ae_node/0, network_id/0]).
|
-export_type([ae_node/0, network_id/0, ae_error/0]).
|
||||||
|
|
||||||
|
|
||||||
-type ae_node() :: {inet:ip_address(), inet:port_number()}.
|
-type ae_node() :: {inet:ip_address(), inet:port_number()}.
|
||||||
@@ -88,9 +96,9 @@
|
|||||||
| {headers, map()}
|
| {headers, map()}
|
||||||
| bad_length
|
| bad_length
|
||||||
| gc_out_of_range.
|
| gc_out_of_range.
|
||||||
-type pubkey() :: string(). % "ak_" ++ _
|
-type pubkey() :: unicode:chardata(). % "ak_" ++ _
|
||||||
-type account_id() :: pubkey().
|
-type account_id() :: pubkey().
|
||||||
-type contract_id() :: string(). % "ct_" ++ _
|
-type contract_id() :: unicode:chardata(). % "ct_" ++ _
|
||||||
-type peer_pubkey() :: string(). % "pp_" ++ _
|
-type peer_pubkey() :: string(). % "pp_" ++ _
|
||||||
-type keyblock_hash() :: string(). % "kh_" ++ _
|
-type keyblock_hash() :: string(). % "kh_" ++ _
|
||||||
-type contract_byte_array() :: string(). % "cb_" ++ _
|
-type contract_byte_array() :: string(). % "cb_" ++ _
|
||||||
@@ -134,7 +142,20 @@
|
|||||||
% "block_height" => pos_integer(),
|
% "block_height" => pos_integer(),
|
||||||
% "hash" => tx_hash(),
|
% "hash" => tx_hash(),
|
||||||
% "signatures" => [signature()],
|
% "signatures" => [signature()],
|
||||||
% "tx" => map()}. % FIXME
|
% "tx" =>
|
||||||
|
% #{"abi_version" => pos_integer(),
|
||||||
|
% "amount" => non_neg_integer(),
|
||||||
|
% "call_data" => contract_byte_array(),
|
||||||
|
% "code" => contract_byte_array(),
|
||||||
|
% "deposit" => non_neg_integer(),
|
||||||
|
% "fee" => pos_integer(),
|
||||||
|
% "gas" => pos_integer(),
|
||||||
|
% "gas_price" => pos_integer(),
|
||||||
|
% "nonce" => pos_integer(),
|
||||||
|
% "owner_id" => account_id(),
|
||||||
|
% "type" => string(),
|
||||||
|
% "version" => pos_integer(),
|
||||||
|
% "vm_version" => pos_integer()}}
|
||||||
-type generation() :: #{string() => term()}.
|
-type generation() :: #{string() => term()}.
|
||||||
% #{"key_block" => keyblock(),
|
% #{"key_block" => keyblock(),
|
||||||
% "micro_blocks" => [microblock_hash()]}.
|
% "micro_blocks" => [microblock_hash()]}.
|
||||||
@@ -321,10 +342,15 @@ kb_current_height() ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
%-spec kb_pending() ->
|
-spec kb_pending() -> {ok, keyblock_hash()} | {error, Reason}
|
||||||
%
|
when Reason :: string().
|
||||||
%kb_pending() ->
|
%% @doc
|
||||||
% request("/v2/key-blocks/pending").
|
%% Request the hash of the pending keyblock of a mining node's beneficiary.
|
||||||
|
%% If the node queried is not configured for mining it will return
|
||||||
|
%% `{error, "Beneficiary not configured"}'
|
||||||
|
|
||||||
|
kb_pending() ->
|
||||||
|
result(request("/v2/key-blocks/pending")).
|
||||||
|
|
||||||
|
|
||||||
-spec kb_by_hash(ID) -> {ok, KeyBlock} | {error, Reason}
|
-spec kb_by_hash(ID) -> {ok, KeyBlock} | {error, Reason}
|
||||||
@@ -488,16 +514,15 @@ acc_at_block_id(AccountID, BlockID) ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
% TODO
|
-spec acc_pending_txs(AccountID) -> {ok, TXs} | {error, Reason}
|
||||||
%-spec acc_pending_txs(AccountID) -> {ok, TXs} | {error, Reason}
|
when AccountID :: account_id(),
|
||||||
% when AccountID :: account_id(),
|
TXs :: [tx_hash()],
|
||||||
% TXs ::
|
Reason :: ae_error() | string().
|
||||||
% Reason ::
|
%% @doc
|
||||||
%%% @doc
|
%% Retrieve a list of transactions pending for the given account.
|
||||||
%%% Retrieve a list of transactions pending for the given account.
|
|
||||||
%
|
acc_pending_txs(AccountID) ->
|
||||||
%acc_pending_txs(AccountID) ->
|
request(["/v2/accounts/", AccountID, "/transactions/pending"]).
|
||||||
% request(["/v2/accounts/", AccountID, "/transactions/pending"]).
|
|
||||||
|
|
||||||
|
|
||||||
-spec next_nonce(AccountID) -> {ok, Nonce} | {error, Reason}
|
-spec next_nonce(AccountID) -> {ok, Nonce} | {error, Reason}
|
||||||
@@ -508,8 +533,14 @@ acc_at_block_id(AccountID, BlockID) ->
|
|||||||
%% Retrieve the next nonce for the given account
|
%% Retrieve the next nonce for the given account
|
||||||
|
|
||||||
next_nonce(AccountID) ->
|
next_nonce(AccountID) ->
|
||||||
case request(["/v2/accounts/", AccountID, "/next-nonce"]) of
|
% case request(["/v2/accounts/", AccountID, "/next-nonce"]) of
|
||||||
{ok, #{"next_nonce" := Nonce}} -> {ok, Nonce};
|
% {ok, #{"next_nonce" := Nonce}} -> {ok, Nonce};
|
||||||
|
% {ok, #{"reason" := "Account not found"}} -> {ok, 1};
|
||||||
|
% {ok, #{"reason" := Reason}} -> {error, Reason};
|
||||||
|
% Error -> Error
|
||||||
|
% end.
|
||||||
|
case request(["/v2/accounts/", AccountID]) of
|
||||||
|
{ok, #{"nonce" := Nonce}} -> {ok, Nonce + 1};
|
||||||
{ok, #{"reason" := "Account not found"}} -> {ok, 1};
|
{ok, #{"reason" := "Account not found"}} -> {ok, 1};
|
||||||
{ok, #{"reason" := Reason}} -> {error, Reason};
|
{ok, #{"reason" := Reason}} -> {error, Reason};
|
||||||
Error -> Error
|
Error -> Error
|
||||||
@@ -527,27 +558,45 @@ next_nonce(AccountID) ->
|
|||||||
%% {ok, Hash} = vanillae:kb_current_hash(),
|
%% {ok, Hash} = vanillae:kb_current_hash(),
|
||||||
%% vanilla:dry_run(TX, Hash),
|
%% vanilla:dry_run(TX, Hash),
|
||||||
%% '''
|
%% '''
|
||||||
|
%% NOTE:
|
||||||
|
%% For this function to work the Aeternity node you are sending the request
|
||||||
|
%% to must have its configuration set to `http: endpoints: dry-run: true'
|
||||||
|
|
||||||
dry_run(TX) ->
|
dry_run(TX) ->
|
||||||
|
dry_run(TX, []).
|
||||||
|
|
||||||
|
|
||||||
|
-spec dry_run(TX, Accounts) -> {ok, Result} | {error, Reason}
|
||||||
|
when TX :: binary() | string(),
|
||||||
|
Accounts :: [pubkey()],
|
||||||
|
Result :: term(), % FIXME
|
||||||
|
Reason :: term(). % FIXME
|
||||||
|
|
||||||
|
dry_run(TX, Accounts) ->
|
||||||
case kb_current_hash() of
|
case kb_current_hash() of
|
||||||
{ok, Hash} -> dry_run(TX, Hash);
|
{ok, Hash} -> dry_run(TX, Accounts, Hash);
|
||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
-spec dry_run(TX, KBHash) -> {ok, Result} | {error, Reason}
|
-spec dry_run(TX, Accounts, KBHash) -> {ok, Result} | {error, Reason}
|
||||||
when TX :: binary() | string(),
|
when TX :: binary() | string(),
|
||||||
KBHash :: binary() | string(),
|
Accounts :: [pubkey()],
|
||||||
Result :: term(), % FIXME
|
KBHash :: binary() | string(),
|
||||||
Reason :: term(). % FIXME
|
Result :: term(), % FIXME
|
||||||
|
Reason :: term(). % FIXME
|
||||||
%% @doc
|
%% @doc
|
||||||
%% Execute a read-only transaction on the chain at the height indicated by the
|
%% Execute a read-only transaction on the chain at the height indicated by the
|
||||||
%% hash provided.
|
%% hash provided.
|
||||||
|
|
||||||
dry_run(TX, KBHash) ->
|
dry_run(TX, Accounts, KBHash) ->
|
||||||
KBB = to_binary(KBHash),
|
KBB = to_binary(KBHash),
|
||||||
TXB = to_binary(TX),
|
TXB = to_binary(TX),
|
||||||
JSON = zj:binary_encode(#{top => KBB, accounts => [], txs => [#{tx => TXB}]}),
|
DryData = #{top => KBB,
|
||||||
|
accounts => Accounts,
|
||||||
|
txs => [#{tx => TXB}],
|
||||||
|
tx_events => true},
|
||||||
|
JSON = zj:binary_encode(DryData),
|
||||||
request("/v2/dry-run", JSON).
|
request("/v2/dry-run", JSON).
|
||||||
|
|
||||||
to_binary(S) when is_binary(S) -> S;
|
to_binary(S) when is_binary(S) -> S;
|
||||||
@@ -584,7 +633,8 @@ tx_info(ID) ->
|
|||||||
%% Post a transaction to the chain.
|
%% Post a transaction to the chain.
|
||||||
|
|
||||||
post_tx(Data) ->
|
post_tx(Data) ->
|
||||||
request("/v2/transactions", Data).
|
JSON = zj:binary_encode(#{tx => Data}),
|
||||||
|
request("/v2/transactions", JSON).
|
||||||
|
|
||||||
|
|
||||||
-spec contract(ID) -> {ok, ContractData} | {error, Reason}
|
-spec contract(ID) -> {ok, ContractData} | {error, Reason}
|
||||||
@@ -611,11 +661,13 @@ contract_code(ID) ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
% FIXME: Is this broken? Seems to just stall
|
-spec contract_poi(ID) -> {ok, Bytecode} | {error, Reason}
|
||||||
% -spec conract_poi(ID) ->
|
when ID :: contract_id(),
|
||||||
%
|
Bytecode :: contract_byte_array(),
|
||||||
%contract_poi(ID) ->
|
Reason :: ae_error() | string().
|
||||||
% request(["/v2/contracts/", ID, "/poi"]).
|
|
||||||
|
contract_poi(ID) ->
|
||||||
|
request(["/v2/contracts/", ID, "/poi"]).
|
||||||
|
|
||||||
% TODO
|
% TODO
|
||||||
%oracle(ID) ->
|
%oracle(ID) ->
|
||||||
@@ -675,23 +727,22 @@ status() ->
|
|||||||
request("/v2/status").
|
request("/v2/status").
|
||||||
|
|
||||||
|
|
||||||
% TODO
|
-spec status_chainends() -> {ok, ChainEnds} | {error, Reason}
|
||||||
%-spec status_chainends() -> {ok, ChainEnds} | {error, Reason}
|
when ChainEnds :: [keyblock_hash()],
|
||||||
% when ChainEnds :: [keyblock_hash()],
|
Reason :: ae_error().
|
||||||
% Reason :: ae_error().
|
%% @doc
|
||||||
%%% @doc
|
%% Retrieve the latest keyblock hashes
|
||||||
%%% Retrieve the latest keyblock hashes
|
|
||||||
%
|
status_chainends() ->
|
||||||
%status_chainends() ->
|
request("/v2/status/chain-ends").
|
||||||
% request("/v2/status/chain-ends").
|
|
||||||
|
|
||||||
|
|
||||||
request(Path) ->
|
request(Path) ->
|
||||||
vanillae_man:request(Path).
|
vanillae_man:request(unicode:characters_to_list(Path)).
|
||||||
|
|
||||||
|
|
||||||
request(Path, Payload) ->
|
request(Path, Payload) ->
|
||||||
vanillae_man:request(Path, Payload).
|
vanillae_man:request(unicode:characters_to_list(Path), Payload).
|
||||||
|
|
||||||
|
|
||||||
result({ok, #{"reason" := Reason}}) -> {error, Reason};
|
result({ok, #{"reason" := Reason}}) -> {error, Reason};
|
||||||
@@ -701,6 +752,236 @@ result(Received) -> Received.
|
|||||||
|
|
||||||
%%% Contract calls
|
%%% Contract calls
|
||||||
|
|
||||||
|
-spec contract_create(CreatorID, Path, InitArgs) -> Result
|
||||||
|
when CreatorID :: unicode:chardata(),
|
||||||
|
Path :: file:filename(),
|
||||||
|
InitArgs :: [string()],
|
||||||
|
Result :: {ok, CreateTX} | {error, Reason},
|
||||||
|
CreateTX :: binary(),
|
||||||
|
Reason :: file:posix() | term().
|
||||||
|
%% @doc
|
||||||
|
%% This function reads the source of a Sophia contract (an .aes file)
|
||||||
|
%% and returns the unsigned create contract call data with default values.
|
||||||
|
%% For more control over exactly what those values are, use create_contract/8.
|
||||||
|
|
||||||
|
contract_create(CreatorID, Path, InitArgs) ->
|
||||||
|
case next_nonce(CreatorID) of
|
||||||
|
{ok, Nonce} ->
|
||||||
|
Amount = 0,
|
||||||
|
Gas = 100000,
|
||||||
|
GasPrice = min_gas_price(),
|
||||||
|
Fee = min_fee(),
|
||||||
|
contract_create(CreatorID, Nonce,
|
||||||
|
Amount, Gas, GasPrice, Fee,
|
||||||
|
Path, InitArgs);
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
-spec contract_create(CreatorID, Nonce,
|
||||||
|
Amount, Gas, GasPrice, Fee,
|
||||||
|
Path, InitArgs) -> Result
|
||||||
|
when CreatorID :: unicode:chardata(),
|
||||||
|
Nonce :: pos_integer(),
|
||||||
|
Amount :: non_neg_integer(),
|
||||||
|
Gas :: pos_integer(),
|
||||||
|
GasPrice :: pos_integer(),
|
||||||
|
Fee :: non_neg_integer(),
|
||||||
|
Path :: file:filename(),
|
||||||
|
InitArgs :: [string()],
|
||||||
|
Result :: {ok, CreateTX} | {error, Reason},
|
||||||
|
CreateTX :: binary(),
|
||||||
|
Reason :: term().
|
||||||
|
%% @doc
|
||||||
|
%% Create a "create contract" call using the supplied values.
|
||||||
|
%%
|
||||||
|
%% Contract creation is an even more opaque process than contract calls if you're new
|
||||||
|
%% to Aeternity.
|
||||||
|
%%
|
||||||
|
%% The meaning of each argument is as follows:
|
||||||
|
%% <ul>
|
||||||
|
%% <li>
|
||||||
|
%% <b>CreatorID:</b>
|
||||||
|
%% This is the <em>public</em> key of the entity who will be posting the contract
|
||||||
|
%% to the chain.
|
||||||
|
%% The key must be encoded as a binary string prefixed with <<"ak_">>.
|
||||||
|
%% The returned call will still need to be signed by the caller's <em>private</em>
|
||||||
|
%% key.
|
||||||
|
%% </li>
|
||||||
|
%% <li>
|
||||||
|
%% <b>Nonce:</b>
|
||||||
|
%% This is a sequential integer value that ensures that the hash value of two
|
||||||
|
%% sequential signed calls with the same contract ID, function and arguments can
|
||||||
|
%% never be the same.
|
||||||
|
%% This avoids replay attacks and ensures indempotency despite the distributed
|
||||||
|
%% nature of the blockchain network).
|
||||||
|
%% Every CallerID on the chain has a "next nonce" value that can be discovered by
|
||||||
|
%% querying your Aeternity node (via `vanillae:next_nonce(CallerID)', for example).
|
||||||
|
%% </li>
|
||||||
|
%% <li>
|
||||||
|
%% <b>Amount:</b>
|
||||||
|
%% All Aeternity transactions can carry an "amount" spent from the origin account
|
||||||
|
%% (in this case the `CallerID') to the destination. In a "Spend" transaction this
|
||||||
|
%% is the only value that really matters, but in a contract call the utility is
|
||||||
|
%% quite different, as you can pay money <em>into</em> a contract and have that
|
||||||
|
%% contract hold it (for future payouts, to be held in escrow, as proof of intent
|
||||||
|
%% to purchase or engage in an auction, whatever). Typically this value is 0, but
|
||||||
|
%% of course there are very good reasons why it should be set to a non-zero value
|
||||||
|
%% in the case of calls related to contract-governed payment systems.
|
||||||
|
%% </li>
|
||||||
|
%% <li>
|
||||||
|
%% <b>Gas:</b>
|
||||||
|
%% This number sets a limit on the maximum amount of computation the caller is willing
|
||||||
|
%% to pay for on the chain.
|
||||||
|
%% Both storage and thunks are costly as the entire Aeternity network must execute,
|
||||||
|
%% verify, store and replicate all state changes to the chain.
|
||||||
|
%% Each byte stored on the chain carries a cost of 20 gas, which is not an issue if
|
||||||
|
%% you are storing persistent values of some state trasforming computation, but
|
||||||
|
%% high enough to discourage frivolous storage of media on the chain (which would be
|
||||||
|
%% a burden to the entire network).
|
||||||
|
%% Computation is less expensive, but still costs and is calculated very similarly
|
||||||
|
%% to the Erlang runtime's per-process reduction budget.
|
||||||
|
%% The maximum amount of gas that a microblock is permitted to carry (its maximum
|
||||||
|
%% computational weight, so to speak) is 6,000,000.
|
||||||
|
%% Typical contract calls range between about 100 to 15,000 gas, so the default gas
|
||||||
|
%% limit set by the `contract_call/6' function is only 20,000.
|
||||||
|
%% Setting the gas limit to 6,000,000 or more will cause your contract call to fail.
|
||||||
|
%% All transactions cost some gas with the exception of stateless or read-only
|
||||||
|
%% calls to your Aeternity node (executed as "dry run" calls and not propagated to
|
||||||
|
%% the network).
|
||||||
|
%% The gas consumed by the contract call transaction is multiplied by the `GasPrice'
|
||||||
|
%% provided and rolled into the block reward paid out to the node that mines the
|
||||||
|
%% transaction into a microblock.
|
||||||
|
%% Unused gas is refunded to the caller.
|
||||||
|
%% </li>
|
||||||
|
%% <li>
|
||||||
|
%% <b>GasPrice:</b>
|
||||||
|
%% This is a factor that is used calculate a value in aettos (the smallest unit of
|
||||||
|
%% Aeternity's currency value) for the gas consumed. In times of high contention
|
||||||
|
%% in the mempool increasing the gas price increases the value of mining a given
|
||||||
|
%% transaction, thus making miners more likely to prioritize the high value ones.
|
||||||
|
%% </li>
|
||||||
|
%% <li>
|
||||||
|
%% <b>Fee:</b>
|
||||||
|
%% This value should really be caled `Bribe' or `Tip'.
|
||||||
|
%% This is a flat fee in aettos that is paid into the block reward, thereby allowing
|
||||||
|
%% an additional way to prioritize a given transaction above others, even if the
|
||||||
|
%% transaction will not consume much gas.
|
||||||
|
%% </li>
|
||||||
|
%% <li>
|
||||||
|
%% <b>ACI:</b>
|
||||||
|
%% This is the compiled contract's metadata. It provides the information necessary
|
||||||
|
%% for the contract call data to be formed in a way that the Aeternity runtime will
|
||||||
|
%% understand.
|
||||||
|
%% This ACI data must be already formatted in the native Erlang format as an .aci
|
||||||
|
%% file rather than as the JSON serialized format produced by the Sophia CLI tool.
|
||||||
|
%% The easiest way to create native ACI data is to use the Aeternity Launcher,
|
||||||
|
%% a GUI tool with a "Developers' Workbench" feature that can assist with this.
|
||||||
|
%% </li>
|
||||||
|
%% <li>
|
||||||
|
%% <b>ConID:</b>
|
||||||
|
%% This is the on-chain address of the contract instance that is to be called.
|
||||||
|
%% Note, this is different from the `name' of the contract, as a single contract may
|
||||||
|
%% be deployed multiple times.
|
||||||
|
%% </li>
|
||||||
|
%% <li>
|
||||||
|
%% <b>Fun:</b>
|
||||||
|
%% This is the name of the entrypoint function to be called on the contract,
|
||||||
|
%% provided as a string (not a binary string, but a textual string as a list).
|
||||||
|
%% </li>
|
||||||
|
%% <li>
|
||||||
|
%% <b>Args:</b>
|
||||||
|
%% This is a list of the arguments to provide to the function, listed in order
|
||||||
|
%% according to the function's spec, and represented as strings (that is, an integer
|
||||||
|
%% argument of `10' must be cast to the textual representation `"10"').
|
||||||
|
%% </li>
|
||||||
|
%% '''
|
||||||
|
%% As should be obvious from the above description, it is pretty helpful to have a
|
||||||
|
%% source copy of the contract you intend to call so that you can re-generate the ACI
|
||||||
|
%% if you do not already have a copy, and can check the spec of a function before
|
||||||
|
%% trying to form a contract call.
|
||||||
|
|
||||||
|
contract_create(CreatorID, Nonce,
|
||||||
|
Amount, Gas, GasPrice, Fee,
|
||||||
|
Path, InitArgs) ->
|
||||||
|
case aeso_compiler:file(Path, [{aci, json}]) of
|
||||||
|
{ok, Compiled} ->
|
||||||
|
contract_create2(CreatorID, Nonce,
|
||||||
|
Amount, Gas, GasPrice, Fee,
|
||||||
|
Compiled, InitArgs);
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
contract_create2(CreatorID, Nonce,
|
||||||
|
Amount, Gas, GasPrice, Fee,
|
||||||
|
Compiled, InitArgs) ->
|
||||||
|
AACI = prepare_aaci(maps:get(aci, Compiled)),
|
||||||
|
case encode_call_data(AACI, "init", InitArgs) of
|
||||||
|
{ok, CallData} ->
|
||||||
|
contract_create3(CreatorID, Nonce,
|
||||||
|
Amount, Gas, GasPrice, Fee,
|
||||||
|
Compiled, CallData);
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
contract_create3(CreatorID, Nonce,
|
||||||
|
Amount, Gas, GasPrice, Fee,
|
||||||
|
Compiled, CallData) ->
|
||||||
|
PK = unicode:characters_to_binary(CreatorID),
|
||||||
|
try
|
||||||
|
{account_pubkey, OwnerID} = aeser_api_encoder:decode(PK),
|
||||||
|
contract_create4(OwnerID, Nonce,
|
||||||
|
Amount, Gas, GasPrice, Fee,
|
||||||
|
Compiled, CallData)
|
||||||
|
catch
|
||||||
|
Error:Reason -> {Error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
contract_create4(OwnerID, Nonce,
|
||||||
|
Amount, Gas, GasPrice, Fee,
|
||||||
|
Compiled, CallData) ->
|
||||||
|
Code = aeser_contract_code:serialize(Compiled),
|
||||||
|
VM = 7,
|
||||||
|
ABI = 3,
|
||||||
|
<<CTVersion:32>> = <<VM:16, ABI:16>>,
|
||||||
|
ContractCreateVersion = 1,
|
||||||
|
TTL = 0,
|
||||||
|
Type = contract_create_tx,
|
||||||
|
Fields =
|
||||||
|
[{owner_id, aeser_id:create(account, OwnerID)},
|
||||||
|
{nonce, Nonce},
|
||||||
|
{code, Code},
|
||||||
|
{ct_version, CTVersion},
|
||||||
|
{fee, Fee},
|
||||||
|
{ttl, TTL},
|
||||||
|
{deposit, 0},
|
||||||
|
{amount, Amount},
|
||||||
|
{gas, Gas},
|
||||||
|
{gas_price, GasPrice},
|
||||||
|
{call_data, CallData}],
|
||||||
|
Template =
|
||||||
|
[{owner_id, id},
|
||||||
|
{nonce, int},
|
||||||
|
{code, binary},
|
||||||
|
{ct_version, int},
|
||||||
|
{fee, int},
|
||||||
|
{ttl, int},
|
||||||
|
{deposit, int},
|
||||||
|
{amount, int},
|
||||||
|
{gas, int},
|
||||||
|
{gas_price, int},
|
||||||
|
{call_data, binary}],
|
||||||
|
TXB = aeser_chain_objects:serialize(Type, ContractCreateVersion, Template, Fields),
|
||||||
|
try
|
||||||
|
{ok, aeser_api_encoder:encode(transaction, TXB)}
|
||||||
|
catch
|
||||||
|
error:Reason -> {error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
-spec read_aci(Path) -> Result
|
-spec read_aci(Path) -> Result
|
||||||
when Path :: file:filename(),
|
when Path :: file:filename(),
|
||||||
Result :: {ok, ACI} | {error, Reason},
|
Result :: {ok, ACI} | {error, Reason},
|
||||||
@@ -733,14 +1014,15 @@ read_aci(Path) ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
-spec contract_call(CallerID, Nonce, ACI, ConID, Fun, Args) -> CallTX
|
-spec contract_call(CallerID, AACI, ConID, Fun, Args) -> Result
|
||||||
when CallerID :: binary(),
|
when CallerID :: unicode:chardata(),
|
||||||
Nonce :: pos_integer(),
|
AACI :: map(),
|
||||||
ACI :: binary(),
|
ConID :: unicode:chardata(),
|
||||||
ConID :: binary(),
|
|
||||||
Fun :: string(),
|
Fun :: string(),
|
||||||
Args :: [string()],
|
Args :: [string()],
|
||||||
CallTX :: string().
|
Result :: {ok, CallTX} | {error, Reason},
|
||||||
|
CallTX :: binary(),
|
||||||
|
Reason :: term().
|
||||||
%% @doc
|
%% @doc
|
||||||
%% Form a contract call using hardcoded default values for `Gas', `GasPrice', `Fee',
|
%% Form a contract call using hardcoded default values for `Gas', `GasPrice', `Fee',
|
||||||
%% and `Amount' to simplify the call (10 args is a bit much for normal calls!).
|
%% and `Amount' to simplify the call (10 args is a bit much for normal calls!).
|
||||||
@@ -750,30 +1032,60 @@ read_aci(Path) ->
|
|||||||
%% For details on the meaning of these and other argument values see the doc comment
|
%% For details on the meaning of these and other argument values see the doc comment
|
||||||
%% for contract_call/10.
|
%% for contract_call/10.
|
||||||
|
|
||||||
contract_call(CallerID, Nonce, ACI, ConID, Fun, Args) ->
|
contract_call(CallerID, AACI, ConID, Fun, Args) ->
|
||||||
Gas = 20000,
|
{ok, Nonce} = next_nonce(CallerID),
|
||||||
|
Gas = min_gas(),
|
||||||
GasPrice = min_gas_price(),
|
GasPrice = min_gas_price(),
|
||||||
Fee = 20000,
|
Fee = min_fee(),
|
||||||
Amount = 0,
|
Amount = 0,
|
||||||
contract_call(CallerID, Nonce,
|
contract_call(CallerID, Nonce,
|
||||||
Gas, GasPrice, Fee, Amount,
|
Gas, GasPrice, Fee, Amount,
|
||||||
ACI, ConID, Fun, Args).
|
AACI, ConID, Fun, Args).
|
||||||
|
|
||||||
|
|
||||||
|
-spec contract_call(CallerID, Gas, AACI, ConID, Fun, Args) -> Result
|
||||||
|
when CallerID :: unicode:chardata(),
|
||||||
|
Gas :: pos_integer(),
|
||||||
|
AACI :: map(),
|
||||||
|
ConID :: unicode:chardata(),
|
||||||
|
Fun :: string(),
|
||||||
|
Args :: [string()],
|
||||||
|
Result :: {ok, CallTX} | {error, Reason},
|
||||||
|
CallTX :: binary(),
|
||||||
|
Reason :: term().
|
||||||
|
%% @doc
|
||||||
|
%% Just like contract_call/5, but allows you to specify the amount of gas
|
||||||
|
%% without getting into a major adventure with the other arguments.
|
||||||
|
%%
|
||||||
|
%% For details on the meaning of these and other argument values see the doc comment
|
||||||
|
%% for contract_call/10.
|
||||||
|
|
||||||
|
contract_call(CallerID, Gas, AACI, ConID, Fun, Args) ->
|
||||||
|
{ok, Nonce} = next_nonce(CallerID),
|
||||||
|
GasPrice = min_gas_price(),
|
||||||
|
Fee = min_fee(),
|
||||||
|
Amount = 0,
|
||||||
|
contract_call(CallerID, Nonce,
|
||||||
|
Gas, GasPrice, Fee, Amount,
|
||||||
|
AACI, ConID, Fun, Args).
|
||||||
|
|
||||||
|
|
||||||
-spec contract_call(CallerID, Nonce,
|
-spec contract_call(CallerID, Nonce,
|
||||||
Gas, GasPrice, Fee, Amount,
|
Gas, GasPrice, Fee, Amount,
|
||||||
ACI, ConID, Fun, Args) -> CallTX
|
AACI, ConID, Fun, Args) -> Result
|
||||||
when CallerID :: binary(),
|
when CallerID :: unicode:chardata(),
|
||||||
Nonce :: pos_integer(),
|
Nonce :: pos_integer(),
|
||||||
Gas :: pos_integer(),
|
Gas :: pos_integer(),
|
||||||
GasPrice :: pos_integer(),
|
GasPrice :: pos_integer(),
|
||||||
Fee :: non_neg_integer(),
|
Fee :: non_neg_integer(),
|
||||||
Amount :: pos_integer(),
|
Amount :: non_neg_integer(),
|
||||||
ACI :: binary(),
|
AACI :: map(),
|
||||||
ConID :: binary(),
|
ConID :: unicode:chardata(),
|
||||||
Fun :: string(),
|
Fun :: string(),
|
||||||
Args :: [string()],
|
Args :: [string()],
|
||||||
CallTX :: string().
|
Result :: {ok, CallTX} | {error, Reason},
|
||||||
|
CallTX :: binary(),
|
||||||
|
Reason :: term().
|
||||||
%% @doc
|
%% @doc
|
||||||
%% Form a contract call using the supplied values.
|
%% Form a contract call using the supplied values.
|
||||||
%%
|
%%
|
||||||
@@ -786,7 +1098,8 @@ contract_call(CallerID, Nonce, ACI, ConID, Fun, Args) ->
|
|||||||
%% <b>CallerID:</b>
|
%% <b>CallerID:</b>
|
||||||
%% This is the <em>public</em> key of the entity making the contract call.
|
%% This is the <em>public</em> key of the entity making the contract call.
|
||||||
%% The key must be encoded as a binary string prefixed with <<"ak_">>.
|
%% The key must be encoded as a binary string prefixed with <<"ak_">>.
|
||||||
%% The returned call will still need to be signed by the caller's <em>private</em>.
|
%% The returned call will still need to be signed by the caller's <em>private</em>
|
||||||
|
%% key.
|
||||||
%% </li>
|
%% </li>
|
||||||
%% <li>
|
%% <li>
|
||||||
%% <b>Nonce:</b>
|
%% <b>Nonce:</b>
|
||||||
@@ -796,8 +1109,7 @@ contract_call(CallerID, Nonce, ACI, ConID, Fun, Args) ->
|
|||||||
%% This avoids replay attacks and ensures indempotency despite the distributed
|
%% This avoids replay attacks and ensures indempotency despite the distributed
|
||||||
%% nature of the blockchain network).
|
%% nature of the blockchain network).
|
||||||
%% Every CallerID on the chain has a "next nonce" value that can be discovered by
|
%% Every CallerID on the chain has a "next nonce" value that can be discovered by
|
||||||
%% querying your Aeternity node (via `v_ejaa:next_nonce(CallerID, Node)', for
|
%% querying your Aeternity node (via `vanillae:next_nonce(CallerID)', for example).
|
||||||
%% example).
|
|
||||||
%% </li>
|
%% </li>
|
||||||
%% <li>
|
%% <li>
|
||||||
%% <b>Gas:</b>
|
%% <b>Gas:</b>
|
||||||
@@ -882,18 +1194,39 @@ contract_call(CallerID, Nonce, ACI, ConID, Fun, Args) ->
|
|||||||
%% if you do not already have a copy, and can check the spec of a function before
|
%% if you do not already have a copy, and can check the spec of a function before
|
||||||
%% trying to form a contract call.
|
%% trying to form a contract call.
|
||||||
|
|
||||||
contract_call(CallerID, Nonce, Gas, GasPrice, Fee, Amount, ACI, ConID, Fun, Args) ->
|
contract_call(CallerID, Nonce, Gas, GP, Fee, Amount, AACI, ConID, Fun, Args) ->
|
||||||
{ok, CallData} = encode_call_data(ACI, Fun, Args),
|
case encode_call_data(AACI, Fun, Args) of
|
||||||
|
{ok, CD} -> contract_call2(CallerID, Nonce, Gas, GP, Fee, Amount, ConID, CD);
|
||||||
|
Error -> Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
contract_call2(CallerID, Nonce, Gas, GasPrice, Fee, Amount, ConID, CallData) ->
|
||||||
|
CallerBin = unicode:characters_to_binary(CallerID),
|
||||||
|
try
|
||||||
|
{account_pubkey, PK} = aeser_api_encoder:decode(CallerBin),
|
||||||
|
contract_call3(PK, Nonce, Gas, GasPrice, Fee, Amount, ConID, CallData)
|
||||||
|
catch
|
||||||
|
Error:Reason -> {Error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
contract_call3(PK, Nonce, Gas, GasPrice, Fee, Amount, ConID, CallData) ->
|
||||||
|
ConBin = unicode:characters_to_binary(ConID),
|
||||||
|
try
|
||||||
|
{contract_pubkey, CK} = aeser_api_encoder:decode(ConBin),
|
||||||
|
contract_call4(PK, Nonce, Gas, GasPrice, Fee, Amount, CK, CallData)
|
||||||
|
catch
|
||||||
|
Error:Reason -> {Error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
contract_call4(PK, Nonce, Gas, GasPrice, Fee, Amount, CK, CallData) ->
|
||||||
ABI = 3,
|
ABI = 3,
|
||||||
TTL = 100,
|
TTL = 0,
|
||||||
CallVersion = 1,
|
CallVersion = 1,
|
||||||
Type = contract_call_tx,
|
Type = contract_call_tx,
|
||||||
{account_pubkey, PK} = aeser_api_encoder:decode(CallerID),
|
|
||||||
{contract_pubkey, CK} = aeser_api_encoder:decode(ConID),
|
|
||||||
Fields =
|
Fields =
|
||||||
[{caller_id, {id, account, PK}},
|
[{caller_id, aeser_id:create(account, PK)},
|
||||||
{nonce, Nonce},
|
{nonce, Nonce},
|
||||||
{contract_id, {id, contract, CK}},
|
{contract_id, aeser_id:create(contract, CK)},
|
||||||
{abi_version, ABI},
|
{abi_version, ABI},
|
||||||
{fee, Fee},
|
{fee, Fee},
|
||||||
{ttl, TTL},
|
{ttl, TTL},
|
||||||
@@ -913,7 +1246,11 @@ contract_call(CallerID, Nonce, Gas, GasPrice, Fee, Amount, ACI, ConID, Fun, Args
|
|||||||
{gas_price, int},
|
{gas_price, int},
|
||||||
{call_data, binary}],
|
{call_data, binary}],
|
||||||
TXB = aeser_chain_objects:serialize(Type, CallVersion, Template, Fields),
|
TXB = aeser_chain_objects:serialize(Type, CallVersion, Template, Fields),
|
||||||
aeser_api_encoder:encode(transaction, TXB).
|
try
|
||||||
|
{ok, aeser_api_encoder:encode(transaction, TXB)}
|
||||||
|
catch
|
||||||
|
error:Reason -> {error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
-spec prepare_contract(File) -> {ok, AACI} | {error, Reason}
|
-spec prepare_contract(File) -> {ok, AACI} | {error, Reason}
|
||||||
@@ -926,7 +1263,7 @@ contract_call(CallerID, Nonce, Gas, GasPrice, Fee, Amount, ACI, ConID, Fun, Args
|
|||||||
|
|
||||||
prepare_contract(File) ->
|
prepare_contract(File) ->
|
||||||
case aeso_compiler:file(File, [{aci, json}]) of
|
case aeso_compiler:file(File, [{aci, json}]) of
|
||||||
{ok, #{aci := ACI}} -> prepare_aaci(ACI);
|
{ok, #{aci := ACI}} -> {ok, prepare_aaci(ACI)};
|
||||||
Error -> Error
|
Error -> Error
|
||||||
end.
|
end.
|
||||||
|
|
||||||
@@ -960,21 +1297,43 @@ type(Name) -> binary_to_list(Name).
|
|||||||
%type(#{<<"map">> := {K, V}} -> {map, type(K), type(V)};
|
%type(#{<<"map">> := {K, V}} -> {map, type(K), type(V)};
|
||||||
%type(<<"string">>) -> string;
|
%type(<<"string">>) -> string;
|
||||||
|
|
||||||
coerce({integer, S}) ->
|
coerce({{ArgName, integer}, S}, {Good, Broken}) ->
|
||||||
list_to_integer(S);
|
try
|
||||||
coerce({address, S}) ->
|
N = list_to_integer(S),
|
||||||
{account_pubkey, Key} = aeser_api_encoder:decode(S),
|
{[N | Good], Broken}
|
||||||
{address, Key};
|
catch
|
||||||
coerce({contract, S}) ->
|
error:Reason -> {Good, [{ArgName, Reason} | Broken]}
|
||||||
aeser_api_encoder:decode(S);
|
end;
|
||||||
coerce({bool, S}) ->
|
coerce({{ArgName, address}, S}, {Good, Broken}) ->
|
||||||
S;
|
try
|
||||||
coerce({_, S}) ->
|
case aeser_api_encoder:decode(unicode:characters_to_binary(S)) of
|
||||||
S.
|
{account_pubkey, Key} -> {[{address, Key} | Good], Broken};
|
||||||
|
_ -> {Good, [{ArgName, bad_pubkey} | Broken]}
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
error:Reason -> {Good, [{ArgName, Reason} | Broken]}
|
||||||
|
end;
|
||||||
|
coerce({{ArgName, contract}, S}, {Good, Broken}) ->
|
||||||
|
try
|
||||||
|
case aeser_api_encoder:decode(unicode:characters_to_binary(S)) of
|
||||||
|
R = {contract_bytearray, _} -> {[R | Good], Broken};
|
||||||
|
_ -> {Good, [{ArgName, bad_contract} | Broken]}
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
error:Reason -> {Good, [{ArgName, Reason} | Broken]}
|
||||||
|
end;
|
||||||
|
coerce({{_, bool}, true}, {Good, Broken}) ->
|
||||||
|
{[true | Good], Broken};
|
||||||
|
coerce({{_, bool}, false}, {Good, Broken}) ->
|
||||||
|
{[false | Good], Broken};
|
||||||
|
coerce({{ArgName, bool}, _}, {Good, Broken}) ->
|
||||||
|
{Good, [{ArgName, not_bool} | Broken]};
|
||||||
|
coerce({_, S}, {Good, Broken}) ->
|
||||||
|
{[S | Good], Broken}.
|
||||||
|
|
||||||
|
|
||||||
-spec min_gas_price() -> integer().
|
-spec min_gas_price() -> integer().
|
||||||
%% @private
|
%% @doc
|
||||||
%% This function always returns 1,000,000,000 in the current version.
|
%% This function always returns 1,000,000,000 in the current version.
|
||||||
%%
|
%%
|
||||||
%% This is the minimum gas price returned by aec_tx_pool:minimum_miner_gas_price(),
|
%% This is the minimum gas price returned by aec_tx_pool:minimum_miner_gas_price(),
|
||||||
@@ -990,12 +1349,109 @@ min_gas_price() ->
|
|||||||
1000000000.
|
1000000000.
|
||||||
|
|
||||||
|
|
||||||
encode_call_data({aaci, _Name, FunDefs}, Fun, Args) ->
|
-spec min_gas() -> integer().
|
||||||
ArgDef = maps:get(Fun, FunDefs),
|
%% @doc
|
||||||
Binding = lists:zip([element(2, D) || D <- ArgDef], Args),
|
%% This function always returns 20,000 in the current version.
|
||||||
Coerced = lists:map(fun coerce/1, Binding),
|
%%
|
||||||
aeb_fate_abi:create_calldata(Fun, Coerced).
|
%% There is no actual minimum gas price, but this figure provides a lower limit toward
|
||||||
|
%% successful completion of general contract calls while not too severely limiting the
|
||||||
|
%% number of TXs that may appear in a single microblock based on the per-block gas
|
||||||
|
%% maximum (6,000,000 / 20,000 = 300 TXs in a microblock -- which at the moment seems
|
||||||
|
%% like plenty).
|
||||||
|
|
||||||
|
min_gas() ->
|
||||||
|
20000.
|
||||||
|
|
||||||
|
|
||||||
|
-spec min_fee() -> integer().
|
||||||
|
%% @doc
|
||||||
|
%% This function always returns 200,000,000,000,000 in the current version.
|
||||||
|
%%
|
||||||
|
%% This is the minimum fee amount currently accepted -- it is up to callers whether
|
||||||
|
%% they want to customize this value higher (or possibly lower, though as things stand
|
||||||
|
%% that would only work on an independent AE-based network, not the actual Aeternity
|
||||||
|
%% mainnet or testnet).
|
||||||
|
|
||||||
|
min_fee() ->
|
||||||
|
200000000000000.
|
||||||
|
|
||||||
|
|
||||||
|
encode_call_data({aaci, _, FunDefs}, Fun, Args) ->
|
||||||
|
case maps:find(Fun, FunDefs) of
|
||||||
|
{ok, ArgDef} -> encode_call_data2(ArgDef, Fun, Args);
|
||||||
|
error -> {error, bad_fun_name}
|
||||||
|
end.
|
||||||
|
|
||||||
|
encode_call_data2(ArgDef, Fun, Args) ->
|
||||||
|
DefLength = length(ArgDef),
|
||||||
|
ArgLength = length(Args),
|
||||||
|
if
|
||||||
|
DefLength =:= ArgLength -> encode_call_data3(ArgDef, Fun, Args);
|
||||||
|
DefLength > ArgLength -> {error, too_few_args};
|
||||||
|
DefLength < ArgLength -> {error, too_many_args}
|
||||||
|
end.
|
||||||
|
|
||||||
|
encode_call_data3(ArgDef, Fun, Args) ->
|
||||||
|
Binding = lists:zip(ArgDef, Args),
|
||||||
|
case lists:foldl(fun coerce/2, {[], []}, Binding) of
|
||||||
|
{Coerced, []} ->
|
||||||
|
Reversed = lists:reverse(Coerced),
|
||||||
|
aeb_fate_abi:create_calldata(Fun, Reversed);
|
||||||
|
{_, Errors} ->
|
||||||
|
{error, {args, lists:reverse(Errors)}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
verify_signature(Sig, Message, PubKey) ->
|
||||||
|
case aeser_api_encoder:decode(PubKey) of
|
||||||
|
{account_pubkey, PK} -> verify_signature2(Sig, Message, PK);
|
||||||
|
Other -> {error, {bad_key, Other}}
|
||||||
|
end.
|
||||||
|
|
||||||
|
verify_signature2(Sig, Message, PK) ->
|
||||||
|
% Superhero salts/hashes the message before signing it, in order to protect
|
||||||
|
% the user from accidentally signing a transaction disguised as a message.
|
||||||
|
% In order to verify the signature, we have to duplicate superhero's
|
||||||
|
% salt/hash procedure here.
|
||||||
|
%
|
||||||
|
% Salt the message then hash with blake2b. See:
|
||||||
|
% 1. Erlang Blake2 blake2b/2 function: https://github.com/aeternity/eblake2/blob/60a079f00d72d1bfcc25de8e6996d28f912db3fd/src/eblake2.erl#L23-L25
|
||||||
|
% 2. SDK salting step: https://github.com/aeternity/aepp-sdk-js/blob/370f1e30064ad0239ba59931908d9aba0a2e86b6/src/utils/crypto.ts#L171-L175
|
||||||
|
% 3. SDK hashing: https://github.com/aeternity/aepp-sdk-js/blob/370f1e30064ad0239ba59931908d9aba0a2e86b6/src/utils/crypto.ts#L83-L85
|
||||||
|
Prefix = <<"aeternity Signed Message:\n">>,
|
||||||
|
{ok, PSize} = vencode(byte_size(Prefix)),
|
||||||
|
{ok, MSize} = vencode(byte_size(Message)),
|
||||||
|
Smashed = iolist_to_binary([PSize, Prefix, MSize, Message]),
|
||||||
|
{ok, Hashed} = eblake2:blake2b(32, Smashed),
|
||||||
|
Signature = <<(binary_to_integer(Sig, 16)):(64 * 8)>>,
|
||||||
|
Result = enacl:sign_verify_detached(Signature, Hashed, PK),
|
||||||
|
{ok, Result}.
|
||||||
|
|
||||||
|
|
||||||
|
% This is Bitcoin's variable-length unsigned integer encoding
|
||||||
|
% See: https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
|
||||||
|
vencode(N) when N < 0 ->
|
||||||
|
{error, {negative_N, N}};
|
||||||
|
vencode(N) when N < 16#FD ->
|
||||||
|
{ok, <<N>>};
|
||||||
|
vencode(N) when N =< 16#FFFF ->
|
||||||
|
NBytes = eu(N, 2),
|
||||||
|
{ok, <<16#FD, NBytes/binary>>};
|
||||||
|
vencode(N) when N =< 16#FFFF_FFFF ->
|
||||||
|
NBytes = eu(N, 4),
|
||||||
|
{ok, <<16#FE, NBytes/binary>>};
|
||||||
|
vencode(N) when N < (2 bsl 64) ->
|
||||||
|
NBytes = eu(N, 8),
|
||||||
|
{ok, <<16#FF, NBytes/binary>>}.
|
||||||
|
|
||||||
|
|
||||||
|
% eu = encode unsigned (little endian with a given byte width)
|
||||||
|
% means add zero bytes to the end as needed
|
||||||
|
eu(N, Size) ->
|
||||||
|
Bytes = binary:encode_unsigned(N, little),
|
||||||
|
NExtraZeros = Size - byte_size(Bytes),
|
||||||
|
ExtraZeros = << <<0>> || _ <- lists:seq(1, NExtraZeros) >>,
|
||||||
|
<<Bytes/binary, ExtraZeros/binary>>.
|
||||||
|
|
||||||
|
|
||||||
%%% Debug functionality
|
%%% Debug functionality
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
-record(fetcher,
|
-record(fetcher,
|
||||||
{pid = none :: none | pid(),
|
{pid = none :: none | pid(),
|
||||||
mon = none :: none | reference(),
|
mon = none :: none | reference(),
|
||||||
time = none :: none | erlang:timestamp(),
|
time = none :: none | integer(), % nanosecond timestamp
|
||||||
node = none :: none | vanilae:ae_node(),
|
node = none :: none | vanilae:ae_node(),
|
||||||
from = none :: none | gen_server:from(),
|
from = none :: none | gen_server:from(),
|
||||||
req = none :: none | binary()}).
|
req = none :: none | binary()}).
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
%% @doc
|
|
||||||
%% Vanillae data humanization
|
|
||||||
%%
|
|
||||||
%% This is similar to serialization/deserialization, but not the same thing
|
|
||||||
%%
|
|
||||||
%% This code exists to work out concepts and code structure for Vanillae TS, it
|
|
||||||
%% may eventually become productized. Please do not use this.
|
|
||||||
%%
|
|
||||||
%% References:
|
|
||||||
%%
|
|
||||||
%% 1. https://github.com/aeternity/protocol/blob/master/serializations.md
|
|
||||||
%% 2. https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md
|
|
||||||
-module(vanth).
|
|
||||||
|
|
||||||
-compile([export_all, nowarn_export_all]).
|
|
||||||
|
|
||||||
|
|
||||||
%% semantic alias for "ak_" ++ string().
|
|
||||||
-type ak_str() :: string().
|
|
||||||
%% semantic alias for "tx_" ++ string().
|
|
||||||
-type tx_str() :: string().
|
|
||||||
|
|
||||||
%% TODO: expand
|
|
||||||
%% See: https://github.com/aeternity/protocol/blob/master/serializations.md#the-id-type
|
|
||||||
-type anth_id() :: ak_str().
|
|
||||||
|
|
||||||
%% See: https://github.com/aeternity/protocol/blob/master/serializations.md#spend-transaction
|
|
||||||
-type anth_spendtx() :: #{sender := anth_id(),
|
|
||||||
recipient := anth_id(),
|
|
||||||
amount := integer(),
|
|
||||||
fee := integer(),
|
|
||||||
ttl := integer(),
|
|
||||||
nonce := integer(),
|
|
||||||
payload := binary()}.
|
|
||||||
|
|
||||||
-spec humanize(API_String) -> HumanData
|
|
||||||
when API_String :: tx_str(),
|
|
||||||
HumanData :: {ok, anth_spendtx()}
|
|
||||||
| {error, Reason :: term()}.
|
|
||||||
%% @doc
|
|
||||||
%% Humanize some data
|
|
||||||
%% @end
|
|
||||||
|
|
||||||
humanize("tx_" ++ Base64) ->
|
|
||||||
hum_tx_b64(Base64);
|
|
||||||
humanize(X) ->
|
|
||||||
{error, {nyi, X}}.
|
|
||||||
|
|
||||||
|
|
||||||
%% decode the base64 and check the hash thing
|
|
||||||
hum_tx_b64(B64_str) ->
|
|
||||||
B64_Bytes = list_to_binary(B64_str),
|
|
||||||
%% This has the double sha at the end
|
|
||||||
Stupid_Bytes = base64:decode(B64_Bytes),
|
|
||||||
Stupid_Size = byte_size(Stupid_Bytes),
|
|
||||||
%% pull apart data
|
|
||||||
<<RLP_encoded_data : (Stupid_Size - 4) /binary,
|
|
||||||
Check : 4 /binary>> = Stupid_Bytes,
|
|
||||||
ActualDoubleSha = shasha(RLP_encoded_data),
|
|
||||||
case Check =:= ActualDoubleSha of
|
|
||||||
false ->
|
|
||||||
{error, checksum_mismatch};
|
|
||||||
true ->
|
|
||||||
decode_and_dispatch(RLP_encoded_data)
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Double sha
|
|
||||||
shasha(Bytes) ->
|
|
||||||
<<Result:4/binary, _/binary>> = crypto:hash(sha256, crypto:hash(sha256, Bytes)),
|
|
||||||
Result.
|
|
||||||
|
|
||||||
%% decode rlp data
|
|
||||||
decode_and_dispatch(RLP_encoded_bytes) ->
|
|
||||||
{DecodedData, Remainder} = vrlp:decode(RLP_encoded_bytes),
|
|
||||||
case Remainder of
|
|
||||||
<<>> -> hum_dispatch(DecodedData);
|
|
||||||
_ -> {error, trailing_data}
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% at this point we have the rlp data, and based on the first field, we are
|
|
||||||
%% going to humanize the data
|
|
||||||
|
|
||||||
hum_dispatch([Tag_Bytes, Vsn_Bytes | Fields]) ->
|
|
||||||
Tag = binary:decode_unsigned(Tag_Bytes),
|
|
||||||
Vsn = binary:decode_unsigned(Vsn_Bytes),
|
|
||||||
hd2(Tag, Vsn, Fields);
|
|
||||||
hum_dispatch(X) ->
|
|
||||||
{error, {invalid_data, X}}.
|
|
||||||
|
|
||||||
%% 12 = spendtx, version = 1
|
|
||||||
hd2(_spendtx = 12, 1, Fields) ->
|
|
||||||
hum_spendtx_fields(Fields);
|
|
||||||
hd2(Tag, Vsn, Fields) ->
|
|
||||||
{error, {nyi, {hd2, Tag, Vsn, Fields}}}.
|
|
||||||
|
|
||||||
%% See: https://github.com/aeternity/protocol/blob/master/serializations.md#spend-transaction
|
|
||||||
hum_spendtx_fields([SenderBytes,
|
|
||||||
RecipBytes,
|
|
||||||
AmountBytes,
|
|
||||||
FeeBytes,
|
|
||||||
TTLBytes,
|
|
||||||
NonceBytes,
|
|
||||||
Payload]) ->
|
|
||||||
% TODO: drop-through to make sure id humanization works
|
|
||||||
SenderStr = humanize_id(SenderBytes),
|
|
||||||
RecipStr = humanize_id(RecipBytes),
|
|
||||||
Amount = binary:decode_unsigned(AmountBytes),
|
|
||||||
Fee = binary:decode_unsigned(FeeBytes),
|
|
||||||
TTL = binary:decode_unsigned(TTLBytes),
|
|
||||||
Nonce = binary:decode_unsigned(NonceBytes),
|
|
||||||
{ok, #{sender => SenderStr,
|
|
||||||
recipient => RecipStr,
|
|
||||||
amount => Amount,
|
|
||||||
fee => Fee,
|
|
||||||
ttl => TTL,
|
|
||||||
nonce => Nonce,
|
|
||||||
payload => Payload}}.
|
|
||||||
|
|
||||||
humanize_id(<<1, IdBytes:32/binary>>) ->
|
|
||||||
Check = shasha(IdBytes),
|
|
||||||
Str = vb58:enc(<<IdBytes/binary, Check/binary>>),
|
|
||||||
"ak_" ++ Str.
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
%% @doc
|
|
||||||
%% Vanillae Base58 Encoding/Decoding module
|
|
||||||
%%
|
|
||||||
%% References
|
|
||||||
%%
|
|
||||||
%% 1. https://digitalbazaar.github.io/base58-spec/#encode
|
|
||||||
%% 2. https://www.youtube.com/watch?v=GedV3S9X89c
|
|
||||||
%% @end
|
|
||||||
-module(vb58).
|
|
||||||
|
|
||||||
-export([enc/1, dec/1]).
|
|
||||||
|
|
||||||
%% TODO: move these cases to test or something
|
|
||||||
%% this originated from the
|
|
||||||
%%-mode(compile).
|
|
||||||
%%-spec enc(binary()) -> string().
|
|
||||||
%%% https://digitalbazaar.github.io/base58-spec/#encode
|
|
||||||
%
|
|
||||||
%main([]) ->
|
|
||||||
% {ok, Cases} = file:consult("b58_cases_3.eterms"),
|
|
||||||
% test_cases(Cases).
|
|
||||||
%
|
|
||||||
%test_cases([{{encoded, E}, {decoded, D}} | Rest]) ->
|
|
||||||
% EncodeOk = E =:= enc(D),
|
|
||||||
% DecodeOk = D =:= dec(E),
|
|
||||||
% ok =
|
|
||||||
% case EncodeOk of
|
|
||||||
% true -> ok;
|
|
||||||
% false -> io:format("===============================~n"
|
|
||||||
% "YOU ARE A FAILURE TO ENCODE~n"
|
|
||||||
% "===============================~n"
|
|
||||||
% "decoded : ~tw~n"
|
|
||||||
% "expected : ~ts~n"
|
|
||||||
% "actual : ~ts~n~n",
|
|
||||||
% [D, E, enc(D)])
|
|
||||||
% end,
|
|
||||||
% ok =
|
|
||||||
% case DecodeOk of
|
|
||||||
% true -> ok;
|
|
||||||
% false -> io:format("===============================~n"
|
|
||||||
% "YOU ARE A FAILURE TO DECODE~n"
|
|
||||||
% "===============================~n"
|
|
||||||
% "encoded : ~ts~n"
|
|
||||||
% "expected : ~tw~n"
|
|
||||||
% "actual : ~tw~n~n",
|
|
||||||
% [E, D, dec(E)])
|
|
||||||
% end,
|
|
||||||
% test_cases(Rest);
|
|
||||||
%test_cases([]) ->
|
|
||||||
% ok.
|
|
||||||
|
|
||||||
% this was much clearer: https://www.youtube.com/watch?v=GedV3S9X89c
|
|
||||||
|
|
||||||
-spec enc(Bytes) -> Base58
|
|
||||||
when Bytes :: binary(),
|
|
||||||
Base58 :: string().
|
|
||||||
%% @doc
|
|
||||||
%% Encode a bytestring into base58 notation
|
|
||||||
|
|
||||||
enc(Bytes) ->
|
|
||||||
% grab leading 0s
|
|
||||||
{NumLeadingZeros, Rest} = split_zeros(Bytes, 0),
|
|
||||||
NBitsInRest = bit_size(Rest),
|
|
||||||
<<RestBigNum:NBitsInRest>> = Rest,
|
|
||||||
ZerosBase58 = [$1 || _ <- lists:seq(1, NumLeadingZeros)],
|
|
||||||
RestBase58 = enc(RestBigNum, []),
|
|
||||||
ZerosBase58 ++ RestBase58.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-spec split_zeros(Bytes, InitZeros) -> {NumLeadingZeros, Rest}
|
|
||||||
when Bytes :: binary(),
|
|
||||||
InitZeros :: integer(),
|
|
||||||
NumLeadingZeros :: binary(),
|
|
||||||
Rest :: binary().
|
|
||||||
|
|
||||||
split_zeros(<<0:8, Rest/binary>>, NumZerosAcc) ->
|
|
||||||
NewNumZerosAcc = NumZerosAcc + 1,
|
|
||||||
split_zeros(Rest, NewNumZerosAcc);
|
|
||||||
split_zeros(Rest, NumZerosAcc) ->
|
|
||||||
{NumZerosAcc, Rest}.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-spec enc(BytesBigNum, Base58Acc) -> Base58
|
|
||||||
when BytesBigNum :: integer(),
|
|
||||||
Base58Acc :: [0..57],
|
|
||||||
Base58 :: string().
|
|
||||||
|
|
||||||
enc(0, Acc) ->
|
|
||||||
lists:map(fun int2char/1, Acc);
|
|
||||||
enc(BitNum, Acc) ->
|
|
||||||
Q = BitNum div 58,
|
|
||||||
R = BitNum rem 58,
|
|
||||||
enc(Q, [R | Acc]).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-spec dec(Base58) -> DecodedBytes
|
|
||||||
when Base58 :: string(),
|
|
||||||
DecodedBytes :: binary().
|
|
||||||
%% @doc
|
|
||||||
%% Decode a Base58-encoded string into a bytestring
|
|
||||||
|
|
||||||
dec(Str) ->
|
|
||||||
% the number of leading 1s tells us the number of leading zeros
|
|
||||||
{NumLeadingZeros, RestStr} = split_ones(Str, 0),
|
|
||||||
LeadingZeros = << <<0>> || _ <- lists:seq(1, NumLeadingZeros) >>,
|
|
||||||
RestNs = lists:map(fun char2int/1, RestStr),
|
|
||||||
RestBytes = dec(RestNs, 0),
|
|
||||||
<<LeadingZeros/binary, RestBytes/binary>>.
|
|
||||||
|
|
||||||
split_ones([$1 | Rest], NOnes) ->
|
|
||||||
split_ones(Rest, NOnes + 1);
|
|
||||||
split_ones(B58Str, NOnes) ->
|
|
||||||
{NOnes, B58Str}.
|
|
||||||
|
|
||||||
|
|
||||||
dec([N | Ns], Acc) ->
|
|
||||||
NewAcc = (Acc*58) + N,
|
|
||||||
dec(Ns, NewAcc);
|
|
||||||
dec([], FinalAccN) ->
|
|
||||||
bignum_to_binary_bige(FinalAccN, <<>>).
|
|
||||||
|
|
||||||
bignum_to_binary_bige(0, Acc) ->
|
|
||||||
Acc;
|
|
||||||
bignum_to_binary_bige(N, Acc) ->
|
|
||||||
Q = N div 256,
|
|
||||||
R = N rem 256,
|
|
||||||
NewAcc = <<R, Acc/binary>>,
|
|
||||||
bignum_to_binary_bige(Q, NewAcc).
|
|
||||||
|
|
||||||
int2char( 0) -> $1;
|
|
||||||
int2char( 1) -> $2;
|
|
||||||
int2char( 2) -> $3;
|
|
||||||
int2char( 3) -> $4;
|
|
||||||
int2char( 4) -> $5;
|
|
||||||
int2char( 5) -> $6;
|
|
||||||
int2char( 6) -> $7;
|
|
||||||
int2char( 7) -> $8;
|
|
||||||
int2char( 8) -> $9;
|
|
||||||
int2char( 9) -> $A;
|
|
||||||
int2char(10) -> $B;
|
|
||||||
int2char(11) -> $C;
|
|
||||||
int2char(12) -> $D;
|
|
||||||
int2char(13) -> $E;
|
|
||||||
int2char(14) -> $F;
|
|
||||||
int2char(15) -> $G;
|
|
||||||
int2char(16) -> $H;
|
|
||||||
int2char(17) -> $J;
|
|
||||||
int2char(18) -> $K;
|
|
||||||
int2char(19) -> $L;
|
|
||||||
int2char(20) -> $M;
|
|
||||||
int2char(21) -> $N;
|
|
||||||
int2char(22) -> $P;
|
|
||||||
int2char(23) -> $Q;
|
|
||||||
int2char(24) -> $R;
|
|
||||||
int2char(25) -> $S;
|
|
||||||
int2char(26) -> $T;
|
|
||||||
int2char(27) -> $U;
|
|
||||||
int2char(28) -> $V;
|
|
||||||
int2char(29) -> $W;
|
|
||||||
int2char(30) -> $X;
|
|
||||||
int2char(31) -> $Y;
|
|
||||||
int2char(32) -> $Z;
|
|
||||||
int2char(33) -> $a;
|
|
||||||
int2char(34) -> $b;
|
|
||||||
int2char(35) -> $c;
|
|
||||||
int2char(36) -> $d;
|
|
||||||
int2char(37) -> $e;
|
|
||||||
int2char(38) -> $f;
|
|
||||||
int2char(39) -> $g;
|
|
||||||
int2char(40) -> $h;
|
|
||||||
int2char(41) -> $i;
|
|
||||||
int2char(42) -> $j;
|
|
||||||
int2char(43) -> $k;
|
|
||||||
int2char(44) -> $m;
|
|
||||||
int2char(45) -> $n;
|
|
||||||
int2char(46) -> $o;
|
|
||||||
int2char(47) -> $p;
|
|
||||||
int2char(48) -> $q;
|
|
||||||
int2char(49) -> $r;
|
|
||||||
int2char(50) -> $s;
|
|
||||||
int2char(51) -> $t;
|
|
||||||
int2char(52) -> $u;
|
|
||||||
int2char(53) -> $v;
|
|
||||||
int2char(54) -> $w;
|
|
||||||
int2char(55) -> $x;
|
|
||||||
int2char(56) -> $y;
|
|
||||||
int2char(57) -> $z.
|
|
||||||
|
|
||||||
char2int($1) -> 0;
|
|
||||||
char2int($2) -> 1;
|
|
||||||
char2int($3) -> 2;
|
|
||||||
char2int($4) -> 3;
|
|
||||||
char2int($5) -> 4;
|
|
||||||
char2int($6) -> 5;
|
|
||||||
char2int($7) -> 6;
|
|
||||||
char2int($8) -> 7;
|
|
||||||
char2int($9) -> 8;
|
|
||||||
char2int($A) -> 9;
|
|
||||||
char2int($B) -> 10;
|
|
||||||
char2int($C) -> 11;
|
|
||||||
char2int($D) -> 12;
|
|
||||||
char2int($E) -> 13;
|
|
||||||
char2int($F) -> 14;
|
|
||||||
char2int($G) -> 15;
|
|
||||||
char2int($H) -> 16;
|
|
||||||
char2int($J) -> 17;
|
|
||||||
char2int($K) -> 18;
|
|
||||||
char2int($L) -> 19;
|
|
||||||
char2int($M) -> 20;
|
|
||||||
char2int($N) -> 21;
|
|
||||||
char2int($P) -> 22;
|
|
||||||
char2int($Q) -> 23;
|
|
||||||
char2int($R) -> 24;
|
|
||||||
char2int($S) -> 25;
|
|
||||||
char2int($T) -> 26;
|
|
||||||
char2int($U) -> 27;
|
|
||||||
char2int($V) -> 28;
|
|
||||||
char2int($W) -> 29;
|
|
||||||
char2int($X) -> 30;
|
|
||||||
char2int($Y) -> 31;
|
|
||||||
char2int($Z) -> 32;
|
|
||||||
char2int($a) -> 33;
|
|
||||||
char2int($b) -> 34;
|
|
||||||
char2int($c) -> 35;
|
|
||||||
char2int($d) -> 36;
|
|
||||||
char2int($e) -> 37;
|
|
||||||
char2int($f) -> 38;
|
|
||||||
char2int($g) -> 39;
|
|
||||||
char2int($h) -> 40;
|
|
||||||
char2int($i) -> 41;
|
|
||||||
char2int($j) -> 42;
|
|
||||||
char2int($k) -> 43;
|
|
||||||
char2int($m) -> 44;
|
|
||||||
char2int($n) -> 45;
|
|
||||||
char2int($o) -> 46;
|
|
||||||
char2int($p) -> 47;
|
|
||||||
char2int($q) -> 48;
|
|
||||||
char2int($r) -> 49;
|
|
||||||
char2int($s) -> 50;
|
|
||||||
char2int($t) -> 51;
|
|
||||||
char2int($u) -> 52;
|
|
||||||
char2int($v) -> 53;
|
|
||||||
char2int($w) -> 54;
|
|
||||||
char2int($x) -> 55;
|
|
||||||
char2int($y) -> 56;
|
|
||||||
char2int($z) -> 57.
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
%% @doc
|
|
||||||
%% Vanillae RLP encoder/decoder
|
|
||||||
%%
|
|
||||||
%% Reference: https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/
|
|
||||||
%%
|
|
||||||
%% Agrees with Ethereum's Python implementation in randomized tests
|
|
||||||
-module(vrlp).
|
|
||||||
|
|
||||||
-export_type([decoded_data/0]).
|
|
||||||
-export([encode/1, decode/1]).
|
|
||||||
|
|
||||||
-type decoded_data() :: binary() | [decoded_data()].
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-spec encode(Data) -> RLP
|
|
||||||
when Data :: decoded_data(),
|
|
||||||
RLP :: binary().
|
|
||||||
%% @doc
|
|
||||||
%% encode some data
|
|
||||||
|
|
||||||
encode(Binary) when is_binary(Binary) ->
|
|
||||||
encode_binary(Binary);
|
|
||||||
encode(List) when is_list(List) ->
|
|
||||||
encode_list(List).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-spec encode_binary(Bytes) -> RLP
|
|
||||||
when Bytes :: binary(),
|
|
||||||
RLP :: binary().
|
|
||||||
%% @private
|
|
||||||
%% encode a binary in rlp
|
|
||||||
%% @end
|
|
||||||
|
|
||||||
% single byte case when the byte is between 0..127
|
|
||||||
% result is the byte itself
|
|
||||||
encode_binary(<<Byte>>) when Byte =< 127 ->
|
|
||||||
<<Byte>>;
|
|
||||||
% if the bytestring is 0..55 items long, the first byte is 128 + Length,
|
|
||||||
% the rest of the string is the string
|
|
||||||
encode_binary(Bytes) when byte_size(Bytes) =< 55 ->
|
|
||||||
Size = byte_size(Bytes),
|
|
||||||
<<(128 + Size), Bytes/binary>>;
|
|
||||||
% more than 55 bytes long, first byte is 183 + ByteLengthOfLength
|
|
||||||
% max byte size is 2^64 - 1
|
|
||||||
encode_binary(Bytes) when 55 < byte_size(Bytes), byte_size(Bytes) < (1 bsl 64) ->
|
|
||||||
SizeInt = byte_size(Bytes),
|
|
||||||
SizeBytes = binary:encode_unsigned(SizeInt, big),
|
|
||||||
SizeOfSizeInt = byte_size(SizeBytes),
|
|
||||||
%% 183 = 128 + 55
|
|
||||||
%% SizeOfSizeInt > 0
|
|
||||||
<<(183 + SizeOfSizeInt),
|
|
||||||
SizeBytes/binary,
|
|
||||||
Bytes/binary>>.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-spec encode_list(List) -> RLP
|
|
||||||
when List :: [decoded_data()],
|
|
||||||
RLP :: binary().
|
|
||||||
%% @private
|
|
||||||
%% encode a list in rlp
|
|
||||||
%% @end
|
|
||||||
|
|
||||||
% first we encode the total payload of the list
|
|
||||||
% depending on how long it is, we then branch
|
|
||||||
encode_list(List) ->
|
|
||||||
Payload = << (encode(Item)) || Item <- List>>,
|
|
||||||
Payload_Size = byte_size(Payload),
|
|
||||||
if
|
|
||||||
Payload_Size =< 55 ->
|
|
||||||
<<(192 + Payload_Size), Payload/binary>>;
|
|
||||||
55 < Payload_Size ->
|
|
||||||
SizeBytes = binary:encode_unsigned(Payload_Size, big),
|
|
||||||
SizeOfSizeInt = byte_size(SizeBytes),
|
|
||||||
%% 247 = 192 + 55
|
|
||||||
%% SizeOfSizeInt > 0
|
|
||||||
<<(247 + SizeOfSizeInt),
|
|
||||||
SizeBytes/binary,
|
|
||||||
Payload/binary>>
|
|
||||||
end.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-spec decode(RLP) -> {Data, Rest}
|
|
||||||
when RLP :: binary(),
|
|
||||||
Data :: decoded_data(),
|
|
||||||
Rest :: binary().
|
|
||||||
%% @doc
|
|
||||||
%% decode an RLP-encoded string
|
|
||||||
%% @end
|
|
||||||
|
|
||||||
% if the first byte is between 0 and 127, that is the data
|
|
||||||
decode(<<Byte, Rest/binary>>) when Byte =< 127 ->
|
|
||||||
{<<Byte>>, Rest};
|
|
||||||
% if the first byte is between 128 and 183 = 128 + 55, it is a bytestring and
|
|
||||||
% the length is Byte - 128
|
|
||||||
decode(<<Byte, Rest/binary>>) when Byte =< 183 ->
|
|
||||||
PayloadByteLength = Byte - 128,
|
|
||||||
%PayloadBitLength = 8 * PayloadByteLength,
|
|
||||||
%io:format("Byte : ~p~n"
|
|
||||||
% "Rest : ~w~n"
|
|
||||||
% "PayloadByteLength : ~p~n",
|
|
||||||
% %"PayloadBitLength : ~p~n",
|
|
||||||
% [Byte, Rest, PayloadByteLength]),
|
|
||||||
<<Payload:PayloadByteLength/binary,
|
|
||||||
Rest2/binary>> = Rest,
|
|
||||||
{Payload, Rest2};
|
|
||||||
% If the first byte is between 184 = 183 + 1 and 191 = 183 + 8, it is a
|
|
||||||
% bytestring. The byte length of the byte length of bytestring is FirstByte -
|
|
||||||
% 183. Then pull out the actual data
|
|
||||||
decode(<<Byte, Rest/binary>>) when Byte =< 191 ->
|
|
||||||
ByteLengthOfByteLength = Byte - 183,
|
|
||||||
BitLengthOfByteLength = 8 * ByteLengthOfByteLength,
|
|
||||||
<<ByteLengthInt:BitLengthOfByteLength,
|
|
||||||
Rest2/binary>> = Rest,
|
|
||||||
<<Payload:ByteLengthInt/binary,
|
|
||||||
Rest3/binary>> = Rest2,
|
|
||||||
{Payload, Rest3};
|
|
||||||
% If the first byte is between 192 and 247 = 192 + 55, it is a list. The byte
|
|
||||||
% length of the list-payload is FirstByte - 192. Then the list payload, which
|
|
||||||
% needs to be decoded on its own.
|
|
||||||
decode(<<Byte, Rest/binary>>) when Byte =< 247 ->
|
|
||||||
ByteLengthOfListPayload = Byte - 192,
|
|
||||||
<<ListPayload:ByteLengthOfListPayload/binary,
|
|
||||||
Rest2/binary>> = Rest,
|
|
||||||
List = decode_list(ListPayload),
|
|
||||||
{List, Rest2};
|
|
||||||
% If the first byte is between 248 = 247 + 1 and 255 = 247 + 8, it is a list.
|
|
||||||
% The byte length of the byte length of the list-payload is FirstByte - 247.
|
|
||||||
% Then the byte length of the list. Then the list payload, which needs to be
|
|
||||||
% decoded on its own.
|
|
||||||
decode(<<Byte, Rest/binary>>) ->
|
|
||||||
ByteLengthOfByteLengthOfListPayload_int = Byte - 247,
|
|
||||||
BitLengthOfByteLengthOfListPayload_int = 8 * ByteLengthOfByteLengthOfListPayload_int,
|
|
||||||
<<ByteLengthOfListPayload_int:BitLengthOfByteLengthOfListPayload_int,
|
|
||||||
Rest2/binary>> = Rest,
|
|
||||||
<<ListPayload_bytes:ByteLengthOfListPayload_int/binary,
|
|
||||||
Rest3/binary>> = Rest2,
|
|
||||||
List = decode_list(ListPayload_bytes),
|
|
||||||
{List, Rest3}.
|
|
||||||
|
|
||||||
decode_list(<<>>) ->
|
|
||||||
[];
|
|
||||||
decode_list(Bytes) ->
|
|
||||||
{Item, Rest} = decode(Bytes),
|
|
||||||
[Item | decode_list(Rest)].
|
|
||||||
@@ -76,3 +76,170 @@ b64_dec([W, X, Y, Z | Rest], Acc) ->
|
|||||||
NewAcc = <<Acc/binary, NW:6, NX:6, NY:6, NZ:6>>,
|
NewAcc = <<Acc/binary, NW:6, NX:6, NY:6, NZ:6>>,
|
||||||
b64_dec(Rest, NewAcc).
|
b64_dec(Rest, NewAcc).
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This document explains the Base58 and Base64 notations, and the algorithms
|
||||||
|
for working with them. I wrote this document because I had a fair bit of
|
||||||
|
difficulty working this out for myself, even with a strong math background.
|
||||||
|
I couldn't find any resource on the internet explaining all of this simply.
|
||||||
|
|
||||||
|
Code examples are given in Erlang and TypeScript. These are the two most
|
||||||
|
common languages used within the Aeternity project, and both happen to be
|
||||||
|
languges that make these tasks easy. This document assumes you are familiar
|
||||||
|
with either/both languages. Even if that's not true, Erlang is a very simple
|
||||||
|
and clean language, so the code should be pretty self-explanatory if you read
|
||||||
|
it.
|
||||||
|
|
||||||
|
Base64 is kind of annoying but it's pretty straightforward to code. My
|
||||||
|
initial assumption was that Base58 was in some way "the same" algorithm but
|
||||||
|
with `n = 58` instead of `n = 64`. When I went to look up the spec, I found
|
||||||
|
this ([source](https://digitalbazaar.github.io/base58-spec/))
|
||||||
|
|
||||||
|
> ### 3. The Base58 Encoding Algorithm
|
||||||
|
>
|
||||||
|
> To encode an array of bytes to a Base58 encoded value, run the following
|
||||||
|
> algorithm. All mathematical operations MUST be performed using integer
|
||||||
|
> arithmetic. Start by initializing a `zero_counter` to zero (`0x0`), an
|
||||||
|
> `encoding_flag` to zero (`0x0`), a `b58_bytes` array, a `b58_encoding`
|
||||||
|
> array, and a `carry` value to zero (`0x0`). For each byte in the array of
|
||||||
|
> bytes and while `carry` does not equal zero (`0x0`) after the first
|
||||||
|
> iteration:
|
||||||
|
>
|
||||||
|
> 1. If `encoding_flag` is not set, and if the byte is a zero (`0x0`),
|
||||||
|
> increment the value of `zero_counter`. If the value is not zero `(0x0)`,
|
||||||
|
> set `encoding_flag` to true `(0x1)`.
|
||||||
|
> 2. If `encoding_flag` is set, multiply the current byte value by 256 and add
|
||||||
|
> it to `carry`.
|
||||||
|
> 3. Set the corresponding byte value in `b58_bytes` to the value of `carry`
|
||||||
|
> modulus 58.
|
||||||
|
> 4. Set `carry` to the value of `carry` divided by 58.
|
||||||
|
>
|
||||||
|
> Once the `b58_bytes` array has been constructed, generate the final
|
||||||
|
> `b58_encoding` using the following algorithm. Set the first `zero_counter`
|
||||||
|
> bytes in `b58_encoding` to `1`. Then, for every byte in `b58_array`, map the
|
||||||
|
> byte value using the Base58 alphabet in the previous section to its
|
||||||
|
> corresponding character in `b58_encoding`. Return `b58_encoding` as the
|
||||||
|
> Base58 representation of the input array of bytes.
|
||||||
|
|
||||||
|
I personally have no idea what that does. I found a YouTube video that
|
||||||
|
explained the Base58 algorithm in a way that made a lot more sense.
|
||||||
|
([source](https://youtu.be/GedV3S9X89c)). The video gave a clear enough
|
||||||
|
explanation of the Base58 algorithm that I could _figure out_ what is going on
|
||||||
|
and why it makes sense. I was able to relate what I was seeing in the video to
|
||||||
|
background context I happen to have from mathematics. But the video didn't
|
||||||
|
provide that context.
|
||||||
|
|
||||||
|
I want this document to explain what both algorithms do, why they make sense,
|
||||||
|
how they are different, and why they _have_ to be different. All with code
|
||||||
|
examples and sufficient mathematical context.
|
||||||
|
|
||||||
|
Let's get started.
|
||||||
|
|
||||||
|
Any data stored in a computer is represented as an integer. For the purposes of
|
||||||
|
this discussion, we're going to assume all integers are non-negative (greater
|
||||||
|
than or equal to 0). The discussion below can easily be modified to accomodate
|
||||||
|
negative integers. This would add a small amount of annoying complexity in
|
||||||
|
exchange for no gain in conceptual clarity. Nothing we are doing requires
|
||||||
|
dealing with negative numbers.
|
||||||
|
|
||||||
|
The problem we are interested in is _how do we represent really big integers in
|
||||||
|
plain text?_.
|
||||||
|
|
||||||
|
The first point I want you to take away is that **these are two totally
|
||||||
|
different solutions**. Do not be fooled by the name. It is **NOT** the
|
||||||
|
case that these are two instances of the same "Base N"
|
||||||
|
algorithm, just one is `N = 64` and one is `N = 58`. **These are two totally
|
||||||
|
different approaches to solving the same problem.**
|
||||||
|
|
||||||
|
More precisely, the underlying mathematics behind the two notations is very
|
||||||
|
similar, but the algorithms for producing them are very different. More detail
|
||||||
|
later.
|
||||||
|
|
||||||
|
Like I said, any given piece of data is---from the perspective of your
|
||||||
|
computer---just a very big integer. The difference between the two algorithms
|
||||||
|
is, roughly:
|
||||||
|
|
||||||
|
1. The Base64 algorithm thinks of that integer as a "stream of digits"
|
||||||
|
2. The Base58 algorithm thinks of that integer as a "pure integer," kind of
|
||||||
|
the way math thinks of an integer: the integer _itself_ is a different
|
||||||
|
thing than the way the integer is _represented_.
|
||||||
|
|
||||||
|
Base64 encoding/decoding involves a straightforward translation back and forth
|
||||||
|
from the machine representation of integers, without thinking too much (or at
|
||||||
|
all) about the math involved.
|
||||||
|
|
||||||
|
Base58 encoding/decoding requires thinking about the integer from a more mathy
|
||||||
|
point of view. That weird arcane algorithm above is what happens when you try
|
||||||
|
to phrase the mathematics in terms of the machine representation of
|
||||||
|
really big integers.
|
||||||
|
|
||||||
|
We're going to focus on the mathy point of view and then circle back to the
|
||||||
|
weird arcane algorithm later on.
|
||||||
|
|
||||||
|
There is actually a good reason we don't use decimal notation for really big
|
||||||
|
integers: it's extremely wasteful.
|
||||||
|
|
||||||
|
I'll explain the following in more detail in a later section. Roll with me. To
|
||||||
|
any piece of data there is associated a quantity called **information**. The
|
||||||
|
_unit_ of information is the _bit_, in the same sense that the unit of length
|
||||||
|
is the meter.
|
||||||
|
|
||||||
|
1. There are 256 distinct bytes. A single byte (machine digit)
|
||||||
|
contains exactly 8 bits $8 = \log_2 256$ of information.
|
||||||
|
|
||||||
|
2. There are 10 distinct decimal ("Base10") symbols. A single decimal digit
|
||||||
|
contains approximately 3.32 bits (`3.32 ~ log2(10)`) of information.
|
||||||
|
|
||||||
|
3. There are 64 distinct Base64 symbols. A single Base64 digit contains
|
||||||
|
exactly $6$ bits (`6 = log2(64)`) of information.
|
||||||
|
|
||||||
|
4. There are 58 distinct Base58 symbols. A single Base58 digit contains
|
||||||
|
approximately 5.86 bits (`5.86 ~ log2(58)`) of information.
|
||||||
|
|
||||||
|
What this means is, in base64 notation, each symbol consumes 6 bits of
|
||||||
|
information, roughly twice the rate of decimal notation (~3.32 bits per
|
||||||
|
symbol). What this means in practice is that a number written in Base64
|
||||||
|
notation is about half as long as a number written in decimal notation.
|
||||||
|
|
||||||
|
For instance, the number `K = 90 682 877 680 429`
|
||||||
|
|
||||||
|
1. requires 14 digits (count them!) in decimal notation
|
||||||
|
|
||||||
|
$$
|
||||||
|
\frac{(\log_2 K) \text{ bits}}
|
||||||
|
{(\log_2 10) \text{ bits per symbol}}
|
||||||
|
\approx
|
||||||
|
\frac{46.37 \text{ bits}}
|
||||||
|
{ 3.37 \text{ bits per symbol}}
|
||||||
|
\approx 13.96 \text{ symbols}
|
||||||
|
$$
|
||||||
|
|
||||||
|
2. requires 8 digits in Base64 notation (`UnnAthst`)
|
||||||
|
|
||||||
|
$$
|
||||||
|
\frac{(\log_2 K) \text{ bits}}
|
||||||
|
{(\log_2 64) \text{ bits per symbol}}
|
||||||
|
\approx
|
||||||
|
\frac{46.37 \text{ bits}}
|
||||||
|
{ 6 \text{ bits per symbol}}
|
||||||
|
\approx 7.73 \text{ symbols}
|
||||||
|
$$
|
||||||
|
|
||||||
|
3. requires 8 digits in Base58 notation (`i55xNZNt`)
|
||||||
|
|
||||||
|
$$
|
||||||
|
\frac{(\log_2 K) \text{ bits}}
|
||||||
|
{(\log_2 58) \text{ bits per symbol}}
|
||||||
|
\approx
|
||||||
|
\frac{46.37 \text{ bits}}
|
||||||
|
{ 5.86 \text{ bits per symbol}}
|
||||||
|
\approx 7.91 \text{ symbols}
|
||||||
|
$$
|
||||||
|
|
||||||
|
As you can see, the difference in space complexity between Base58 and Base64 is
|
||||||
|
pretty small, but the difference between Base10 is pretty large. Base58 has
|
||||||
|
the same alphabet (set of symbols) as Base64, minus a handful that can cause
|
||||||
|
readability or manual input issues. For instance, the Base64 alphabet contains
|
||||||
|
both the symbol `0` (numeral zero) and `O` (uppercase letter `o`). The Base58
|
||||||
|
alphabet contains neither.
|
||||||
|
|||||||
Reference in New Issue
Block a user