%%% @doc %%% The Hakuzaru Erlang Interface to Gajumaru %%% %%% This module is the high-level interface to the Gajumaru blockchain system. %%% The interface is split into three main sections: %%% - Get/Set admin functions %%% - Node JSON query interface functions %%% - Contract call and serialization interface functions %%% %%% The get/set admin functions are for setting or checking things like the Gajumaru %%% "network ID" and list of addresses of nodes you want to use for answering %%% queries to the blockchain. Get functions are arity 0, and set functions are arity 1. %%% %%% The JSON query interface functions are the blockchain query functions themselves %%% which are translated to network queries and return Erlang messages as responses. %%% %%% The contract call and serialization interface are the functions used to convert %%% a desired call to a smart contract on the chain to call data serialized in a form %%% that a Gajumaru compatible wallet or library can sign and submit to a Gajumaru node. %%% %%% NOTE: %%% This module does not implement the OTP application behavior. Refer to hakuzaru.erl. %%% @end -module(hz). -vsn("0.8.2"). -author("Craig Everett "). -copyright("Craig Everett "). -license("GPL-3.0-or-later"). % Get/Set admin functions. -export([chain_nodes/0, chain_nodes/1, tls/0, tls/1, timeout/0, timeout/1]). % Node JSON query interface functions -export([network_id/0, top_height/0, top_block/0, kb_current/0, kb_current_hash/0, kb_current_height/0, kb_pending/0, kb_by_hash/1, kb_by_height/1, % kb_insert/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, acc/1, acc_at_height/2, acc_at_block_id/2, acc_pending_txs/1, next_nonce/1, dry_run/1, dry_run/2, dry_run/3, dry_run_map/1, tx/1, tx_info/1, post_tx/1, contract/1, contract_code/1, contract_source/1, contract_poi/1, name/1, % channel/1, peer_pubkey/0, status/0, status_chainends/0]). % Contract call and serialization interface functions -export([read_aci/1, min_gas/0, min_gas_price/0, contract_create/3, contract_create_built/3, contract_create_built/8, contract_create/8, prepare_contract/1, cache_aaci/2, lookup_aaci/1, aaci_lookup_spec/2, contract_call/5, contract_call/6, contract_call/10, decode_bytearray_fate/1, decode_bytearray/2, spend/5, spend/10, sign_tx/2, sign_tx/3, sign_message/2, verify_signature/3, sign_binary/2, verify_bin_signature/3]). %%% Types -export_type([chain_node/0, network_id/0, chain_error/0, aaci/0]). -include_lib("eunit/include/eunit.hrl"). -type chain_node() :: {inet:ip_address(), inet:port_number()}. -type network_id() :: string(). -type chain_error() :: not_started | no_nodes | timeout | {timeout, Received :: binary()} | inet:posix() | {received, binary()} | headers | {headers, map()} | bad_length | gc_out_of_range. -type aaci() :: {aaci, term(), term(), term()}. -type pubkey() :: unicode:chardata(). % "ak_" ++ _ -type account_id() :: pubkey(). -type contract_id() :: unicode:chardata(). % "ct_" ++ _ -type peer_pubkey() :: string(). % "pp_" ++ _ -type keyblock_hash() :: string(). % "kh_" ++ _ -type contract_byte_array() :: string(). % "cb_" ++ _ -type microblock_hash() :: string(). % "mh_" ++ _ %-type block_state_hash() :: string(). % "bs_" ++ _ %-type proof_of_fraud_hash() :: string() | no_fraud. % "bf_" ++ _ %-type signature() :: string(). % "sg_" ++ _ %-type block_tx_hash() :: string(). % "bx_" ++ _ -type tx_hash() :: string(). % "th_" ++ _ %-type name_hash() :: string(). % "nm_" ++ _ %-type protocol_info() :: #{string() => term()}. % #{"effective_at_height" => non_neg_integer(), % "version" => pos_integer()}. -type keyblock() :: #{string() => term()}. %
% #{"beneficiary"   => account_id(),
%   "hash"          => keyblock_hash(),
%   "height"        => pos_integer(),
%   "info"          => contract_byte_array(),
%   "miner"         => account_id(),
%   "nonce"         => non_neg_integer(),
%   "pow"           => [non_neg_integer()],
%   "prev_hash"     => microblock_hash(),
%   "prev_key_hash" => keyblock_hash(),
%   "state_hash"    => block_state_hash(),
%   "target"        => non_neg_integer(),
%   "time"          => non_neg_integer(),
%   "version"       => 5}.
% 
-type microblock_header() :: #{string() => term()}. %
% #{"hash"          => microblock_hash(),
%   "height"        => pos_integer(),
%   "pof_hash"      => proof_of_fraud_hash(),
%   "prev_hash"     => microblock_hash() | keyblock_hash(),
%   "prev_key_hash" => keyblock_hash(),
%   "signature"     => signature(),
%   "state_hash"    => block_state_hash(),
%   "time"          => non_neg_integer(),
%   "txs_hash"      => block_tx_hash(),
%   "version"       => 1}.
% 
-type transaction() :: #{string() => term()}. %
% #{"block_hash"    => microblock_hash(),
%   "block_height"  => pos_integer(),
%   "hash"          => tx_hash(),
%   "signatures"    => [signature()],
%   "tx"            =>
%       #{"abi_version" => pos_integer(),
%         "amount"      => non_neg_integer(),
%         "call_data"   => contract_byte_array(),
%         "code"        => contract_byte_array(),
%         "deposit"     => non_neg_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()}. %
% #{"key_block"     => keyblock(),
%   "micro_blocks"  => [microblock_hash()]}.
% 
-type account() :: #{string() => term()}. %
% #{"balance" => non_neg_integer(),
%   "id"      => account_id(),
%   "kind"    => "basic",
%   "nonce"   => pos_integer(),
%   "payable" => true}.
% 
-type contract_data() :: #{string() => term()}. %
% #{"abi_version " => pos_integer(),
%   "active"       => boolean(),
%   "deposit"      => non_neg_integer(),
%   "id"           => contract_id(),
%   "owner_id"     => account_id() | contract_id(),
%   "referrer_ids" => [],
%   "vm_version"   => pos_integer()}.
% 
-type name_info() :: #{string() => term()}. %
% #{"id"       => name_hash(),
%   "owner"    => account_id(),
%   "pointers" => [],
%   "ttl"      => non_neg_integer()}.
% 
-type status() :: #{string() => term()}. %
% #{"difficulty"                 => non_neg_integer(),
%   "genesis_key_block_hash"     => keyblock_hash(),
%   "listening"                  => boolean(),
%   "network_id"                 => string(),
%   "node_revision"              => string(),
%   "node_version"               => string(),
%   "peer_connections"           => #{"inbound"  => non_neg_integer(),
%                                     "outbound" => non_neg_integer()},
%   "peer_count"                 => non_neg_integer(),
%   "peer_pubkey"                => peer_pubkey(),
%   "pending_transactions_count" => 51,
%   "protocols"                  => [protocol_info()],
%   "solutions"                  => non_neg_integer(),
%   "sync_progress"              => float(),
%   "syncing"                    => boolean(),
%   "top_block_height"           => non_neg_integer(),
%   "top_key_block_hash"         => keyblock_hash()}.
% 
%%% Get/Set admin functions -spec network_id() -> Result when Result :: {ok, NetworkID} | {error, Reason}, NetworkID :: string(), Reason :: term(). %% @doc %% Returns the network ID or the atom `none' if unavailable. %% Checking this is not normally necessary, but if network ID assignment is dynamic %% in your system it may be necessary to call this before attempting to form %% call data or perform other actions on chain that require a signature. network_id() -> case status() of {ok, #{"network_id" := NetworkID}} -> {ok, NetworkID}; Error -> Error end. -spec chain_nodes() -> [chain_node()]. %% @doc %% Returns the list of currently assigned nodes. %% The normal reason to call this is in preparation for altering the nodes list or %% checking the current list in debugging. Note that the first node in the list is %% the "sticky" node: the one that will be used for submitting transactions and %% querying `next_nonce'. chain_nodes() -> hz_man:chain_nodes(). -spec chain_nodes(List) -> ok | {error, Reason} when List :: [chain_node()], Reason :: {invalid, [term()]}. %% @doc %% Sets the chain nodes that will be queried whenever you communicate with the chain. %% %% The common situation is that a project runs a non-mining node as part of the backend %% infrastructure. Typically one or two nodes is plenty, but this may need to expand %% depending on how much query load your application generates. %% %% There are two situations: one node, or multiple nodes. %% %% Single node: %% In the case of a single node, everything passes through that one node. Duh. %% %% Multiple nodes: %% In the case of multiple nodes a distinction is made between the node to which %% transactions that update the chain state are made and to which `next_nonce' queries %% are made, and nodes that are used for read-only queries. The node to which stateful %% transactions are submitted is called the "sticky node". This is the first node %% (head position) in the list of nodes submitted to the chain when `chain_nodes/1' %% is called. If using multiple nodes but the sticky node should also be used for %% read-only queries, submit the sticky node at the head of the list and again in %% the tail. chain_nodes(List) when is_list(List) -> hz_man:chain_nodes(List). -spec tls() -> boolean(). %% @doc %% Check whether TLS is in use. The typical situation is to not use TLS as nodes that %% serve as part of the backend of an application are typically run in the same %% backend network as the application service. When accessing chain nodes over the WAN %% however, TLS is strongly recommended to avoid a MITM attack. %% %% In this version of Hakuzaru TLS is either on or off for all nodes, making a mixed %% infrastructure complicated to support without two Hakuzaru instances. This will %% likely become a per-node setting in the future. %% %% TLS defaults to `false'. tls() -> hz_man:tls(). -spec tls(boolean()) -> ok. %% @doc %% Set TLS true or false. That's what a boolean is, by the way, `true' or `false'. %% This is a condescending comment. That means I am talking down to you. %% %% TLS defaults to `false'. tls(Boolean) -> hz_man:tls(Boolean). -spec timeout() -> Timeout when Timeout :: pos_integer() | infinity. %% @doc %% Returns the current request timeout setting in milliseconds. %% The default timeout is 5,000ms. %% The max timeout is 120,000ms. timeout() -> hz_man:timeout(). -spec timeout(MS) -> ok when MS :: pos_integer() | infinity. %% @doc %% Sets the request timeout in milliseconds. %% The default timeout is 5,000ms. %% The max timeout is 120,000ms. timeout(MS) -> hz_man:timeout(MS). %%% JSON query interface functions -spec top_height() -> {ok, Height} | {error, Reason} when Height :: pos_integer(), Reason :: chain_error(). %% @doc %% Retrieve the current height of the chain. %% %% NOTE: %% This will return the currently synced height, which may be different than the %% actual current top of the entire chain if the node being queried is still syncing %% (has not yet caught up with the chain). top_height() -> case top_block() of {ok, #{"height" := Height}} -> {ok, Height}; Error -> Error end. -spec top_block() -> {ok, TopBlock} | {error, Reason} when TopBlock :: microblock_header(), Reason :: chain_error(). %% @doc %% Returns the current block height as an integer. top_block() -> request("/v3/headers/top"). -spec kb_current() -> {ok, CurrentBlock} | {error, Reason} when CurrentBlock :: keyblock(), Reason :: chain_error(). %% @doc %% Returns the current keyblock's metadata as a map. kb_current() -> request("/v3/key-blocks/current"). -spec kb_current_hash() -> {ok, Hash} | {error, Reason} when Hash :: keyblock_hash(), Reason :: chain_error(). %% @doc %% Returns the current keyblock's hash. %% Equivalent of calling: %% ``` %% {ok, Current} = kb_current(), %% maps:get("hash", Current), %% ''' kb_current_hash() -> case request("/v3/key-blocks/current/hash") of {ok, #{"reason" := Reason}} -> {error, Reason}; {ok, #{"hash" := Hash}} -> {ok, Hash}; Error -> Error end. -spec kb_current_height() -> {ok, Height} | {error, Reason} when Height :: pos_integer(), Reason :: chain_error() | string(). %% @doc %% Returns the current keyblock's height as an integer. %% Equivalent of calling: %% ``` %% {ok, Current} = kb_current(), %% maps:get("height", Current), %% ''' kb_current_height() -> case request("/v3/key-blocks/current/height") of {ok, #{"reason" := Reason}} -> {error, Reason}; {ok, #{"height" := Height}} -> {ok, Height}; Error -> Error end. -spec kb_pending() -> {ok, keyblock_hash()} | {error, Reason} when Reason :: string(). %% @doc %% 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("/v3/key-blocks/pending")). -spec kb_by_hash(ID) -> {ok, KeyBlock} | {error, Reason} when ID :: keyblock_hash(), KeyBlock :: keyblock(), Reason :: chain_error() | string(). %% @doc %% Returns the keyblock identified by the provided hash. kb_by_hash(ID) -> result(request(["/v3/key-blocks/hash/", ID])). -spec kb_by_height(Height) -> {ok, KeyBlock} | {error, Reason} when Height :: non_neg_integer(), KeyBlock :: keyblock(), Reason :: chain_error() | string(). %% @doc %% Returns the keyblock identigied by the provided height. kb_by_height(Height) -> StringN = integer_to_list(Height), result(request(["/v3/key-blocks/height/", StringN])). %kb_insert(KeyblockData) -> % request("/v3/key-blocks", KeyblockData). -spec mb_header(ID) -> {ok, MB_Header} | {error, Reason} when ID :: microblock_hash(), MB_Header :: microblock_header(), Reason :: chain_error() | string(). %% @doc %% Returns the header of the microblock indicated by the provided ID (hash). mb_header(ID) -> result(request(["/v3/micro-blocks/hash/", ID, "/header"])). -spec mb_txs(ID) -> {ok, TXs} | {error, Reason} when ID :: microblock_hash(), TXs :: [transaction()], Reason :: chain_error() | string(). %% @doc %% Returns a list of transactions included in the microblock. mb_txs(ID) -> case request(["/v3/micro-blocks/hash/", ID, "/transactions"]) of {ok, #{"transactions" := TXs}} -> {ok, TXs}; {ok, #{"reason" := Reason}} -> {error, Reason}; Error -> Error end. -spec mb_tx_index(MicroblockID, Index) -> {ok, TX} | {error, Reason} when MicroblockID :: microblock_hash(), Index :: pos_integer(), TX :: transaction(), Reason :: chain_error() | string(). %% @doc %% Retrieve a single transaction from a microblock by index. %% (Note that indexes start from 1, not zero.) mb_tx_index(ID, Index) -> StrHeight = integer_to_list(Index), result(request(["/v3/micro-blocks/hash/", ID, "/transactions/index/", StrHeight])). -spec mb_tx_count(ID) -> {ok, Count} | {error, Reason} when ID :: microblock_hash(), Count :: non_neg_integer(), Reason :: chain_error() | string(). %% @doc %% Retrieve the number of transactions contained in the indicated microblock. mb_tx_count(ID) -> case request(["/v3/micro-blocks/hash/", ID, "/transactions/count"]) of {ok, #{"count" := Count}} -> {ok, Count}; {ok, #{"reason" := Reason}} -> {error, Reason}; Error -> Error end. -spec gen_current() -> {ok, Generation} | {error, Reason} when Generation :: generation(), Reason :: chain_error() | string(). %% @doc %% Retrieve the generation data (keyblock and list of associated microblocks) for %% the current generation. gen_current() -> result(request("/v3/generations/current")). -spec gen_by_id(ID) -> {ok, Generation} | {error, Reason} when ID :: keyblock_hash(), Generation :: generation(), Reason :: chain_error() | string(). %% @doc %% Retrieve generation data (keyblock and list of associated microblocks) by keyhash. gen_by_id(ID) -> result(request(["/v3/generations/hash/", ID])). -spec gen_by_height(Height) -> {ok, Generation} | {error, Reason} when Height :: non_neg_integer(), Generation :: generation(), Reason :: chain_error() | string(). %% @doc %% Retrieve generation data (keyblock and list of associated microblocks) by height. gen_by_height(Height) -> StrHeight = integer_to_list(Height), result(request(["/v3/generations/height/", StrHeight])). -spec acc(AccountID) -> {ok, Account} | {error, Reason} when AccountID :: account_id(), Account :: account(), Reason :: chain_error() | string(). %% @doc %% Retrieve account data by account ID (public key). acc(AccountID) -> result(request(["/v3/accounts/", AccountID])). -spec acc_at_height(AccountID, Height) -> {ok, Account} | {error, Reason} when AccountID :: account_id(), Height :: non_neg_integer(), Account :: account(), Reason :: chain_error() | string(). %% @doc %% Retrieve data for an account as that account existed at the given height. acc_at_height(AccountID, Height) -> StrHeight = integer_to_list(Height), case request(["/v3/accounts/", AccountID, "/height/", StrHeight]) of {ok, #{"reason" := "Internal server error"}} -> {error, gc_out_of_range}; {ok, #{"reason" := Reason}} -> {error, Reason}; Result -> Result end. -spec acc_at_block_id(AccountID, BlockID) -> {ok, Account} | {error, Reason} when AccountID :: account_id(), BlockID :: keyblock_hash() | microblock_hash(), Account :: account(), Reason :: chain_error() | string(). %% @doc %% Retrieve data for an account as that account existed at the moment the given %% block represented the current state of the chain. acc_at_block_id(AccountID, BlockID) -> case request(["/v3/accounts/", AccountID, "/hash/", BlockID]) of {ok, #{"reason" := "Internal server error"}} -> {error, gc_out_of_range}; {ok, #{"reason" := Reason}} -> {error, Reason}; Result -> Result end. -spec acc_pending_txs(AccountID) -> {ok, TXs} | {error, Reason} when AccountID :: account_id(), TXs :: [tx_hash()], Reason :: chain_error() | string(). %% @doc %% Retrieve a list of transactions pending for the given account. acc_pending_txs(AccountID) -> request(["/v3/accounts/", AccountID, "/transactions/pending"]). -spec next_nonce(AccountID) -> {ok, Nonce} | {error, Reason} when AccountID :: account_id(), Nonce :: non_neg_integer(), Reason :: chain_error() | string(). %% @doc %% Retrieve the next nonce for the given account next_nonce(AccountID) -> case request_sticky(["/v3/accounts/", AccountID, "/next-nonce"]) of {ok, #{"next_nonce" := Nonce}} -> {ok, Nonce}; {ok, #{"reason" := "Account not found"}} -> {ok, 1}; {ok, #{"reason" := Reason}} -> {error, Reason}; Error -> Error end. % case request_sticky(["/v3/accounts/", AccountID]) of % {ok, #{"nonce" := Nonce}} -> {ok, Nonce + 1}; % {ok, #{"reason" := "Account not found"}} -> {ok, 1}; % {ok, #{"reason" := Reason}} -> {error, Reason}; % Error -> Error % end. -spec dry_run(TX) -> {ok, Result} | {error, Reason} when TX :: binary() | string(), Result :: term(), % FIXME Reason :: term(). % FIXME %% @doc %% Execute a read-only transaction on the chain at the current height. %% Equivalent of %% ``` %% {ok, Hash} = hz:kb_current_hash(), %% hz:dry_run(TX, Hash), %% ''' %% NOTE: %% For this function to work the Gajumaru node you are sending the request %% to must have its configuration set to `http: endpoints: dry-run: true' 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 %% @doc %% Execute a read-only transaction on the chain at the current height with the %% supplied accounts. dry_run(TX, Accounts) -> case top_block() of {ok, #{"hash" := Hash}} -> dry_run(TX, Accounts, Hash); Error -> Error end. -spec dry_run(TX, Accounts, KBHash) -> {ok, Result} | {error, Reason} when TX :: binary() | string(), Accounts :: [pubkey()], KBHash :: binary() | string(), Result :: term(), % FIXME Reason :: term(). % FIXME %% @doc %% Execute a read-only transaction on the chain at the height indicated by the %% hash provided. dry_run(TX, Accounts, KBHash) -> KBB = to_binary(KBHash), TXB = to_binary(TX), DryData = #{top => KBB, accounts => Accounts, txs => [#{tx => TXB}], tx_events => true}, JSON = zj:binary_encode(DryData), request("/v3/dry_run", JSON). dry_run_map(Map) -> JSON = zj:binary_encode(Map), request("/v3/dry_run", JSON). -spec decode_bytearray_fate(EncodedStr) -> {ok, Result} | {error, Reason} when EncodedStr :: binary() | string(), Result :: none | term(), Reason :: term(). %% @doc %% Decode the "cb_XXXX" string that came out of a tx_info or dry_run, to %% the Erlang representation of FATE objects used by gmb_fate_encoding. See %% decode_bytearray/2 for an alternative that provides simpler outputs based on %% information provided by an AACI. decode_bytearray_fate(EncodedStr) -> Encoded = unicode:characters_to_binary(EncodedStr), {contract_bytearray, Binary} = gmser_api_encoder:decode(Encoded), case Binary of <<>> -> {ok, none}; <<"Out of gas">> -> {error, out_of_gas}; _ -> % FIXME there may be other errors that are encoded directly into % the byte array. We could try and catch to at least return % *something* for cases that we don't already detect. Object = gmb_fate_encoding:deserialize(Binary), {ok, Object} end. -spec decode_bytearray(Type, EncodedStr) -> {ok, Result} | {error, Reason} when Type :: term(), EncodedStr :: binary() | string(), Result :: none | term(), Reason :: term(). %% @doc %% Decode the "cb_XXXX" string that came out of a tx_info or dry_run, to the %% same format used by contract_call/* and contract_create/*. The Type argument %% must be the result type of the same function in the same AACI that was used %% to create the transaction that EncodedStr came from. decode_bytearray(Type, EncodedStr) -> case decode_bytearray_fate(EncodedStr) of {ok, none} -> {ok, none}; {ok, Object} -> hz_aaci:fate_to_erlang(Type, Object); {error, Reason} -> {error, Reason} end. to_binary(S) when is_binary(S) -> S; to_binary(S) when is_list(S) -> list_to_binary(S). -spec tx(ID) -> {ok, TX} | {error, Reason} when ID :: tx_hash(), TX :: transaction(), Reason :: chain_error() | string(). %% @doc %% Retrieve a transaction by ID. tx(ID) -> request(["/v3/transactions/", ID]). -spec tx_info(ID) -> {ok, Info} | {error, Reason} when ID :: tx_hash(), Info :: term(), % FIXME Reason :: chain_error() | string(). %% @doc %% Retrieve TX metadata by ID. tx_info(ID) -> result(request(["/v3/transactions/", ID, "/info"])). -spec post_tx(Data) -> {ok, Result} | {error, Reason} when Data :: string() | binary(), Result :: term(), % FIXME Reason :: chain_error() | string(). %% @doc %% Post a transaction to the chain. post_tx(Data) when is_binary(Data) -> JSON = zj:binary_encode(#{tx => Data}), request_sticky("/v3/transactions", JSON); post_tx(Data) when is_list(Data) -> post_tx(list_to_binary(Data)). -spec contract(ID) -> {ok, ContractData} | {error, Reason} when ID :: contract_id(), ContractData :: contract_data(), Reason :: chain_error() | string(). %% @doc %% Retrieve a contract's metadata by ID. contract(ID) -> result(request(["/v3/contracts/", ID])). -spec contract_code(ID) -> {ok, Bytecode} | {error, Reason} when ID :: contract_id(), Bytecode :: contract_byte_array(), Reason :: chain_error() | string(). %% @doc %% Retrieve the code of a contract as represented on chain. contract_code(ID) -> case request(["/v3/contracts/", ID, "/code"]) of {ok, #{"bytecode" := Bytecode}} -> {ok, Bytecode}; {ok, #{"reason" := Reason}} -> {error, Reason}; Error -> Error end. -spec contract_source(ID) -> {ok, Bytecode} | {error, Reason} when ID :: contract_id(), Bytecode :: contract_byte_array(), Reason :: chain_error() | string(). %% @doc %% Retrieve the code of a contract as represented on chain. contract_source(ID) -> case request(["/v3/contracts/", ID, "/source"]) of {ok, #{"source" := Source}} -> {ok, Source}; {ok, #{"reason" := Reason}} -> {error, Reason}; Error -> Error end. -spec contract_poi(ID) -> {ok, Bytecode} | {error, Reason} when ID :: contract_id(), Bytecode :: contract_byte_array(), Reason :: chain_error() | string(). %% @doc %% Retrieve the POI of a contract stored on chain. contract_poi(ID) -> request(["/v3/contracts/", ID, "/poi"]). -spec name(Name) -> {ok, Info} | {error, Reason} when Name :: string(), % _ ++ ".chain" Info :: name_info(), Reason :: chain_error() | string(). %% @doc %% Retrieve a name's chain information. name(Name) -> result(request(["/v3/names/", Name])). % TODO %channel(ID) -> % request(["/v3/channels/", ID]). % FIXME: This should take a specific peer address:port otherwise it will be pointlessly % random. -spec peer_pubkey() -> {ok, Pubkey} | {error, Reason} when Pubkey :: peer_pubkey(), Reason :: term(). % FIXME %% @doc %% Returns the given node's public key, assuming a node is reachable at %% the given address. peer_pubkey() -> case request("/v3/peers/pubkey") of {ok, #{"pubkey" := Pubkey}} -> {ok, Pubkey}; {ok, #{"reason" := Reason}} -> {error, Reason}; Error -> Error end. % TODO: Make a status/1 that allows the caller to query a specific node rather than % a random one from the pool. -spec status() -> {ok, Status} | {error, Reason} when Status :: status(), Reason :: chain_error(). %% @doc %% Retrieve the node's status and meta it currently has about the chain. status() -> request("/v3/status"). -spec status_chainends() -> {ok, ChainEnds} | {error, Reason} when ChainEnds :: [keyblock_hash()], Reason :: chain_error(). %% @doc %% Retrieve the latest keyblock hashes status_chainends() -> request("/v3/status/chain-ends"). request_sticky(Path) -> hz_man:request_sticky(unicode:characters_to_list(Path)). request_sticky(Path, Payload) -> hz_man:request_sticky(unicode:characters_to_list(Path), Payload). request(Path) -> hz_man:request(unicode:characters_to_list(Path)). request(Path, Payload) -> hz_man:request(unicode:characters_to_list(Path), Payload). result({ok, #{"reason" := Reason}}) -> {error, Reason}; result(Received) -> Received. %%% 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, {ok, Height} = top_height(), TTL = Height + 262980, Gas = 500000, GasPrice = min_gas_price(), contract_create(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Path, InitArgs); Error -> Error end. -spec contract_create(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Path, InitArgs) -> Result when CreatorID :: pubkey(), Nonce :: pos_integer(), Amount :: non_neg_integer(), TTL :: non_neg_integer(), Gas :: pos_integer(), GasPrice :: pos_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 Gajumaru. %% %% The meaning of each argument is as follows: %% %% 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, TTL, Gas, GasPrice, Path, InitArgs) -> case file:read_file(Path) of {ok, Source} -> Dir = filename:dirname(Path), {ok, CWD} = file:get_cwd(), SrcDir = so_utils:canonical_dir(Path), Options = [{aci, json}, {src_file, Path}, {src_dir, SrcDir}, {include, {file_system, [CWD, so_utils:canonical_dir(Dir)]}}], contract_create2(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Source, Options, InitArgs); Error -> Error end. contract_create2(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Source, Options, InitArgs) -> case so_compiler:from_string(Source, Options) of {ok, Compiled} -> contract_create_built(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, InitArgs); Error -> Error end. -spec contract_create_built(CreatorID, Compiled, InitArgs) -> Result when CreatorID :: unicode:chardata(), Compiled :: map(), InitArgs :: [string()], Result :: {ok, CreateTX} | {error, Reason}, CreateTX :: binary(), Reason :: file:posix() | bad_fun_name | aaci_not_found | term(). %% @doc %% This function takes the compiler output (instead of starting from source), %% 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_built(CreatorID, Compiled, InitArgs) -> case next_nonce(CreatorID) of {ok, Nonce} -> Amount = 0, {ok, Height} = top_height(), TTL = Height + 262980, Gas = 500000, GasPrice = min_gas_price(), contract_create_built(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, InitArgs); Error -> Error end. contract_create_built(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, InitArgs) -> AACI = hz_aaci:prepare_aaci(maps:get(aci, Compiled)), case encode_call_data(AACI, "init", InitArgs) of {ok, CallData} -> assemble_calldata(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData); Error -> Error end. assemble_calldata(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData) -> PK = unicode:characters_to_binary(CreatorID), try {account_pubkey, OwnerID} = gmser_api_encoder:decode(PK), assemble_calldata2(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData) catch Error:Reason -> {Error, Reason} end. assemble_calldata2(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData) -> Code = gmser_contract_code:serialize(Compiled), Source = unicode:characters_to_binary(maps:get(contract_source, Compiled, <<>>)), VM = 1, ABI = 1, <> = <>, ContractCreateVersion = 1, Type = contract_create_tx, Fields = [{owner_id, gmser_id:create(account, OwnerID)}, {nonce, Nonce}, {code, Code}, {source, Source}, {ct_version, CTVersion}, {ttl, TTL}, {deposit, 0}, {amount, Amount}, {gas_price, GasPrice}, {gas, Gas}, {call_data, CallData}], Template = [{owner_id, id}, {nonce, int}, {code, binary}, {source, binary}, {ct_version, int}, {ttl, int}, {deposit, int}, {amount, int}, {gas_price, int}, {gas, int}, {call_data, binary}], TXB = gmser_chain_objects:serialize(Type, ContractCreateVersion, Template, Fields), try {ok, gmser_api_encoder:encode(transaction, TXB)} catch error:Reason -> {error, Reason} end. -spec read_aci(Path) -> Result when Path :: file:filename(), Result :: {ok, ACI} | {error, Reason}, ACI :: tuple(), % FIXME: Change to correct Sophia record Reason :: file:posix() | bad_aci. %% @doc %% This function reads the contents of an .aci file. %% ACI data is required for the contract call encoder to function properly. %% ACI data is can be generated and stored in JSON data, and the Sophia CLI tool %% can perform this action. Unfortunately, JSON is not the way that ACI data is %% represented internally, and here we need the actual native representation. %% %% ACI encding/decoding and contract call encoding is significantly complex enough that %% this provides for a pretty large savings in complexity for this library, dramatically %% reduces runtime dependencies, and makes call encoding much more efficient (as a %% huge number of steps are completely eliminated by this). read_aci(Path) -> case file:read_file(Path) of {ok, Bin} -> try {ok, binary_to_term(Bin, [safe])} catch error:badarg -> {error, bad_aci} end; Error -> Error end. -spec contract_call(CallerID, AACI, ConID, Fun, Args) -> Result when CallerID :: unicode:chardata(), AACI :: aaci() | {aaci, Label :: term()}, ConID :: unicode:chardata(), Fun :: string(), Args :: [string()], Result :: {ok, CallTX} | {error, Reason}, CallTX :: binary(), Reason :: term(). %% @doc %% Form a contract call using hardcoded default values for `Gas', `GasPrice', %% and `Amount' to simplify the call (10 args is a bit much for normal calls!). %% The values used are 20k for `Gas', the `GasPrice' is fixed at 1b (the %% default "miner minimum" defined in default configs), and the `Amount' is 0. %% %% For details on the meaning of these and other argument values see the doc comment %% for contract_call/10. contract_call(CallerID, AACI, ConID, Fun, Args) -> case next_nonce(CallerID) of {ok, Nonce} -> Gas = min_gas(), GasPrice = min_gas_price(), Amount = 0, {ok, Height} = top_height(), TTL = Height + 262980, contract_call(CallerID, Nonce, Gas, GasPrice, Amount, TTL, AACI, ConID, Fun, Args); Error -> Error end. -spec contract_call(CallerID, Gas, AACI, ConID, Fun, Args) -> Result when CallerID :: unicode:chardata(), Gas :: pos_integer(), AACI :: aaci() | {aaci, Label :: term()}, 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) -> case next_nonce(CallerID) of {ok, Nonce} -> GasPrice = min_gas_price(), Amount = 0, {ok, Height} = top_height(), TTL = Height + 262980, contract_call(CallerID, Nonce, Gas, GasPrice, Amount, TTL, AACI, ConID, Fun, Args); Error -> Error end. -spec contract_call(CallerID, Nonce, Gas, GasPrice, Amount, TTL, AACI, ConID, Fun, Args) -> Result when CallerID :: unicode:chardata(), Nonce :: pos_integer(), Gas :: pos_integer(), GasPrice :: pos_integer(), Amount :: non_neg_integer(), TTL :: non_neg_integer(), AACI :: aaci() | {aaci, Label :: term()}, ConID :: unicode:chardata(), Fun :: string(), Args :: [string()], Result :: {ok, CallTX} | {error, Reason}, CallTX :: binary(), Reason :: term(). %% @doc %% Form a contract call using the supplied values. %% %% Contract call formation is a rather opaque process if you're new to Gajumaru or %% smart contract execution in general. %% %% The meaning of each argument is as follows: %%
    %%
  • %% CallerID: %% This is the public key of the entity making the contract call. %% The key must be encoded as a string prefixed with `"ak_"' (or `<<"ak_">>' in the %% case of a binary string, which is also acceptable). %% The returned call will still need to be signed by the caller's private %% key. %%
  • %%
  • %% Nonce: %% 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 Gajumaru node (via `hz:next_nonce(CallerID)', for example). %%
  • %%
  • %% Gas: %% 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 Gajumaru 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 Gajumaru 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. %%
  • %%
  • %% GasPrice: %% This is a factor that is used calculate a value in pucks (the smallest unit of %% Gajumaru'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. %%
  • %%
  • %% Amount: %% All Gajumaru 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 into 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. %%
  • %%
  • %% ACI: %% 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 Gajumaru 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 Gajumaru Launcher, %% a GUI tool with a "Developers' Workbench" feature that can assist with this. %%
  • %%
  • %% ConID: %% 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. %%
  • %%
  • %% Fun: %% 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). %%
  • %%
  • %% Args: %% 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"'). %%
  • %%
%% 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_call(CallerID, Nonce, Gas, GP, Amount, TTL, AACI, ConID, Fun, Args) -> case encode_call_data(AACI, Fun, Args) of {ok, CD} -> contract_call2(CallerID, Nonce, Gas, GP, Amount, TTL, ConID, CD); Error -> Error end. contract_call2(CallerID, Nonce, Gas, GasPrice, Amount, TTL, ConID, CallData) -> CallerBin = unicode:characters_to_binary(CallerID), try {account_pubkey, PK} = gmser_api_encoder:decode(CallerBin), contract_call3(PK, Nonce, Gas, GasPrice, Amount, TTL, ConID, CallData) catch Error:Reason -> {Error, Reason} end. contract_call3(PK, Nonce, Gas, GasPrice, Amount, TTL, ConID, CallData) -> ConBin = unicode:characters_to_binary(ConID), try {contract_pubkey, CK} = gmser_api_encoder:decode(ConBin), contract_call4(PK, Nonce, Gas, GasPrice, Amount, TTL, CK, CallData) catch Error:Reason -> {Error, Reason} end. contract_call4(PK, Nonce, Gas, GasPrice, Amount, TTL, CK, CallData) -> ABI = 1, CallVersion = 1, Type = contract_call_tx, Fields = [{caller_id, gmser_id:create(account, PK)}, {nonce, Nonce}, {contract_id, gmser_id:create(contract, CK)}, {abi_version, ABI}, {ttl, TTL}, {amount, Amount}, {gas_price, GasPrice}, {gas, Gas}, {call_data, CallData}], Template = [{caller_id, id}, {nonce, int}, {contract_id, id}, {abi_version, int}, {ttl, int}, {amount, int}, {gas_price, int}, {gas, int}, {call_data, binary}], TXB = gmser_chain_objects:serialize(Type, CallVersion, Template, Fields), try {ok, gmser_api_encoder:encode(transaction, TXB)} catch error:Reason -> {error, Reason} end. -spec prepare_contract(File) -> {ok, AACI} | {error, Reason} when File :: file:filename(), AACI :: aaci(), Reason :: term(). %% @doc %% Compile a contract and extract the function spec meta for use in future formation %% of calldata prepare_contract(File) -> case so_compiler:file(File, [{aci, json}]) of {ok, #{aci := ACI}} -> {ok, hz_aaci:prepare_aaci(ACI)}; Error -> Error end. -spec cache_aaci(Label, AACI) -> ok when Label :: term(), AACI :: aaci(). %% @doc %% Caches an AACI for future reference in calls that would otherwise require %% the AACI as an argument. Once cached, a pre-built AACI can be referenced in %% later calls by substituting the AACI argument with `{aaci, Label}'. cache_aaci(Label, AACI) -> hz_man:cache_aaci(Label, AACI). -spec lookup_aaci(Label) -> Result when Label :: term(), Result :: {ok, aaci()} | error. %% @doc %% Retrieve a previously prepared and cached AACI. lookup_aaci(Label) -> hz_man:lookup_aaci(Label). -spec aaci_lookup_spec(AACI, Fun) -> {ok, Type} | {error, Reason} when AACI :: aaci() | {aaci, Label :: term()}, Fun :: binary() | string(), Type :: {term(), term()}, % FIXME Reason :: bad_fun_name | aaci_not_found. %% @doc %% Look up the type information of a given function, in the AACI provided by %% prepare_contract/1. This type information, particularly the return type, is %% useful for calling decode_bytearray/2. aaci_lookup_spec(AACI = {aaci, _, _, _}, Fun) -> hz_aaci:get_function_signature(AACI, Fun); aaci_lookup_spec({aaci, Label}, Fun) -> case hz_man:lookup_aaci(Label) of {ok, AACI} -> hz_aaci:get_function_signature(AACI, Fun); error -> {error, aaci_not_found} end. -spec min_gas_price() -> integer(). %% @doc %% 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() %% %% Surely there can be some more nuance to this, but until a "gas station" type %% market/chain survey service exists we will use this naive value as a default %% and users can call contract_call/10 if they want more fine-tuned control over the %% price. This won't really matter much until the chain has a high enough TPS that %% contention becomes an issue. min_gas_price() -> 1_000_000_000. -spec min_gas() -> integer(). %% @doc %% This function always returns 200,000 in the current version. min_gas() -> 200000. encode_call_data({aaci, _ContractName, FunDefs, _TypeDefs}, Fun, Args) -> case maps:find(Fun, FunDefs) of {ok, {ArgDef, _ResultDef}} -> encode_call_data2(ArgDef, Fun, Args); error -> {error, bad_fun_name} end; encode_call_data({aaci, Label}, Fun, Args) -> case hz_man:lookup_aaci(Label) of {ok, AACI} -> encode_call_data(AACI, Fun, Args); error -> {error, aaci_not_found} end. encode_call_data2(ArgDef, Fun, Args) -> case hz_aaci:erlang_args_to_fate(ArgDef, Args) of {ok, Coerced} -> gmb_fate_abi:create_calldata(Fun, Coerced); Errors -> Errors end. sign_tx(Unsigned, SecKey) -> case network_id() of {ok, NetworkID} -> sign_tx(Unsigned, SecKey, NetworkID); Error -> Error end. sign_tx(Unsigned, SecKey, MNetworkID) -> UnsignedBin = unicode:characters_to_binary(Unsigned), NetworkID = unicode:characters_to_binary(MNetworkID), {ok, TX_Data} = gmser_api_encoder:safe_decode(transaction, UnsignedBin), {ok, Hash} = eblake2:blake2b(32, TX_Data), NetworkHash = <>, Signature = ecu_eddsa:sign_detached(NetworkHash, SecKey), SigTxType = signed_tx, SigTxVsn = 1, SigTemplate = [{signatures, [binary]}, {transaction, binary}], TX = [{signatures, [Signature]}, {transaction, TX_Data}], SignedTX = gmser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX), gmser_api_encoder:encode(transaction, SignedTX). spend(SenderID, SecKey, ReceipientID, Amount, Payload) -> case status() of {ok, #{"top_block_height" := Height, "network_id" := NetworkID}} -> spend(SenderID, SecKey, ReceipientID, Amount, Payload, Height, NetworkID); Error -> Error end. spend(SenderID, SecKey, RecipientID, Amount, Payload, Height, NetworkID) -> case next_nonce(SenderID) of {ok, Nonce} -> {ok, Height} = top_height(), TTL = Height + 262980, Gas = 20000, GasPrice = min_gas_price(), spend(SenderID, SecKey, RecipientID, Amount, GasPrice, Gas, TTL, Nonce, Payload, NetworkID); Error -> Error end. spend(SenderID, SecKey, RecipientID, Amount, GasPrice, Gas, TTL, Nonce, Payload, NetworkID) -> case decode_account_id(unicode:characters_to_binary(SenderID)) of {ok, DSenderID} -> spend2(gmser_id:create(account, DSenderID), SecKey, RecipientID, Amount, GasPrice, Gas, TTL, Nonce, Payload, NetworkID); Error -> Error end. spend2(DSenderID, SecKey, RecipientID, Amount, GasPrice, Gas, TTL, Nonce, Payload, NetworkID) -> case decode_account_id(unicode:characters_to_binary(RecipientID)) of {ok, DRecipientID} -> spend3(DSenderID, SecKey, gmser_id:create(account, DRecipientID), Amount, GasPrice, Gas, TTL, Nonce, Payload, NetworkID); Error -> Error end. decode_account_id(B) -> try {account_pubkey, PK} = gmser_api_encoder:decode(B), {ok, PK} catch E:R -> {E, R} end. spend3(DSenderID, SecKey, DRecipientID, Amount, GasPrice, Gas, TTL, Nonce, Payload, MNetworkID) -> NetworkID = unicode:characters_to_binary(MNetworkID), Type = spend_tx, Vsn = 1, Fields = [{sender_id, DSenderID}, {recipient_id, DRecipientID}, {amount, Amount}, {gas_price, GasPrice}, {gas, Gas}, {ttl, TTL}, {nonce, Nonce}, {payload, Payload}], Template = [{sender_id, id}, {recipient_id, id}, {amount, int}, {gas_price, int}, {gas, int}, {ttl, int}, {nonce, int}, {payload, binary}], BinaryTX = gmser_chain_objects:serialize(Type, Vsn, Template, Fields), NetworkTX = <>, Signature = ecu_eddsa:sign_detached(NetworkTX, SecKey), SigTxType = signed_tx, SigTxVsn = 1, SigTemplate = [{signatures, [binary]}, {transaction, binary}], TX_Data = [{signatures, [Signature]}, {transaction, BinaryTX}], SignedTX = gmser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX_Data), Encoded = gmser_api_encoder:encode(transaction, SignedTX), hz:post_tx(Encoded). -spec sign_message(Message, SecKey) -> Sig when Message :: binary(), SecKey :: binary(), Sig :: binary(). sign_message(Message, SecKey) -> Prefix = message_sig_prefix(), {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), ecu_eddsa:sign_detached(Hashed, SecKey). -spec verify_signature(Sig, Message, PubKey) -> Result when Sig :: string(), % base64 encoded signature, Message :: iodata(), PubKey :: pubkey(), Result :: {ok, Outcome :: boolean()} | {error, Reason :: term()}. %% @doc %% Verify a message signature given the signature, the message that was signed, and the %% public half of the key that was used to sign. %% %% The result of a complete signature check is a boolean value return in an `{ok, Outcome}' %% tuple, and any `{error, Reason}' return value is an indication that something about the %% check failed before verification was able to pass or fail (bad key encoding or similar). verify_signature(Sig, Message, PubKey) -> case gmser_api_encoder:decode(PubKey) of {account_pubkey, PK} -> verify_signature2(Sig, Message, PK); Other -> {error, {bad_key, Other}} end. verify_signature2(Sig, Message, PK) -> % Gajumaru signatures require messages to be salted and hashed, then % the hash is what gets signed in order to protect % the user from accidentally signing a transaction disguised as a message. % % Salt the message then hash with blake2b. Prefix = message_sig_prefix(), {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)>>, Signature = base64:decode(Sig), Result = ecu_eddsa:sign_verify_detached(Signature, Hashed, PK), {ok, Result}. message_sig_prefix() -> <<"Gajumaru Signed Message:\n">>. % 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, {non_pos_N, N}}; vencode(N) when N < 16#FD -> {ok, <>}; 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) >>, <>. -spec sign_binary(Binary, SecKey) -> Sig when Binary :: binary(), SecKey :: binary(), Sig :: binary(). sign_binary(Binary, SecKey) -> Prefix = binary_sig_prefix(), Target = <>, {ok, Hash} = eblake2:blake2b(32, Target), ecu_eddsa:sign_detached(Hash, SecKey). -spec verify_bin_signature(Sig, Binary, PubKey) -> Result when Sig :: string(), % base64 encoded signature, Binary :: binary(), PubKey :: pubkey(), Result :: {ok, Outcome :: boolean()} | {error, Reason :: term()}. verify_bin_signature(Sig, Binary, PubKey) -> case gmser_api_encoder:decode(PubKey) of {account_pubkey, PK} -> verify_bin_signature2(Sig, Binary, PK); Other -> {error, {bad_key, Other}} end. verify_bin_signature2(Sig, Binary, PK) -> Prefix = binary_sig_prefix(), Target = <>, {ok, Hash} = eblake2:blake2b(32, Target), Signature = base64:decode(Sig), Result = ecu_eddsa:sign_verify_detached(Signature, Hash, PK), {ok, Result}. binary_sig_prefix() -> <<"Gajumaru Signed Binary:">>. %%% Debug functionality % debug_network() -> % request("/v3/debug/network"). % % /v3/debug/contracts/create % /v3/debug/contracts/call % /v3/debug/oracles/register % /v3/debug/oracles/extend % /v3/debug/oracles/query % /v3/debug/oracles/respond % /v3/debug/names/preclaim % /v3/debug/names/claim % /v3/debug/names/update % /v3/debug/names/transfer % /v3/debug/names/revoke % /v3/debug/transactions/spend % /v3/debug/channels/create % /v3/debug/channels/deposit % /v3/debug/channels/withdraw % /v3/debug/channels/snapshot/solo % /v3/debug/channels/set-delegates % /v3/debug/channels/close/mutual % /v3/debug/channels/close/solo % /v3/debug/channels/slash % /v3/debug/channels/settle % /v3/debug/transactions/pending % /v3/debug/names/commitment-id % /v3/debug/accounts/beneficiary % /v3/debug/accounts/node % /v3/debug/peers % /v3/debug/transactions/dry-run % /v3/debug/transactions/paying-for % /v3/debug/check-tx/pool/{hash} % /v3/debug/token-supply/height/{height} % /v3/debug/crash