Nomadic Labs
Nomadic Labs

How to write a Tezos protocol

A Tezos node is parameterized by a software component called an economic protocol (or protocol for short). Different protocol implementations can be used to implement different types of blockchains.

This is the first post of a tutorial series on how to implement such a protocol. We will see how to write, compile, register, activate and use an extremely simple protocol. By doing so, we will also start to explore the interface between the protocol and the node (more specifically the shell component of the node). In later blog posts, we will gradually work our way up to a more realistic protocol.

In what follows, we suppose you have cloned the Tezos repository and we specifically look at revision 2d903a01 on the master branch. You should be already familiar with running a node in sandbox mode. All paths are relative to the root of this repository. All Bash commands are to be executed in sandbox mode.

Protocol Registration

A node can contain several economics protocols (they are said to be registered), but only one is activated at any given time.

We can query a node to know the registered protocols. Protocols are identified by a b58check hash. On the master branch, hashes are arbitrary values and do not depend on the actual code, but on production branches, they are hashes of the source code of the protocol.

$ tezos-admin-client list protocols

ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK
ProtoDemoNoopsDemoNoopsDemoNoopsDemoNoopsDemo6XBoYp
ProtoGenesisGenesisGenesisGenesisGenesisGenesk612im

The node in this example contains three protocols. They were statically linked (embedded) to the node at compile time. genesis is the protocol activated at start-up. alpha is the main Tezos protocol. demo_noops is a simple protocol without operations (hence the name no-ops) that we will use as our main example in this article.

Protocols can also be registered dynamically at run-time via an RPC (a.k.a. protocol injection). As an example, let us inject the test protocol /src/bin_client/test/proto_test_injection available as a test case in the Tezos code base.

$ tezos-admin-client inject protocol \
  src/bin_client/test/proto_test_injection

Injected protocol PshuejubNkeGc5nU2xwF7uGzCdujcZY7ZV3duFfffmG4z5SoMAM successfully

Under the hood, the protocol is compiled and sent to the node using the POST RPC /injection/protocol.

We can check that the protocol was successfully injected

$ tezos-admin-client list protocols

ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK
ProtoDemoNoopsDemoNoopsDemoNoopsDemoNoopsDemo6XBoYp
ProtoGenesisGenesisGenesisGenesisGenesisGenesk612im
PshuejubNkeGc5nU2xwF7uGzCdujcZY7ZV3duFfffmG4z5SoMAM

Lastly the node can also fetch a protocol over the network, for example before starting the test chain or activating a new amendment. Like in the previous case, once the code is downloaded, it will be compiled and dynamically linked.

Protocol Activation

Generally, a node starts its execution with the genesis protocol. genesis provides an operation to upgrade to a new protocol. Interestingly, upgradability is a feature of the protocol, not of the shell (though the shell can also force a protocol upgrade). Protocols may or may not be upgradable. The raison d’ĂȘtre of genesis is upgradability, alpha is upgradable by voting, while demo_noops is not upgradable.

The client command activate protocol is a shorthand to craft the activation operation offered by genesis to upgrade to a new protocol.

$ mkdir tmp && echo { } > tmp/protocol_parameters.json
$ tezos-client activate protocol \
  ProtoDemoNoopsDemoNoopsDemoNoopsDemoNoopsDemo6XBoYp \
  with fitness 5 and key activator and parameters tmp/protocol_parameters.json

Injected BMHupUhxqJqT

This command injects a so-called “activation block” to the blockchain (the command returns the prefix of the hash of this block). This block is the only one using the genesis protocol. It is a block that contains only one operation: the operation that activates the next protocol (in our case, demo_noops). The next block in the blockchain will be the first block using the activated protocol. Let us detail the parameters of this command:

  • ProtoDemoNoopsDemoNoopsDemoNoopsDemoNoopsDemo6XBoYp is the hash of the protocol to be activated. The protocol must be registered.
  • activator is an alias for an activation secret key. In this example, the corresponding public key has been passed as a parameter to tezos-node at startup (using the --sandbox argument). The alias is known to the client because it is added by default in sandbox mode.
  • 5 is the fitness of the activation block (more details below). It can be any number.
  • protocol_parameters.json is a file that contains protocol-specific initialization parameters. There are no parameters for demo_noops, so this file contains an empty json object (i.e. { }).

