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 totezos-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 fordemo_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 interfaceUpdater.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:
-
implement protocol-specific types, such as the type of operations or protocol-specific block header data (in addition to the shell generic header),
-
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 optionalprotocol_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 afterbegin_application
orbegin_construction
, and beforefinalize_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:
/chains/main/blocks/head/helpers/preapply/block
/chains/main/blocks/head/helpers/forge_block_header
/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 RPC. After the third 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.