Financial Transaction
Suppose Alice wants to transfer 100 tokens to Bob. In this case we are talking about native Near Protocol tokens, oppose to user-defined tokens implemented through a smart contract. There are several way this can be done:
- Direct transfer through a transaction containing transfer action;
- Alice calling a smart contract that in turn creates a financial transaction towards Bob.
In this section we are talking about the former simpler scenario.
Pre-requisites
For this to work both Alice and Bob need to have accounts and an access to them through the full access keys.
Suppose Alice has account alice_near
and Bob has account bob_near
. Also, some time in the past,
each of them has created a public-secret key-pair, saved the secret key somewhere (e.g. in a wallet application)
and created a full access key with the public key for the account.
We also need to assume that both Alice and Bob has some number of tokens on their accounts. Alice needs >100 tokens on the account so that she could transfer 100 tokens to Bob, but also Alice and Bob need to have some tokens to pay for the rent of their account -- which is essentially the cost of the storage occupied by the account in the Near Protocol network.
Creating a transaction
To send the transaction neither Alice nor Bob need to run a node. However, Alice needs a way to create and sign a transaction structure. Suppose Alice uses near-shell or any other third-party tool for that. The tool then creates the following structure:
Transaction {
signer_id: "alice_near",
public_key: "ed25519:32zVgoqtuyRuDvSMZjWQ774kK36UTwuGRZMmPsS6xpMy",
nonce: 57,
receiver_id: "bob_near",
block_hash: "CjNSmWXTWhC3EhRVtqLhRmWMTkRbU96wUACqxMtV1uGf",
actions: vec![
Action::Transfer(TransferAction {deposit: 100} )
],
}
Which contains one token transfer action, the id of the account that signs this transaction (alice_near
)
the account towards which this transaction is addressed (bob_near
). Alice also uses the public key
associated with one of the full access keys of alice_near
account.
Additionally, Alice uses the nonce which is unique value that allows Near Protocol to differentiate the transactions (in case there are several transfers coming in rapid succession) which should be strictly increasing with each transaction. Unlike in Ethereum, nonces are associated with access keys, oppose to the entire accounts, so several users using the same account through different access keys need not to worry about accidentally reusing each other's nonces.
The block hash is used to calculate the transaction "freshness". It is used to make sure the transaction does
not get lost (let's say somewhere in the network) and then arrive hours, days, or years later when it is not longer relevant
or would be undesirable to execute. The transaction does not need to arrive at the specific block, instead it is required to
arrive within certain number of blocks from the bock identified by the block_hash
(as of 2019-10-27 the constant is 10 blocks).
Any transaction arriving outside this threshold is considered to be invalid.
near-shell or other tool that Alice uses then signs this transaction, by: computing the hash of the transaction and signing it
with the secret key, resulting in a SignedTransaction
object.
Sending the transaction
To send the transaction, near-shell connects through the RPC to any Near Protocol node and submits it.
If users wants to wait until the transaction is processed they can use send_tx_commit
JSONRPC method which waits for the
transaction to appear in a block. Otherwise the user can use send_tx_async
.
Transaction to receipt
We skip the details on how the transaction arrives to be processed by the runtime, since it is a part of the blockchain layer
discussion.
We consider the moment where SignedTransaction
is getting passed to Runtime::apply
of the
runtime
crate.
Runtime::apply
immediately passes transaction to Runtime::process_transaction
which in turn does the following:
- Verifies that transaction is valid;
- Applies initial reversible and irreversible charges to
alice_near
account; - Creates a receipt with the same set of actions directed towards
bob_near
.
The first two items are performed inside Runtime::verify_and_charge_transaction
method.
Specifically it does the following checks:
- Verifies that
alice_near
andbob_near
are syntactically valid account ids; - Verifies that the signature of the transaction is correct based on the transaction hash and the attached public key;
- Retrieves the latest state of the
alice_near
account, and simultaneously checks that it exists; - Retrieves the state of the access key of that
alice_near
used to sign the transaction; - Checks that transaction nonce is greater than the nonce of the latest transaction executed with that access key;
- Checks whether the account that signed the transaction is the same as the account that receives it. In our case the sender (
alice_near
) and the receiver (bob_near
) are not the same. We apply different fees if receiver and sender is the same account; - Applies the storage rent to the
alice_near
account; - Computes how much gas we need to spend to convert this transaction to a receipt;
- Computes how much balance we need to subtract from
alice_near
, in this case it is 100 tokens; - Deducts the tokens and the gas from
alice_near
balance, using the current gas price; - Checks whether after all these operations account has enough balance to passively pay for the rent for the next several blocks (an economical constant defined by Near Protocol). Otherwise account will be open for an immediate deletion, which we do not want;
- Updates the
alice_near
account with the new balance and the used access key with the new nonce; - Computes how much reward should be paid to the validators from the burnt gas.
If any of the above operations fail all of the changes will be reverted.
Processing receipt
The receipt created in the previous section will eventually arrive to a runtime on the shard that hosts bob_near
account.
Again, it will be processed by Runtime::apply
which will immediately call Runtime::process_receipt
.
It will check that this receipt does not have data dependencies (which is only the case of function calls) and will then call Runtime::apply_action_receipt
on TransferAction
.
Runtime::apply_action_receipt
will perform the following checks:
- Retrieves the state of
bob_near
account, if it still exists (it is possible that Bob has deleted his account concurrently with the transfer transaction); - Applies the rent to Bob's account;
- Computes the cost of processing a receipt and a transfer action;
- Checks if
bob_near
still exists and if it is deposits the transferred tokens; - Computes how much reward should be paid to the validators from the burnt gas.