We suggest the reader runs the following two commands to inspect the first two blocks of the blockchain (in particular the values of the "protocol", "hash", "predecessor", "level", and "fitness" keys).

$ tezos-client rpc get /chains/main/blocks/head
$ tezos-client rpc get /chains/main/blocks/head~1

Protocol Structure and Compilation

Currently, the embedded protocols live in the tezos repository besides the rest of the code. They follow the naming convention proto_*. The code of proto_demo_noops is organized as shown below:

$ ls -R src/proto_demo_noops

src/proto_demo_noops/lib_protocol:
TEZOS_PROTOCOL main.ml main.mli dune.inc tezos-protocol-demo-noops.opam dune
tezos-embedded-protocol-demo-noops.opam

The protocol code resides in the lib_protocol directory. A protocol must define a TEZOS_PROTOCOL json file that contains the hash of the protocol and the list of OCaml modules.

$ cat src/proto_demo_noops/lib_protocol/TEZOS_PROTOCOL

{
    "hash": "ProtoDemoNoopsDemoNoopsDemoNoopsDemoNoopsDemo6XBoYp",
    "modules": ["Main"]
}

Besides the TEZOS_PROTOCOL and main.ml[i] files, the other files in lib_protocol are used for compiling the protocol, checking that it respects the restrictions explained below, and for linking it with the other components of the node.

Currently, protocols are compiled differently depending on whether they are embedded or injected. Injected protocols are compiled by the Tezos compiler embedded in tezos-node. Embedded protocols are compiled as OPAM libraries using the standard toolchain. We leave the practical details for a future post.

Protocol Interface

The economic protocol is a sandboxed component restricted in the following two ways.

  • It can only access modules defined by the protocol environment.

  • It must define a module Main which implements the interface Updater.PROTOCOL from src/lib_protocol_environment/sigs/v1/updater.mli. The shell interacts with the protocol through this interface.

In addition, just like any other node component, the protocol can define RPC services to interact with a client. We will address RPCs in a later post but there is no difficulty here apart from getting accustomed with the Tezos RPC library.

Environment

The environment of the protocol is a fixed set of OCaml modules (their signatures are declared in src/lib_protocol_environment/sigs/v1/), consisting in a carefully chosen subset of the OCaml standard library, plus specialized utility modules. This form of sandboxing the protocol ensures that the protocol code does not use unsafe functions. The Tezos documentation explains in more detail the restrictions the environment imposes.

Any datatype used by the protocol is defined in this environment (in particular, all modules or types mentioned below). The following is the list of Tezos-specific modules defined by the environment that we will be mentioning throughout this blog post: Block_header, Context, Operation, and Updater.

Updater.PROTOCOL

At a very high-level, a protocol must:

  1. implement protocol-specific types, such as the type of operations or protocol-specific block header data (in addition to the shell generic header),

  2. define under which conditions a block is a valid extension of the current blockchain, and define an ordering on blocks to arbitrate between concurrent extensions.

For instance, in a bitcoin-like protocol, the supported operations are transactions, and the block header data contains a proof of work (PoW) stamp. A block is valid if its operations are supported by enough funds and the PoW stamp is correct.

For the second point, at a conceptual level, the protocol defines the function apply: context -> block -> (context * fitness) option which is called whenever the node processes a block. context represents the protocol state and fitness is an integer used to compare blocks. The context is therefore protocol-specific, it may contain, for instance, a list of accounts and their balance. It must contain enough information to determine the validity of a new block. The fitness defines a total ordering between blocks (and therefore between chains). The option type is used here to represent block validity: the function returns None when the block is not valid, while if it is valid, it returns the block’s fitness and the updated protocol state, obtained after applying (the operations contained in) the block.

The signature PROTOCOL in module Updater captures these general ideas (explained in more detail in the Tezos white paper), but is slightly more complex, mostly for efficiency reasons. In this first article, we will cover only some aspects of the interface and we will cover it more fully in later posts.

