How to write a Tezos protocol - Part 2#
This is the second part of a couple of tutorials on how to implement a Tezos protocol.
In the first part, we saw how to write, compile, register, activate and use an extremely simple protocol. We also looked at the interface between the protocol and the shell.
In this tutorial, we consider a new protocol called demo_counter which
extends demo_noops from the first part in several ways.
Blocks can contain simple operations, whose effects update the blockchain state.
It is parameterized by protocol parameters passed at activation time.
It defines REST services (a.k.a. RPCs), in addition to the generic ones already available from the shell.
It defines a client library, extending
octez-clientwith protocol-specific commands.
A large part of this tutorial is devoted to the client library. While this library is not part of the protocol per se, it is needed if we want to communicate with the node in any meaningful way.
This protocol and the client library also make use of additional libraries, such as command-line parsing tools, error monads, RPCs… Describing them in detail would be beyond the scope of this tutorial, but we try to provide enough information to keep the tutorial self-contained.
demo_counter can be found in the Octez repository in src/proto_demo_counter/.
As in the first part, we suppose you have built Octez from sources, as described in Building Octez from source, that you have initialized a sandboxed client, and started a fresh sandboxed node.
Protocol demo_counter#
The protocol is referred to by the hash
ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT. We can check it
is indeed known by the node.
$ octez-admin-client list protocols
ProtoALphaALphaALphaALphaALphaALphaALphaALphaDdp3zK
ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT
ProtoDemoNoopsDemoNoopsDemoNoopsDemoNoopsDemo6XBoYp
ProtoGenesisGenesisGenesisGenesisGenesisGenesk612im
...
The demo_counter protocol, whose sources are in src/proto_demo_counter, defines several modules:
ls src/proto_demo_counter/lib_protocol/*.mli
lib_protocol/apply.mli lib_protocol/main.mli lib_protocol/receipt.mli
lib_protocol/error.mli lib_protocol/proto_operation.mli lib_protocol/services.mli
lib_protocol/header.mli lib_protocol/proto_params.mli lib_protocol/state.mli
Most protocol-specific types required in Main are now defined in
separate modules.
block_header_datais defined asHeader.t,operation_receiptis defined asReceipt.t,operation_datais defined asProto_operation.t.
As for demo_noops, block_header_data is still a string, and
fitness is defined as the height of the chain. However, demo_counter
uses a different fitness format than demo_noops: it uses version
number \255 (ff in hex) and encodes the level as a 64-bit
integer, with a 5-element fitness of the form
ff::00::00::00::LEVEL. This higher version number allows testing
migration from alpha to demo_counter.
More interesting are the protocol operations and the operation receipt.
The protocol defines three operations in Proto_operation.t, which
act on a state State.t stored in the protocol context. As seen in
the first part, in the Tezos model each operation is applied to a
context and can produce a new context, the context is a map that can be
seen as the current state of the blockchain. For proto_counter, the
context maps a key "state" to a serialized form of State.t.
State is simply a couple of nonnegative counters (which we can also
view as the balances of two accounts).
type t = { a : int32; b : int32 }
Operations are defined in Proto_operation as
type t =
| IncrA
| IncrB
| Transfer of Int32.t (* transfer from A to B, possibly a negative amount *)
The module Apply defines a function
val apply : State.t -> Proto_operation.t -> State.t option
that applies the operation as expected. Some operations may be invalid
(and in this case apply returns None). For instance, Transfer
checks the State.invariant (both counters must remain nonnegative)
and returns None if the transfer would violate it. Note that
IncrA and IncrB do not check for overflow — they simply call
Int32.add, which wraps around silently in OCaml.
Operation application is defined by the function
Main.apply_operation.
val apply_operation :
application_state ->
Operation_hash.t ->
operation ->
(application_state * operation_receipt) tzresult Lwt.t
The implementation uses a helper function apply_operation_aux
that is shared with validate_operation:
let apply_operation_aux application_state operation =
let open Lwt_result_syntax in
let {context; fitness} = application_state in
let*! state = State.get_state context in
match Apply.apply state operation.protocol_data with
| None -> Error_monad.tzfail Error.Invalid_operation
| Some state ->
let*! context = State.update_state context state in
return {context; fitness}
let apply_operation application_state _oph operation =
let open Lwt_result_syntax in
Logging.log Notice "apply_operation" ;
let* application_state = apply_operation_aux application_state operation in
let receipt = Receipt.create "operation applied successfully" in
return (application_state, receipt)
This is quite straightforward. If the application succeeds, fitness is
left unchanged and the resulting context contains the updated state.
This function also returns a receipt that describes the effect of the
operation. In this protocol, the receipt is simply a string, but it
could be more descriptive. If the application fails, an error is
returned via an error monad. All
protocol errors are registered in Error.
Note
demo_counter reuses the same helper (apply_operation_aux) for both validate_operation and apply_operation only for simplicity, meaning validation also writes to the context. A production protocol should keep validation lightweight and context-write-free.
Protocol parameters#
We saw in the first part that when a protocol is activated, we can pass
to it initialization parameters through a JSON value. This value is
provided by a user through a file argument, e.g.,
protocol_parameters.json, to the activation command. The
demo_noops protocol did not take advantage of this feature, but
demo_counter uses a JSON value of the form
{"init_a": A, "init_b": B}, where A and B are the initial
values of the counters.
The type of the protocol parameters and their encoding are defined in
Proto_params.
By convention, the protocol parameters are stored in the context under
the key "protocol_parameters". The activation operation of the
genesis protocol sets the parameters under this key, and they are
retrieved by demo_counter in Main.init.
RPC Services#
The protocol implements two services.
/chains/main/blocks/head/counter/areturns the value of countera/chains/main/blocks/head/counter/breturns the value of counterb
Services rely on the RPC_* modules accessible through the
protocol environment.
Ultimately, these modules are implemented by the octez-libs.rpc library.
Services are registered by the function Services.rpc_services, which
is called by Main.rpc_services at protocol activation.
val rpc_services : Updater.rpc_context RPC_directory.t
let rpc_services = Services.rpc_services
Compilation#
Two libraries are compiled from the protocol code.
tezos-protocol-demo-counteris linked to the client library (see below),tezos-embedded-protocol-demo-counteris linked to the node (see src/bin_node/dune).
Recall from the first part that the protocol hash and modules are given
in TEZOS_PROTOCOL.
$ cat src/proto_demo_counter/lib_protocol/TEZOS_PROTOCOL
{
"expected_env_version": 17,
"hash": "ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT",
"modules": ["Error", "Proto_params", "Header", "State",
"Proto_operation", "Receipt", "Apply", "Services", "Main"]
}
The compilation steps are given in a generic dune file common to all
protocols, and a protocol-specific file dune.inc that can be
generated from the TEZOS_PROTOCOL file.
Client library#
The client and the node interact using RPCs. In theory, we could write a
client for demo_counter from scratch in any language, but it is
convenient to simply extend octez-client with a protocol-specific
client library. Hence, we can keep using generic features of
octez-client, such as wallet management, and simply add new commands
specific to the new protocol. Moreover, we can use well-tested OCaml
libraries to conveniently call shell RPCs, and we can access to the
protocol code from the client, for instance to access some of its
datatypes or services.
For demo_counter, the client library is
tezos-client-demo-counter and it is defined in
src/proto_demo_counter/lib_client/. It is linked to octez-client
(see src/bin_client/dune). It is composed of four modules
Client_proto_args, Client_proto_commands, Client_proto_main,
Protocol_client_context which we will describe as we go.
We can check that demo_counter is indeed known to the client.
$ octez-admin-client list understood protocols | grep -i demo
ProtoDemoCou
Note that Proto_demo_noops isn’t in the list since it doesn’t have a
client library.
User interface#
demo_counter adds a few new commands.
$ octez-client -p ProtoDemoCou man
...
Commands for protocol Demo_counter:
bake <message>
Bake a block
<message>: message in block header
increment a
Increment A
increment b
Increment B
transfer <amount>
Transfer from A to B
<amount>: amount taken from A and given to B (possibly negative)
get a
Get A counter
get b
Get B counter
...
Client/Node interaction#
Typically, the client library interacts with the node in two ways.
using shell services (through the
octez-shell-libs.shell-serviceslibrary),using protocol services (through the protocol library, i.e.
tezos-protocol-demo-counter).
Let us see how to use these libraries in practice.
Library tezos-protocol-demo-counter#
The library tezos-protocol-demo-counter contains the protocol code
and its environment. The modules of the protocol are grouped in a module
Protocol. The environment is accessible from module
Protocol.Environment.
Typically, we want to use the datatypes defined in the protocol. For
instance, to build blocks, we need access to the type of operations and
block header data. We also want to access protocol-defined services. In
our case, we can get the counter values using the client stub
Services.get_counter.
Remark that although it is feasible, the client code should not use protocol functions who read or write the protocol context.
Library octez-shell-libs.shell-services#
This library defines client stubs to call shell RPC services. The
demo_counter client library uses injection services and block
services.
As an example, consider the (slightly simplified) function
Shell_services.Injection.block defined in module
Injection_services in src/lib_shell_services/.
val block :
#simple ->
?async:bool ->
?force:bool ->
?chain:Chain_services.chain ->
Bytes.t ->
Operation.t list list ->
Block_hash.t tzresult Lwt.t
#simpleis the RPC context, which identifies the server, deals with the networking aspects of the call, and serializes the transmitted values.Bytes.tcontains the encoded block header.Operation.tis the generic shell operation type.Block_hash.tis the hash of the injected block.
This function calls the service /injection/block and serializes the
parameters and the returned value as expected.
The client library uses two injection services.
Shell_services.Injection.blockto call/injection/block,Shell_services.Injection.operationto call/injection/operation.
The other family of services, block services, is defined as a functor
parameterized by a protocol type Block_services.PROTO defined in
lib_shell_services. This allows the stubs to deserialize the JSON
values returned by the services, and to return the actual protocol types
to the client.
In client_proto_commands.ml, we instantiate the functor
Block_services.Make with the Protocol module (there are two
occurrences because we need to provide a module for both the current and
the next protocol).
module Demo_block_services = Block_services.Make(Protocol)(Protocol)
The client library uses the following stubs:
Demo_block_services.hashto call/chains/main/blocks/head/hashDemo_block_services.Mempool.pending_operationsto call/chains/main/mempool/pending_operationsDemo_block_services.Helpers.Preapply.blockto call/chains/main/blocks/head/helpers/preapply/blockDemo_block_services.Helpers.Preapply.operationsto call/chains/main/blocks/head/helpers/preapply/operations/
Commands implementation#
Commands are implemented with the help of several libraries.
octez-libs.clicis a command-line parsing library.octez-shell-libs.client-commandsprovides the registration function for new commands.octez-shell-libs.client-basedefines notably theClient_context.fullclass, which contains the client context (e.g. wallet, printing facilities, RPC context…).
The commands’ syntax is defined in the modules Client_proto_main and
Client_proto_args. Commands are registered using the function
Client_command.register.
Commands behavior is implemented in module Client_proto_commands. A
command implementation may use a value of type Client_context.full,
which is provided by the registration function. Through this object,
commands can access the client and RPC contexts.
More precisely, our command implementations use a
Protocol_client_context.full object, which is a specialized version
of Client_context.full, defined in Protocol_client_context.
Let us have a closer look to the block baking command
Client_proto_commands.bake.
let bake (cctxt : Protocol_client_context.full) message : unit tzresult Lwt.t =
let open Lwt_result_syntax in
let* {validated; _} =
Demo_block_services.Mempool.pending_operations cctxt ()
in
let operations = List.map snd validated in
let block_header_data = Header.create message in
let* shell, preapply_result =
Demo_block_services.Helpers.Preapply.block
cctxt
[operations]
~protocol_data:block_header_data
in
let block_header_data_encoded =
Data_encoding.Binary.to_bytes_exn Header.encoding block_header_data
in
let header : Block_header.t =
{shell; protocol_data = block_header_data_encoded}
in
let header_encoded =
Data_encoding.Binary.to_bytes_exn Block_header.encoding header
in
let preapply_result =
WithExceptions.Option.get ~loc:__LOC__ @@ List.hd preapply_result
in
let operations = [List.map snd preapply_result.applied] in
let* block_hash =
Shell_services.Injection.block cctxt header_encoded operations
in
let*! () =
cctxt#message "Injected block %a" Block_hash.pp_short block_hash
in
return_unit
First, it retrieves the applied operations from the mempool using
Demo_block_services.Mempool.pending_operations. It then uses the
pre-apply service and ask the node to build a block based on the
proposed operations and block header data. The block is then encoded and
sent to the node through the injection service.
Sample execution of the protocol#
First we activate the protocol using protocol_parameters.json
defined as
{"init_a": 100, "init_b": 100}
$ octez-client -block genesis activate protocol \
ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT \
with fitness 1 and key activator and parameters protocol_parameters.json \
--timestamp 2019-07-05T14:30:35Z
Injected BLf2cXRZKsby
This bakes block of level 1, running protocol genesis, with
demo_counter scheduled for the next block.
$ octez-client rpc get /chains/main/blocks/head/metadata
{ "protocol": "ProtoGenesisGenesisGenesisGenesisGenesisGenesk612im",
"next_protocol": "ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT",
"test_chain_status": { "status": "not_running" }, "max_operations_ttl": 0,
"max_operation_data_length": 100, "max_block_header_length": 100,
"max_operation_list_length": [ { "max_size": 1000 } ] }
Although the head is a genesis block, demo_counter has already
been activated and we can bake an empty block using the bake command
from demo_counter client library.
$ octez-client bake '"This is block 2"'
Injected block BLrQqbn13Vrb
We can check that the block was baked properly, in particular the block
header data has been set as expected. We can also see the protocol state
State.t in the block metadata encoded as specified by
State.encoding.
$ octez-client rpc get /chains/main/blocks/head/
{ "protocol": "ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT",
"chain_id": "NetXdQprcVkpaWU",
"hash": "BLrQqbn13VrbzUprxQypzAg6fc7YmsHaZvGwGrHJk8a4eG6e11B",
"header":
{ "level": 2, "proto": 1,
"predecessor": "BLf2cXRZKsbygWJdtf1PBbrSg8yHkNK39bgoApvdbYBd1EX9ung",
"timestamp": "2019-07-05T14:30:36Z", "validation_pass": 1,
"operations_hash":
"LLoaGLRPRx3Zf8kB4ACtgku8F4feeBiskeb41J1ciwfcXB3KzHKXc",
"fitness": [ "ff", "00", "00", "00", "0000000000000002" ],
"context": "CoVpDgKDiWZ9xcodUFng1C8oGvfXEqBCD5XxQjB8Jrwptkx3vHUB",
"demo_block_header_data": "This is block 2" },
"metadata":
{ "protocol": "ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT",
"next_protocol": "ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT",
"test_chain_status": { "status": "not_running" },
"max_operations_ttl": 0, "max_operation_data_length": 100,
"max_block_header_length": 100,
"max_operation_list_length": [ { "max_size": 1000 } ], "demo_a": 100,
"demo_b": 100 }, "operations": [ [] ] }
We now inject three operations using client commands.
$ octez-client increment a
Operation receipt: operation applied successfully
Injected: op5gBsE7EMi7
$ octez-client increment b
Operation receipt: operation applied successfully
Injected: oo2YhBbAY8Vr
$ octez-client transfer 10
Operation receipt: operation applied successfully
Injected: opJFLuHR98tf
The operations are known to the node, they appear as applied in the node mempool.
$ octez-client rpc get /chains/main/mempool/pending_operations
{ "applied":
[ { "hash": "op45sL79jASRf41kpL5NDDbAUnQeTfwgZpVnZi1sXy4Cj5x18m9",
"branch": "BLa7SnHxjHqPTsGSE2fi8sHBm39u9g6Psd9qPZm4rJCqhzHdkSp",
"IncrA": {} },
{ "hash": "opV6ZMR2z2ZZUSjetzPTknPisjN6x5eFCQnAWhuKNic6GsiRLW7",
"branch": "BLa7SnHxjHqPTsGSE2fi8sHBm39u9g6Psd9qPZm4rJCqhzHdkSp",
"IncrB": {} },
{ "hash": "oo1a3gwKnXFqaHuhpgMb5x69wm3mwbid4hn8Ry1iX8jbvXzQQs7",
"branch": "BLa7SnHxjHqPTsGSE2fi8sHBm39u9g6Psd9qPZm4rJCqhzHdkSp",
"Transfer": 10 } ], "refused": [], "branch_refused": [],
"branch_delayed": [], "unprocessed": [] }
We bake the third block.
$ octez-client bake '"This is block 3"'
Injected block BLz4SrcTnBQU
We can see now that the three operations appear in the operations
section of the block, encoded as specified by
Proto_operation.encoding. The receipt for each operation also
appears in this section.
$ octez-client rpc get /chains/main/blocks/head/
{ "protocol": "ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT",
"chain_id": "NetXdQprcVkpaWU",
"hash": "BLz4SrcTnBQUiXXks1FTzGR9d5MsX6F1mhZ2g23bHTcaQwJbk3S",
"header":
{ "level": 3, "proto": 1,
"predecessor": "BLrQqbn13VrbzUprxQypzAg6fc7YmsHaZvGwGrHJk8a4eG6e11B",
"timestamp": "2019-07-05T14:30:38Z", "validation_pass": 1,
"operations_hash":
"LLoZctr62cmk2pvVu2dqX5nv8rA7PHi3xRNGe6mbqmtAQPKtwHuKK",
"fitness": [ "ff", "00", "00", "00", "0000000000000003" ],
"context": "CoVXzytYqZcw4RQknAZJpK3RAFeLrzcZG2zDMdzuacpDPjX7YSor",
"demo_block_header_data": "This is block 3" },
"metadata":
{ "protocol": "ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT",
"next_protocol": "ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT",
"test_chain_status": { "status": "not_running" },
"max_operations_ttl": 0, "max_operation_data_length": 100,
"max_block_header_length": 100,
"max_operation_list_length": [ { "max_size": 1000 } ], "demo_a": 91,
"demo_b": 111 },
"operations":
[ [ { "protocol": "ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT",
"chain_id": "NetXdQprcVkpaWU",
"hash": "op5gBsE7EMi7gsR3xtSMMQms9XN8Pka5N1pT8XGuN1iP2siizkx",
"branch": "BLrQqbn13VrbzUprxQypzAg6fc7YmsHaZvGwGrHJk8a4eG6e11B",
"data": { "IncrA": {} },
"receipt":
{ "demo_operation_receipt": "operation applied successfully" } },
{ "protocol": "ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT",
"chain_id": "NetXdQprcVkpaWU",
"hash": "oo2YhBbAY8Vr2ASXj5k3PggXoxDVYWAQGvK1Sm22GhPFcXdvjQq",
"branch": "BLrQqbn13VrbzUprxQypzAg6fc7YmsHaZvGwGrHJk8a4eG6e11B",
"data": { "IncrB": {} },
"receipt":
{ "demo_operation_receipt": "operation applied successfully" } },
{ "protocol": "ProtoDemoCounterDemoCounterDemoCounterDemoCou4LSpdT",
"chain_id": "NetXdQprcVkpaWU",
"hash": "opJFLuHR98tfTTXGhsnMMjg6KxgVW8qx7LZ5q2nrhoySXySqZSS",
"branch": "BLrQqbn13VrbzUprxQypzAg6fc7YmsHaZvGwGrHJk8a4eG6e11B",
"data": { "Transfer": 10 },
"receipt":
{ "demo_operation_receipt": "operation applied successfully" } } ] ] }
We can finally test our two RPCs to query the counter values.
$ octez-client rpc get /chains/main/blocks/head/counter/a
91
$ octez-client rpc get /chains/main/blocks/head/counter/b
111
The node’s trace is similar to the one presented in the previous part. What we see in addition are three chunks of output (one per injected operation) of the form:
... NOTICE | demo-counter: begin_validation (partial_construction mode): pred_fitness = ff::00::00::00::0000000000000002 constructed fitness = ff::00::00::00::0000000000000003
... NOTICE | demo-counter: begin_application (partial_construction mode): pred_fitness = ff::00::00::00::0000000000000002 constructed fitness = ff::00::00::00::0000000000000003
... NOTICE | demo-counter: validate_operation
... NOTICE | demo-counter: apply_operation
... NOTICE | operation opBuK7iL... injected
These traces are produced when Client_proto_commands.inject_op calls
Demo_block_services.Helpers.Preapply.operations (which enters
partial_construction mode and runs both the validation and
application pipelines on the operation) and then
Shell_services.Injection.operation (which injects the operation into
the mempool).
When the block is baked, we see two phases. First, the pre-apply phase
(triggered by Demo_block_services.Helpers.Preapply.block) runs the
protocol in construction mode, interleaving validate_operation
and apply_operation for each of the three operations:
... NOTICE | demo-counter: begin_validation (construction mode): pred_fitness = ff::00::00::00::0000000000000002 constructed fitness = ff::00::00::00::0000000000000003
... NOTICE | demo-counter: begin_application (construction mode): pred_fitness = ff::00::00::00::0000000000000002 constructed fitness = ff::00::00::00::0000000000000003
... NOTICE | demo-counter: validate_operation
... NOTICE | demo-counter: apply_operation
... NOTICE | demo-counter: validate_operation
... NOTICE | demo-counter: apply_operation
... NOTICE | demo-counter: validate_operation
... NOTICE | demo-counter: apply_operation
... NOTICE | demo-counter: finalize_validation: fitness = ff::00::00::00::0000000000000003
... NOTICE | demo-counter: finalize_application: fitness = ff::00::00::00::0000000000000003
... NOTICE | block at level 3 successfully pre-applied in 371us
Then, the injection phase (triggered by
Shell_services.Injection.block) runs the block through
application mode with the split validation/application model. The
validation pipeline runs first (begin_validation, all
validate_operation calls, finalize_validation), followed by the
application pipeline (begin_application, all apply_operation
calls, finalize_application). Finally, Mempool.init is called
for the new head:
... NOTICE | demo-counter: begin_validation (application mode): pred_fitness = ff::00::00::00::0000000000000002 block_fitness = ff::00::00::00::0000000000000003
... NOTICE | demo-counter: validate_operation
... NOTICE | demo-counter: validate_operation
... NOTICE | demo-counter: validate_operation
... NOTICE | demo-counter: finalize_validation: fitness = ff::00::00::00::0000000000000003
... NOTICE | demo-counter: begin_application (application mode): pred_fitness = ff::00::00::00::0000000000000002 block_fitness = ff::00::00::00::0000000000000003
... NOTICE | demo-counter: apply_operation
... NOTICE | demo-counter: apply_operation
... NOTICE | demo-counter: apply_operation
... NOTICE | demo-counter: finalize_application: fitness = ff::00::00::00::0000000000000003
... NOTICE | demo-counter: Mempool.init: head fitness = ff::00::00::00::0000000000000003
... NOTICE | head is now BLcidY... (3)
Notice how in construction mode (pre-apply), validation and application are interleaved per operation, while in application mode (injection), the validation pipeline runs entirely before the application pipeline starts. This split is the key difference introduced by the validation/application model: during injection, the shell can validate the block cheaply, advertise it to peers, and only then proceed to the more expensive application phase.
This scenario can be reproduced using the following Tezt test. It launches a node, and runs the client commands to activate the protocol and interact with it.
dune exec tezt/tests/main.exe -- --title demo_counter
If you want to have more detailed traces of the node, you should indicate a data directory when running the sandboxed node, like this:
DATA_DIR='/tmp/tz-data' ./src/bin_node/octez-sandboxed-node.sh 1 --connections 0
Then the node trace appears in a file such as /tmp/tz-data/daily_logs/daily-20251124.log.
Mempool module#
Since environment V7 (protocol Lima, 2022), the PROTOCOL signature
requires protocols to implement a Mempool module. This module
provides the shell with a protocol-level API for managing the mempool —
the set of pending operations that have been received but not yet
included in a block. See The Prevalidator for more details
on how the shell uses this module.
For demo_counter, the mempool state type t is simply
State.t — the current counter values. When an operation is added to
the mempool via add_operation, it is applied to this state using
Apply.apply to verify validity. This means the mempool can reject
invalid operations (e.g. a transfer that would make a counter negative)
before they reach block construction.
Key functions:
Mempool.initinitializes the mempool from the current context by reading the state viaState.get_state.Mempool.add_operationvalidates an operation against the current mempool state and adds it if valid.Mempool.add_valid_operationadds a previously validated operation to the mempool (a synchronous variant).
For demo_noops, which has no operations, the Mempool module is a
trivial implementation where all types are unit and all functions
are no-ops or return errors.
Note that remove_operation, merge, and operations are not
used by the demo protocols and are stubbed out with assert false.
In a production protocol, merge plays an important role: it combines
two mempool states (typically from different peers) into one, resolving
conflicts between operations that cannot coexist — for instance, two
manager operations from the same source with the same counter. The
conflict_handler callback decides whether to Keep the existing
operation or Replace it with the new one. This is essential for the
pipelining model (introduced in protocol K and refined in L and M),
where operations are validated and propagated optimistically before
block inclusion.
Conclusion#
We presented a simple protocol demo_counter which explores further
the interface between the shell and the protocol, and uses more features
available to the protocol developer such as RPC services, the mempool
module, and the split validation/application model. Besides, this
protocol comes with a library that extends octez-client with new
commands to interact with the protocol.