Nomadic Labs
Nomadic Labs

The Protocol: from High-level Command Line to Low-level Operations

Understanding the source code of Tezos may take much time for fresh contributors. This gentle note gives a global presentation of the control flow of a simple transaction example, going from the high-level command line call to the low-level account operations.

This note explains and illustrates flow of control in Tezos using the example of carrying out a simple transaction. We will go from the high-level command line call to the low-level account operations. To guide you through the codebase, we give line numbers based on release 8.2 (commit 6102c808).

Where do I start?

Our scenario. Imagine that Alice and Bob have one account each, and Alice wants to send ꜩ10 from her account to Bob’s. Alice can proceed by using a transfer operation. This transaction will trigger some additional transactions, which will be handled automatically by the system:

  1. Alice must pay baking fees (tokens paid to the baker who writes Alice’s main transfer onto the blockchain).
  2. Alice may also have to pay for key revelation to reveal the public key of her account.1

Here’s a picture:

The easiest way to execute our example is to run a node locally by using the sandboxed mode. In the terminal, we just type:

$ tezos-client transfer 10 from alice to bob

Inside the code

As far as the Tezos codebase is concerned, everything is a contract. This includes user-owned accounts (called implicit accounts in the codebase) and smart contracts (called originated accounts in the codebase). This is reflected in the variable and function names in the code cited below, so for consistency we may also call everything a “contract” — but note that in our example, the source and destination contracts are actually the user-owned (i.e., implicit) accounts of Alice and Bob.

Step 1: Dispatching the client command

  • The flow of control starts with the commands function, whose code is in client_proto_context_commands.ml (l.254). This determines the command-line syntax for the switches following an invocation of ‘tezos-client’.2 In our example, we have invoked the transfer sub-command (l.893).

  • Control now flows to transfer_command (l.110), which distinguishes the type of the source contract (in the pattern-matched variable sourcel.136) — in our case it is implicit and admits a public key hash. Flow of control passes to another function, transfer (l.167).

  • transfer is in client_proto_context.ml (l.73). Its job is to build the transaction operation itself, using a function build_transaction_operation. In our case, the operation is a GADT Transaction of type manager_operation (discussed below). In particular, its field amount contains the amount of tez to be transferred (excluding baking fees). transfer wraps the operation in an annotated_manager_operation by function prepare_manager_operation, adding fee and gas information; then it further wraps the operation into the GADT Injection.Single_manager of type annotated_manager_operation_list; and finally it passes this all on to function inject_manager_operation, detailed below.

Manager operations are operations that compete between themselves for inclusion in a block. This is in contrast to other types of operations such as consensus or governance related operations. The competition is realized through the proposed fees, which go to the baker selecting the operations to be included in the block. There are four types of manager operations, defined by the type manager_operation in operation_repr.ml using the following four constructors:

  1. Reveal for the revelation of a public key, a one-time prerequisite to any signed operation, in order to be able to check the sender’s signature.
  2. Transaction of some amount to some destination contract.
  3. Origination of a contract using a smart-contract script and intially credited with the amount credit.
  4. Delegation to some staking contract (designated by its public key hash).

Although we usually use tezos-client to inject a single operation, the economic protocol use a built-in notion of a list of operations (also called “packed operation”) that can be injected as a whole, for efficiency reasons. In this case the constructor Injection.Cons_manager, instead of Injection.Single_manager, would be used to build a value of type annotated_manager_operation_list.

Step 2: Injecting the operation

  • The function inject_manager_operation in injection.ml (l.911) proceeds, from the previous list of annotated manager operations (of type annotated_manager_operation_list), by creating a list of contents (of type contents_list) where each content (of type contents) is a GADT Manager_operation. This mapping is performed by the local function build_contents, and for each element by the local function contents_of_manager. These functions deconstruct and reconstruct the list of annotated manager operations, providing information for the missing values (fee, gas limit, storage limit).

    Then, function inject_manager_operation proceeds according to two cases:

    1. If Alice’s public key has not yet been revealed, then the function adds one more manager operation to reveal her public key.
    2. If Alice’s public key has been revealed, then the next function is directly called with this list of contents, which is the case in our example.
  • The function inject_operation (l.773) completes the final steps of injecting into the node. It:

    • computes estimated gas and storage (in function may_patch_limits),
    • checks compliance with cap fees (in function may_patch_limits),
    • performs a simulation (by calling preapply), and finally
    • injects the operation (by calling Shell_services.Injection.operation in l.825).

Step 3: Dispatching the operation

The calls to Alpha_block_services.Helpers.Preapply.operations in preapply for simulation and to Shell_services.Injection.operation for injection from Step 2 actually represent two RPC calls from the client to the node. Such RPCs are declared in src/lib_shell_services and implemented in src/lib_shell/ (in files with the name of the form *_directory.ml). To serve these two RPCs, the shell part of the node simply dispatches the operation execution to the relevant economic protocol, here Alpha.

Step 4: Applying the operation in the economic protocol

  • The main entrypoint to apply the operation which we injected in the previous section is the function apply_operation in src/proto_alpha/lib_protocol/main.ml (l.180). A variable operation (of type 'kind operation') containing all information for the operation itself is passed to the next function.

The record-type 'kind operation defined in file operation_repr.ml (l.62) contains a field protocol_data of type 'kind protocol_data (l.67). It wraps together a list of contents (of type contents_list) and a signature of the source contract for authentication purposes.

  • Next, the function apply_operation in src/proto_alpha/lib_protocol/apply.ml (l.1339) is called, which after some initialization, uses operation.protocol_data.contents (of type contents_list) to call the next function.
  • In the function apply_contents_list (l.1103), two function calls are worth mentioning:
    1. On one side (left in the figure), the function precheck_manager_contents_list (l.1327), which in turn calls the function precheck_manager_contents (l.928), and itself will call Contract.spend will trigger the payment of baking fees (l.833).
    2. On the other side (right in the figure), the function apply_manager_contents_list (l.1331) makes successive nested calls to apply_manager_contents_list_rec (l.987), then apply_manager_contents (l.836), and finally apply_manager_operation_content (l.518). Finally, the amount of ꜩ10 is debited from Alice when calling Contract.spend (l.552) and then credited to Bob when calling Contract.credit (l.570).

Summary

To show how operations are handled in Tezos, we started from the command-line level using tezos-client to trigger a token transfer between two accounts. This client process occurs in the shell, outside of the procotol. The command itself is parsed and a manager operation is prepared. This operation is further wrapped up as an annotated manager operation in a single-item list of the one operation.

Furthermore, to prepare for the injection into the economic protocol, this list of annotated manager operations is transformed into a list of contents. A content is a wrapper for various operations, one of them is the manager operation. Finally, the operation is injected in injection.ml.

On the economic protocol side, the operation has been fetched, consisting of a signature and a protocol data, wrapped together with a shell header. This value is then sent to apply.ml which is in charge of calling the final low-level operations for funds debit/credit.


  1. See this short tutorial (search for “first a revelation”). By default (to save space, and to provide a little additional security) only hashes of public keys are recorded on the blockchain. But in order for Alice to sign a transaction from her account to Bob’s, Alice has to pay a one-time cost — ꜩ0.001259, in the example linked to in the tutorial above — for the full public key of her account to be recorded on the blockchain as a special key revelation operation. 

  2. You can fetch a list of them at the command line with tezos-client man.
    We use here a bespoke command-line argument parser developed in Nomadic Labs called Clic. Other parts of the codebase use other parsers, including Arg, and Cmdliner (used in tezos-node).