Concretely, a context (represented by the type Context.t) is a disk-based immutable key-value store, namely, a map from string list to MBytes.t. Such a loosely structured datatype should accommodate most protocols. A fitness (represented by the type Fitness.t) is a list of byte arrays. A total order on blocks is obtained by comparing their fitness first by length and then lexicographically.

A Tezos block is composed of a block header (of type Block_header.t) and a list of operations. A block header has two parts, a protocol-independent shell header (described here) and a protocol-specific header, which is a byte array (with type MBytes.t). Similarly, operations (of type Operation.t) have a protocol independent shell header, and a protocol-specific header. For instance, Block_header.t is defined as follows.

  type t = {
    shell: shell_header ;
    protocol_data: MBytes.t ;
  }

As part of implementing the PROTOCOL signature, the protocol must in particular provide concrete types for the protocol-specific block header (type block_header_data) and operations (type operation_data). These types are private to the protocol. The only functions exported to the shell are encoders/decoders. This allows the shell to serialize these types, either in binary format or in json. Typically, the binary format is used for P2P communications, and json is used for human-readable RPCs. Here is an excerpt from the PROTOCOL signature where these types are declared:

  (** The version specific type of blocks. *)
  type block_header_data

  (** Encoding for version specific part of block headers.  *)
  val block_header_data_encoding: block_header_data Data_encoding.t

  (** A fully parsed block header. *)
  type block_header = {
    shell: Block_header.shell_header ;
    protocol_data: block_header_data ;
  }

Note the analogy between Block_header.t (the shell’s view of the block header) and block_header (the protocol’s view of the block header).

Several functions declared in the PROTOCOL signature realize together the apply functionality: begin_application, begin_partial_application, begin_construction, apply_operation, and finalize_block. A typical apply is represented by a call to begin_(application|construction), followed by a sequence of calls to apply_operation, one for each operation in the block, and finally a call to finalize_block. These functions use values with types validation_result and validation_state. Defined by the PROTOCOL signature, the type validation_result represents the result of a block application, and it is a record type that contains most notably a context and a fitness. validation_state is a protocol-defined datatype used as intermediary state between applications of operations. To understand the usage of these two types, it may be useful to consider the following simplification of the types of the five functions mentioned:

begin_application: Context.t -> block_header -> validation_state
begin_partial_application: Context.t -> block_header -> validation_state
begin_construction: Context.t -> ?protocol_data: block_header_data -> validation_state
apply_operation: validation_state -> operation ->
validation_state finalize_block: validation_state -> validation_result

We briefly describe the role of these five functions:

  • begin_application is used when validating a block received from the network.

  • begin_partial_application is used when the shell receives a block more than one level ahead of the current head (this happens, for instance, when synchronizing a node). This function should run quickly, as its main role is to reject invalid blocks from the chain as early as possible.

  • begin_construction is used by the shell when instructed to build a block and for validating operations as they are gossiped on the network. This two cases are distinguished by the optional protocol_data argument: when only validating operations the argument is missing, as there is no block header. In both of these cases, the operations are not (yet) part of a block which is why the function does not expect a shell block header.

  • apply_operation is called after begin_application or begin_construction, and before finalize_block, for each operation in the block or in the mempool, respectively. Its role is to validate the operation and to update the (intermediary) state accordingly.

  • finalize_block represents the last step in a block validation sequence. It produces the context that will be used as input for the validation of the block’s successor candidates.

Another important function in the PROTOCOL interface is init, which is called when the protocol is activated. It takes as parameters a context and the shell header of the last block of the previous protocol. The context is the context corresponding to this last block, which includes the protocol parameters given at activation time. It returns a validation_result, which contains a context that is prepared for the new protocol. Note that the new context may change the key-value structure of the store compared with the previous protocol. init is therefore responsible for making the migration of the context from the previous protocol to the current protocol.

Finally, let us emphasize that the protocol is a stateless component. Rather than maintaining a mutable state, it implements pure functions that that take a state as a parameter and return a new state. The shell is responsible to store this state between function calls.

Protocol demo_noops

The demo_noops protocol is very simple:

  • It has no operations (hence no-ops).
  • It does not update its state, context is never modified.
  • The fitness of a block is the block’s level (i.e. its height in the blockchain).

