Merge branch 'master' into pharpend/develop

This commit is contained in:
Foo Bar
2022-11-30 22:55:49 -07:00
4 changed files with 708 additions and 122 deletions
-23
View File
@@ -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
+539 -97
View File
@@ -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,95 @@ 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) ->
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}.
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(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
+1 -1
View File
@@ -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()}).
+167
View File
@@ -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.