How to write a Tezos protocol - Part 1#
Note
This tutorial is based on a blog published in 2019, but has been significantly updated to fit contemporary protocol development.
In this tutorial 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 the second tutorial, we will work our way to a protocol maintaining and updating some state.
In what follows, we suppose you have built Octez from sources, as described in Building Octez from source. You should be already familiar with the Sandboxed mode. All paths are relative to the root of the Octez repository. All shell commands are to be executed in sandbox mode.
That is, in your current terminal, do:
eval `./src/bin_client/octez-init-sandboxed-client.sh 1`
And in another terminal, do:
./src/bin_node/octez-sandboxed-node.sh 1 --connections 0
and leave the sandboxed node running.
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.
$ octez-admin-client list protocols
ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK
ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT
ProtoDemoNoopsDemoNoopsDemoNoopsDemoNoopsDemo6XBoYp
ProtoGenesisGenesisGenesisGenesisGenesisGenesk612im
...
The node in this example contains many protocols that 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.
$ octez-admin-client inject protocol src/bin_client/test/proto_test_injection
Injected protocol Ps7nS9ZwNxMvSRQpaFSmu1uTjDvvJhvbdu8fGGDgzninPygNbZz 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
$ octez-admin-client list protocols
ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK
...
Ps7nS9ZwNxMvSRQpaFSmu1uTjDvvJhvbdu8fGGDgzninPygNbZz
...
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.
$ echo "{ }" > /tmp/protocol_parameters.json
$ octez-client activate protocol \
ProtoDemoNoopsDemoNoopsDemoNoopsDemoNoopsDemo6XBoYp \
with fitness 5 and key activator and parameters /tmp/protocol_parameters.json
Injected BMX1tHrYDdWC
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:
ProtoDemoNoopsDemoNoopsDemoNoopsDemoNoopsDemo6XBoYpis the hash of the protocol to be activated. The protocol must be registered.activatoris an alias for an activation secret key. In this example, the corresponding public key has been passed as a parameter tooctez-nodeat startup (using the--sandboxargument). The alias is known to the client because it is added by default in sandbox mode.5is the fitness of the activation block (more details below). It can be any number.protocol_parameters.jsonis a file that contains protocol-specific initialization parameters. There are no parameters fordemo_noops, so this file contains an empty json object (i.e.{ }).
Run 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).
$ curl localhost:18731/chains/main/blocks/head | jq
$ curl localhost:18731/chains/main/blocks/head~1 | jq
Note
After activating demo_noops, the command
octez-client rpc get /chains/main/blocks/head will fail with
Fatal error: unknown protocol version. This happens because
octez-client tries to load protocol-specific CLI extensions by
querying the head block’s next_protocol. Since demo_noops does
not register a client commands module (it has no lib_client
directory), the client does not know how to handle this protocol.
To work around this, either force the client to use a known protocol’s
extensions with the -p flag:
$ octez-client -p ProtoGenesisGenesisGenesisGenesisGenesisGenesk612im \
rpc get /chains/main/blocks/head
or use curl directly as shown above.
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
lib_protocol/
src/proto_demo_noops/lib_protocol:
TEZOS_PROTOCOL dune main.ml main.mli
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
{
"expected_env_version": 17,
"hash": "ProtoDemoNoopsDemoNoopsDemoNoopsDemoNoopsDemo6XBoYp",
"modules": ["Main"]
}
Besides the TEZOS_PROTOCOL and main.ml[i] files, the dune file
in lib_protocol is 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.
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
Mainwhich implements the interfaceUpdater.PROTOCOLfrom src/lib_protocol_environment/sigs/v17/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 the second tutorial 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/v17/), 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. Protocol Environment 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 tutorial: 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
(see src/lib_protocol_environment/sigs/v17/updater.mli)
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
tutorial, we will cover only some aspects of the interface and we
will cover it more fully in second tutorial.
Concretely, a context (represented by the type Context.t) is a
disk-based immutable key-value store, namely, a map from string list
to bytes. 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
and a protocol-specific header, which is a byte array (with type
bytes). 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: bytes ;
}
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).
Note
The original blog posts on which these tutorials are based described
a 5-function model (begin_application, begin_partial_application,
begin_construction, apply_operation, finalize_block).
Since environment V7 (protocol Lima, 2022), the PROTOCOL interface
has been refactored into a split validation/application model, as
described below. See Protocol Lima for historical
context on this change.
Several functions declared in the PROTOCOL signature realize
together the apply functionality. Block processing is parameterized
by a mode type that indicates the circumstances:
type mode =
| Application of block_header
| Partial_validation of block_header
| Construction of {
predecessor_hash : Block_hash.t;
timestamp : Time.t;
block_header_data : block_header_data;
}
| Partial_construction of {
predecessor_hash : Block_hash.t;
timestamp : Time.t;
}
Processing is split into two parallel pipelines — validation (lightweight, no context writes) and application (executes effects, updates the context). Here are simplified types:
(* Validation pipeline *)
begin_validation : Context.t -> Chain_id.t -> mode ->
predecessor:Block_header.shell_header -> validation_state tzresult Lwt.t
validate_operation : validation_state -> Operation_hash.t ->
operation -> validation_state tzresult Lwt.t
finalize_validation : validation_state -> unit tzresult Lwt.t
(* Application pipeline *)
begin_application : Context.t -> Chain_id.t -> mode ->
predecessor:Block_header.shell_header -> application_state tzresult Lwt.t
apply_operation : application_state -> Operation_hash.t ->
operation -> (application_state * operation_receipt) tzresult Lwt.t
finalize_application : application_state ->
Block_header.shell_header option ->
(validation_result * block_header_metadata) tzresult Lwt.t
The mode type replaces the old separate begin_* functions:
Application— Standard processing of a block received from the network.Partial_validation— Quick rejection of obviously invalid alternate branches. Only validation (not application) makes sense in this mode.Construction— Building a new block. Theblock_header_datais a prototype (e.g. with a placeholder signature) since the final value is not yet known.Partial_construction— Minimal construction for validating individual operations, used by the mempool and some RPCs.
The motivation for the validation/application split is pipelining: the shell can validate a block cheaply (checking structural correctness, signatures, basic preconditions) without committing any context changes, advertise the valid block to peers, and only then proceed to the expensive application phase. See The validation subsystem for more details on the shell’s two-step block validator.
The PROTOCOL signature also defines two distinct state types:
validation_state (used by the validation pipeline) and
application_state (used by the application pipeline). For simple
protocols like the demos, these can be the same type.
The signature also requires protocols to implement a Mempool module
that provides the shell with a protocol-level API for managing pending
operations. We will cover this in the second tutorial.
Another important function in the PROTOCOL interface is init,
which is called when the protocol is activated. It takes as parameters
the chain id, 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 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, whose sources are in src/proto_demo_noops, is very simple:
It has no operations (hence no-ops).
It does not update its state,
contextis 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 Plain)))
For the encoding of the (protocol-specific) block header we rely on the
data_encoding library, see The data_encoding library.
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 (and an
application_state, which in this protocol is the same type).
We define it as a record datatype that contains a context and a
fitness, because these need to be passed to the validation_result
returned by finalize_application.
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 is
a list of five byte arrays. The first element (of length 1) represents
the protocol version, the second encodes the level as an int32
number (of length 4), and three additional elements are present for
compatibility with the alpha fitness format. In demo_noops, the
version element is \002.
The helper functions needed to implement the fitness are as follows:
let fitness_from_level level =
let version_number = "\002" in
let int32_to_bytes i =
let b = Bytes.make 4 '\000' in
TzEndian.set_int32 b 0 i ;
b
in
[
Bytes.of_string version_number;
int32_to_bytes level ;
Bytes.empty ;
int32_to_bytes (-1l) ;
int32_to_bytes 0l ;
]
The fitness of a new block is set in begin_validation (in
Construction or Partial_construction modes), which has the
following implementation:
let begin_validation context _chain_id mode
~(predecessor : Block_header.shell_header) =
let fitness =
match mode with
| Application block_header | Partial_validation block_header ->
block_header.shell.fitness
| Construction _ | Partial_construction _ ->
fitness_from_level Int32.(succ predecessor.level)
in
return {context; fitness}
let begin_application = begin_validation
In demo_noops, begin_application is simply an alias for
begin_validation since there is no meaningful difference between
the two phases for this trivial protocol.
validate_operation and apply_operation both return an error
(since there are no valid operations). finalize_validation returns
unit, and finalize_application builds a validation_result
from the application 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.
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_validation and finalize_validation 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.
$ octez-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": [] }
octez-client can use protocol-specific extensions. By default,
octez-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 (mind replacing the fields “predecessor”, “timestamp”, “operations_hash”, “fitness” (the whole list), and “context” with the values output by the previous command):
$ octez-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 Plain)))
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 (mind replacing the field “data” with the value output by the previous command in field “block”). After the block is validated by
the protocol (the RPC service calls begin_validation,
begin_application, finalize_validation, and
finalize_application), the RPC returns its hash.
$ octez-client -p ProtoGenesisGenesisGenesisGenesisGenesisGenesk612im \
rpc post injection/block with \
'{"data": "0000000201b478f20b61340c9e8290d7b45edf057fd180891d0e0b290abc...",
"operations": []}'
"BM6qcDPhm57sXHv1js25qcy9WESah1C3qcpKn9y8bRZzpf8s7g8"
We can look at the newly created block:
$ octez-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 at startup:
Nov 24 13:56:37.284 NOTICE | the node configuration has been successfully
Nov 24 13:56:37.284 NOTICE | validated.
Nov 24 13:56:37.285 NOTICE | read identity file
Nov 24 13:56:37.285 NOTICE | starting the Octez node 21.0~rc3+dev (e1573177)
Nov 24 13:56:37.383 NOTICE | disabled local peer discovery
Nov 24 13:56:37.384 NOTICE | p2p initialization: bootstrapping
Nov 24 13:56:37.397 NOTICE | p2p initialization: p2p_maintenance_started
Nov 24 13:56:37.809 NOTICE | external validator initialized
Nov 24 13:56:37.809 NOTICE | initializing brasaia context at
Nov 24 13:56:37.809 NOTICE | /tmp/tezos-node[...]
Nov 24 13:56:38.469 WARN | protocol
Nov 24 13:56:38.469 WARN | PsGXQmSv1UDDm2brdEFsuJCHjajS9Cv8mp44w93wNS2TpXFSvTC
Nov 24 13:56:38.469 WARN | won't be stored: wrong hash
Nov 24 13:56:38.770 NOTICE | activate chain NetXdQprcVkpaWU
Nov 24 13:56:38.770 NOTICE | chain is bootstrapped
Nov 24 13:56:38.770 NOTICE | synchronisation status: synced
Nov 24 13:56:38.771 NOTICE | starting local RPC server on
Nov 24 13:56:38.771 NOTICE | ::ffff:127.0.0.1:18731 (acl = AllowAll)
Nov 24 13:56:38.771 NOTICE | the Tezos node is now running
Note
The warning above about a wrong hash for a past embedded protocol can be safely ignored.
After the protocol activation command, the shell-level traces show:
Nov 24 13:57:37.232 NOTICE | initializing protocol ProtoDemoNoo...
Nov 24 13:57:37.233 NOTICE | block at level 1 successfully pre-applied in 748us
Nov 24 13:57:37.237 NOTICE | the protocol table was updated: protocol
Nov 24 13:57:37.237 NOTICE | ProtoDemoNoo (level 1) was activated on block
Nov 24 13:57:37.237 NOTICE | BLrgJzwN4M68uCCGrkYXRcKUqxpDewMWM1gxUwyZ7J8sgYZCuvZ
Nov 24 13:57:37.237 NOTICE | (level 1)
Nov 24 13:57:37.237 NOTICE | head is now
Nov 24 13:57:37.237 NOTICE | BLrgJzwN4M68uCCGrkYXRcKUqxpDewMWM1gxUwyZ7J8sgYZCuvZ
Nov 24 13:57:37.237 NOTICE | (1)
And when a block is baked:
Nov 24 13:58:45.304 NOTICE | block at level 2 successfully pre-applied in 568us
Nov 24 14:00:35.405 NOTICE | head is now
Nov 24 14:00:35.405 NOTICE | BKzSsYtHSxgPWgyw7YktHTrddzgGQgKr3bLAm3NjxwMCyrFR7qH
Nov 24 14:00:35.405 NOTICE | (2)
Note
Unlike demo_counter (that we’ll see in the second part, which uses Logging.log to produce
protocol-specific traces prefixed with demo-counter:), the
demo_noops protocol has no Logging.log calls in its code.
Therefore, the only traces you will see come from the shell (the
validator and prevalidator subsystems), not from the protocol itself.
If you want more detailed traces from the shell, you can set the log
level using the TEZOS_LOG environment variable before starting the
node. For example:
TEZOS_LOG="validator:debug;prevalidator:debug" \
./src/bin_node/octez-sandboxed-node.sh 1 --connections 0
This will show the validator and prevalidator subsystems calling into
the protocol’s begin_validation, finalize_validation,
begin_application, and finalize_application functions, even
though the protocol itself remains silent.
Protocol tests#
The tests we covered are also implemented in the testsuite of the demo protocol Noop, and can be executed with:
dune exec tezt/tests/main.exe -- --title demo_noops
The test is contained in module Demo_noops within file tezt/tests/demo_protocols.ml.
The test launches a node in sandbox, activates the demo_noops
protocol, and bakes a block.
Conclusion#
We saw how to write, compile, register, activate and use a simple
protocol. In the next tutorial, 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
octez-client command-line interface with protocol-specific commands.