We now go through the types and functions which do not have a trivial definition. First, we simply choose to have a string as the block header. Therefore we define in main.ml:

  type block_header_data = string

  let block_header_data_encoding =
    Data_encoding.(obj1 (req "block_header_data" string))

For the encoding of the (protocol-specific) block header we rely on the data_encoding library, explained here.

As there are no operations, the type of an operation header is just unit. Similarly, as we do not use the other helper datatypes like block_header_metadata and `operation_receipt, we simply set these types to unit.

Next, we need to define a validation_state. We define it as record datatype that contains a context and a fitness, because these need to be passed to the validation_result returned by finalize_block.

  type validation_state = {
    context : Context.t ;
    fitness : Fitness.t ;
  }

Concerning the fitness, we assume that the protocol is instantiated from genesis. Note that this may not be the case in general. demo_noops could very well be instantiated from a previous protocol with a totally different format for the fitness. The protocol should be able to adjust to different fitness models. Here, however, we use the same fitness model as genesis (and alpha), where the fitness has the form xx:xxxxxxxxxxxxxxxx. That is, the fitness is a list of two byte arrays, the first one (xx, of length 1), representing the protocol version, and the second one encoding an int64 number (thus of length 8). Recall that there is only one block using the genesis protocol. For this block, the fitness’ first element is 00 and its second element encodes the integer given as the fitness parameter when activating the next protocol. In demo_noops, the first element is 01 and the second element represents the level.

The helper functions needed to implement the fitness are as follows:

  let version_number = "\001"

  let int64_to_bytes i =
    let b = MBytes.create 8 in
    MBytes.set_int64 b 0 i;
    b

  let fitness_from_level level =
    [ MBytes.of_string version_number ;
      int64_to_bytes level ]

The fitness of a new block is actually set in begin_construction, which has the following very simple implementation:

  let begin_construction
      ~chain_id:_
      ~predecessor_context:context
      ~predecessor_timestamp:_
      ~predecessor_level
      ~predecessor_fitness:_
      ~predecessor:_
      ~timestamp:_
      ?protocol_data:_ ()
    =
    let fitness = fitness_from_level Int64.(succ (of_int32 predecessor_level)) in
    ... (* output a log message *)
    return { context ; fitness }

The implementation of the other main functions is trivial: begin_application just builds the validation state from the predecessor context and the fitness from (the shell part of) the block header. begin_partial_application behaves like begin_application. apply_operation returns an error (however, it is never called), and finalize_block builds a validation result from the validation state by copying the context and the fitness, and setting default values for the other fields. Most functions also record a log message which allows one to see when these functions are called during the node’s execution. They also show how the fitness is updated.

Finally, this protocol does not define any RPC.

let rpc_services = RPC_directory.empty

Baking a block

We can build a rudimentary baker simply using the RPCs provided by the node. The RPC to inject a block is /injection/block. However, we need to provide an hexadecimal binary encoding of the block header. To obtain it we use the following RPC: /chains/main/blocks/head/helpers/forge_block_header. This RPC expects as argument a json representation of the block header. The json representation of the shell header (the protocol-independent part of the header) can be obtained with the following RPC: /chains/main/blocks/head/helpers/forge_block_header.

We will thus use the following RPCs to bake a block:

  1. /chains/main/blocks/head/helpers/preapply/block
  2. /chains/main/blocks/head/helpers/forge_block_header
  3. /injection/block

We call the first RPC with the protocol hash, the protocol block header, and the (empty) list of operations. The RPC service calls begin_construction and finalize_block of the demo_noops protocol and returns the built (but not injected) block in json format. Notice the json representation of the protocol block header data "block_header_data": "hello world") is the one we defined in our implementation of demo_noops.

$ tezos-client -p ProtoGenesisGenesisGenesisGenesisGenesisGenesk612im \
  rpc post /chains/main/blocks/head/helpers/preapply/block with \
  '{"protocol_data":
      {"protocol": "ProtoDemoNoopsDemoNoopsDemoNoopsDemoNoopsDemo6XBoYp",
       "block_header_data": "hello world"},
       "operations": []}'

{ "shell_header":
    { "level": 2, "proto": 1,
      "predecessor": "BLCJ5s7SGvMzmJd7Y7jbpuNiTN5c8yz9L4Q2GtBpLaJTAHKMEoz",
      "timestamp": "2019-06-21T15:35:37Z", "validation_pass": 0,
      "operations_hash":
        "LLoZS2LW3rEi7KYU4ouBQtorua37aWWCtpDmv1n2x3xoKi6sVXLWp",
      "fitness": [ "01", "0000000000000002" ],
      "context": "CoV3MLpgMM91DbHGuqGz7uwgmMYjnh7EQSsqt1CxPqvxQpU9pczA" },
  "operations": [] }

tezos-client can use protocol-specific extensions. By default, tezos-client tries to use the extension corresponding to the node’s protocol. In our case no such extension has been given, therefore we need to specify an extension using the -p XXX option, where XXX is a protocol hash.

Now we use the second RPC to obtain the binary encoding of the protocol block header:

$ tezos-client -p ProtoGenesisGenesisGenesisGenesisGenesisGenesk612im \
    rpc post /chains/main/blocks/head/helpers/forge_block_header \
    with '{"level": 2, "proto": 1,
           "predecessor": "BLCJ5s7SGvMzmJd7Y7jbpuNiTN5c8yz9L4Q2GtBpLaJTAHKMEoz",
           "timestamp": "2019-06-21T15:35:37Z", "validation_pass": 0,
           "operations_hash": "LLoZS2LW3rEi7KYU4ouBQtorua37aWWCtpDmv1n2x3xoKi6sVXLWp",
           "fitness": ["01", "0000000000000002"],
           "context": "CoV3MLpgMM91DbHGuqGz7uwgmMYjnh7EQSsqt1CxPqvxQpU9pczA",
           "protocol_data": "0000000b68656c6c6f20776f726c64"}'

{ "block": "0000000201b478f20b61340c9e8290d7b45edf057fd180891d0e0b290abc..." }

Notice the last field protocol_data. It must contain the binary-encoded block header data. Remember that we specified this encoding in the protocol with

let block_header_data_encoding =
  Data_encoding.(obj1 (req "block_header_data" string))

We can compute the binary encoding on the client side, for instance using the Data_encoding library, or by writing the encoder in a different language using the public specification of the Data_encoding library.

For this example, "0000000b68656c6c6f20776f726c64" is the binary encoding of "hello world".

Finally, the last RPC injects the block. After the block is validated by the protocol (the RPC service calls begin_application and finalize_block), the RPC returns its hash.

$ tezos-client -p ProtoGenesisGenesisGenesisGenesisGenesisGenesk612im \
  rpc post injection/block with \
   '{"data": "0000000201b478f20b61340c9e8290d7b45edf057fd180891d0e0b290abc...",
     "operations": []}'

"BM6qcDPhm57sXHv1js25qcy9WESah1C3qcpKn9y8bRZzpf8s7g8"

We can look at the newly created block:

$ tezos-client -p ProtoGenesisGenesisGenesisGenesisGenesisGenesk612im \
  rpc get /chains/main/blocks/head/

{ "protocol": "ProtoDemoNoopsDemoNoopsDemoNoopsDemoNoopsDemo6XBoYp",
  "chain_id": "NetXdQprcVkpaWU",
  "hash": "BMKeY5PDbm3acKDPUt7XnARkFBj5JoUDfMbBqYTYvWrGFHPt89a",
  "header":
    { "level": 2, "proto": 1,
      "predecessor": "BLCJ5s7SGvMzmJd7Y7jbpuNiTN5c8yz9L4Q2GtBpLaJTAHKMEoz",
      "timestamp": "2019-06-21T15:35:37Z", "validation_pass": 0,
      "operations_hash":
        "LLoZS2LW3rEi7KYU4ouBQtorua37aWWCtpDmv1n2x3xoKi6sVXLWp",
      "fitness": [ "01", "0000000000000002" ],
      "context": "CoV3MLpgMM91DbHGuqGz7uwgmMYjnh7EQSsqt1CxPqvxQpU9pczA",
      "block_header_data": "hello world" },
  "metadata":
    { "protocol": "ProtoDemoNoopsDemoNoopsDemoNoopsDemoNoopsDemo6XBoYp",
      "next_protocol": "ProtoDemoNoopsDemoNoopsDemoNoopsDemoNoopsDemo6XBoYp",
      "test_chain_status": { "status": "not_running" },
      "max_operations_ttl": 0, "max_operation_data_length": 0,
      "max_block_header_length": 100, "max_operation_list_length": [] },
  "operations": [] }

For completeness, we show below the node’s trace, that is, the output obtained when executing the following command:

$ ./src/bin_node/tezos-sandboxed-node.sh 1 --connections 1

After the node’s initialization, we see the following:

Generating a new identity... (level: 0.00)
Stored the new identity (idrCKw61WKnEHc2kuqd7BZvV1V39CN) into '/tmp/tezos-node.cJVCgcW0/identity.json'.
Jun 21 17:34:22 - node.main: Starting the Tezos node...
Jun 21 17:34:22 - node.main: No local peer discovery.
Jun 21 17:34:22 - node.main: Peer's global id: idrCKw61WKnEHc2kuqd7BZvV1V39CN
Jun 21 17:34:22 - main: shell-node initialization: bootstrapping
Jun 21 17:34:22 - main: shell-node initialization: p2p_maintain_started
Jun 21 17:34:22 - validator.block: Worker started
Jun 21 17:34:22 - validation_process.sequential: Initialized
Jun 21 17:34:22 - node.validator: activate chain NetXdQprcVkpaWU
Jun 21 17:34:22 - validator.chain_1: Worker started for NetXdQprcVkpa
Jun 21 17:34:22 - prevalidator.NetXdQprcVkpa.ProtoGenesis_1: Worker started for NetXdQprcVkpa.ProtoGenesis
Jun 21 17:34:22 - node.main: Starting a RPC server listening on ::ffff:127.0.0.1:18731.
Jun 21 17:34:22 - node.main: The Tezos node is now running!

After the protocol activation command, we further see the following output:

Jun 21 17:34:53 - demo-noops: init: fitness = 00::0000000000000005
Jun 21 17:34:53 - demo-noops: init: fitness = 00::0000000000000005
Jun 21 17:34:54 - validator.block: Block BLCJ5s7SGvMzmJd7Y7jbpuNiTN5c8yz9L4Q2GtBpLaJTAHKMEoz successfully validated
Jun 21 17:34:54 - validator.block: Pushed: 2019-06-21T15:34:53-00:00, Treated: 2019-06-21T15:34:53-00:00, Completed: 2019-06-21T15:34:54-00:00
Jun 21 17:34:54 - prevalidator.NetXdQprcVkpa.ProtoDemoNoo_1: Worker started for NetXdQprcVkpa.ProtoDemoNoo
Jun 21 17:34:54 - demo-noops: begin_construction (mempool): pred_fitness = 00::0000000000000005  constructed fitness = 01::0000000000000002
Jun 21 17:34:54 - prevalidator.NetXdQprcVkpa.ProtoGenesis_1: Worker terminated [NetXdQprcVkpa.ProtoGenesis]
Jun 21 17:34:54 - validator.chain_1: Update current head to BLCJ5s7SGvMzmJd7Y7jbpuNiTN5c8yz9L4Q2GtBpLaJTAHKMEoz (fitness 00::0000000000000005), same branch
Jun 21 17:34:54 - validator.chain_1: Pushed: 2019-06-21T15:34:54-00:00, Treated: 2019-06-21T15:34:54-00:00, Completed: 2019-06-21T15:34:54-00:00

After the first RPC, we obtain:

Jun 21 17:35:37 - demo-noops: begin_construction (block): pred_fitness = 00::0000000000000005  constructed fitness = 01::0000000000000002
Jun 21 17:35:37 - demo-noops: finalize_block: fitness = 01::0000000000000002

There is no output corresponding to the second and third RPCs. After the fourth and last RPC, we see the following output:

Jun 21 17:38:35 - demo-noops: begin_application: pred_fitness = 00::0000000000000005  block_fitness = 01::0000000000000002
Jun 21 17:38:35 - demo-noops: finalize_block: fitness = 01::0000000000000002
Jun 21 17:38:35 - validator.block: Block BM6qcDPhm57sXHv1js25qcy9WESah1C3qcpKn9y8bRZzpf8s7g8 successfully validated
Jun 21 17:38:35 - validator.block: Pushed: 2019-06-21T15:38:35-00:00, Treated: 2019-06-21T15:38:35-00:00, Completed: 2019-06-21T15:38:35-00:00
Jun 21 17:38:35 - demo-noops: finalize_block: fitness = 01::0000000000000002
Jun 21 17:38:35 - demo-noops: begin_construction (mempool): pred_fitness = 01::0000000000000002  constructed fitness = 01::0000000000000003
Jun 21 17:38:35 - prevalidator.NetXdQprcVkpa.ProtoDemoNoo_1: switching to new head BM6qcDPhm57sXHv1js25qcy9WESah1C3qcpKn9y8bRZzpf8s7g8
Jun 21 17:38:35 - prevalidator.NetXdQprcVkpa.ProtoDemoNoo_1: Pushed: 2019-06-21T15:38:35-00:00, Treated: 2019-06-21T15:38:35-00:00, Completed: 2019-06-21T15:38:35-00:00
Jun 21 17:38:35 - validator.chain_1: Update current head to BM6qcDPhm57sXHv1js25qcy9WESah1C3qcpKn9y8bRZzpf8s7g8 (fitness 01::0000000000000002), same branch
Jun 21 17:38:35 - validator.chain_1: Pushed: 2019-06-21T15:38:35-00:00, Treated: 2019-06-21T15:38:35-00:00, Completed: 2019-06-21T15:38:35-00:00

Wrapping up with Python

Two python scripts summarize what we have covered in this post: . tests_python/tests/test_injection.py . tests_python/examples/proto_demo_noops.py

This first one shows how to inject a new protocol in a node. The second script launches a node in sandbox, activates the demo_noops protocol, and bakes a block. See here for more details about how to run Python scripts, including installation instructions.

Excerpt from tests_python/examples/proto_demo_noops.py:

with Sandbox(paths.TEZOS_HOME,
                constants.IDENTITIES,
                constants.GENESIS_PK,
                log_dir='tmp') as sandbox:
    # launch a sandbox node
    sandbox.add_node(0)
    client = sandbox.client(0)

    protocols = client.list_protocols()
    assert PROTO_DEMO in protocols

    parameters = {}
    client.activate_protocol_json(PROTO_DEMO, parameters, key='activator',
                                    fitness='1')

    head = client.rpc('get', '/chains/main/blocks/head/', params=PARAMS)
    # current protocol is still genesis and level == 1
    assert head['header']['level'] == 1
    assert head['protocol'] == PROTO_GENESIS

    time.sleep(1)

    # bake a block for new protocol, using fours RPCs:
    # - helpers/preapply/block builds the block
    # - helpers/forge_block_header encodes the whole block header
    # - /injection/block injects it
    message = "hello world"

    data = {"protocol_data":
            {"protocol": PROTO_DEMO, "block_header_data": message},
            "operations": []}
    block = client.rpc(
        'post',
        '/chains/main/blocks/head/helpers/preapply/block',
        data=data,
        params=PARAMS)

    protocol_data = {'block_header_data': message}
    encoded = forge_block_header_data(protocol_data)

    shell_header = block['shell_header']
    shell_header['protocol_data'] = encoded
    encoded = client.rpc(
        'post',
        '/chains/main/blocks/head/helpers/forge_block_header',
        data=shell_header,
        params=PARAMS)

    inject = {'data': encoded['block'], 'operations': []}
    client.rpc('post', '/injection/block', data=inject, params=PARAMS)

    head = client.rpc('get', '/chains/main/blocks/head/', params=PARAMS)
    assert head['header']['level'] == 2

Conclusion

We saw how to write, compile, register, activate and use a simple protocol. In the next blog post, we’ll make this protocol more realistic by adding operations and block validation. We’ll also improve the client interface by defining RPCs in the protocol, as well as extending the tezos-client command-line interface with protocol-specific commands.


Receive Updates

ATOM

Contacts