This module provides functions pertaining to the validation of blocks and operations. Most elements in this module are either used or wrapped in the Main
module (though some of them are also directly used by the plugin).
The purpose of validation is to decide quickly whether a block or an operation is valid, with minimal computations and without writing anything in the storage. A block is considered valid if it can be applied without failure (see Apply
). An operation is valid if it can be safely included in a block without causing it to fail. Therefore, the current module is responsible for ensuring that calling functions from Apply
on validated blocks and operations will not fail.
Block validation
The process of validation of a block may be started by calling one of the following functions, depending on the circumstances (aka mode):
begin_application
is used for the validation of a preexisting block, typically received through the network, and usually in preparation for its future application.
begin_partial_validation
is used to quickly but partially validate an existing block. It is intended for quickly assessing a series of blocks in an alternate branch (multipass validation). For this reason, in this mode, the initial Alpha_context.t
may be based on an ancestor block of the block to validate, instead of necessarily its predecessor as in other modes.
begin_full_construction
is used for the construction of a new block, typically by a baker.
Then, validate_operation
should be called on every operation in the block (in order of validation pass: see Operation_repr.acceptable_pass
). Lastly, finalize_block
performs final checks on the block; if this function succeeds then the block is valid.
Validation state
The process of block validation relies on a validation_state
transmitted throughout the aforementioned function calls. More precisely, this immutable functional state is initialized by the begin_...
functions, read and updated by validate_operation
(as in, a slightly different validation_state
is returned), and required by finalize_block
. It consists in three fields:
info
contains static information required by validate_operation
and finalize_block
, notably the initial Alpha_context.t
. It is fully filled in by the begin_...
functions, then only read, never updated.
operation_conflict_state
keeps track of every validated operation in the block, so that it can detect any conflict between operations (e.g. two manager operations from the same source). Consequently, it is both filled in and read by validate_operation
, but not used at all by finalize_block
.
block_state
registers global block metrics such as total gas used or endorsement power. It is filled in by validate_operation
, which also uses it, e.g. to immediately return an error if the block gas limit is exceeded. It is also essential to several checks in finalize_block
.
The immutability of the validation_state
allows the caller to pause, replay, or backtrack throughout the steps of the validation process.
Operation validation
Operations may be validated either as part of the validation of a block in which they are included (see above), or on their own:
begin_partial_construction
allows to initialize a validation_state
for the validation of operations outside of the process of validation of a block. It is intended for mempools (see Mempool_validation
) and for some RPCs. The global block properties such as total block gas and endorsement power are not checked. Calling finalize_block
on such a validation_state
does not make much sense and simply returns unit.
begin_no_predecessor_info
is a special weaker version of begin_partial_construction
: see its own documentation below.
Even outside of the context of a given block validation, the validation of operations aims at deciding whether they could theoretically be included in a future block. Indeed, for a mempool, this means that they are worth transmitting to a baker and propagating to peers; or for the caller of an RPC, it means that the tested operations may be injected in the node.
An important property to maintain is that applying (see Apply.apply_operation
) any subset of validated operations should always succeed, even if they are not applied in the same order as they were validated (as long as the order of application respects the validation passes ordering). In other words, for all operations A and B that have both been validated: if A has an earlier or the same validation pass as B, then applying A then B must succeed; and if B has an earlier or the same validation pass as A, then applying B then A must succeed. Some restrictions, such as one-operation-per-manager-per-block (1M), have been introduced to preserve this property, and are enforced with the help of the operation_conflict_state
. An important consequence of this property is that a baker may select any subset of validated operations to bake into a new block, which is then guaranteed to be applicable (provided that it verifies some additional global properties such as including enough (pre)endorsing power; the baker is responsible for ensuring this).
For a manager operation, validity is mainly solvability, ie. the operation must be well-formed and we must be able to take its fees. Indeed, this is sufficient for the safe inclusion of the operation in a block: even if there is an error during the subsequent application of the manager operation, this will cause the operation to have no further effects, but won't impact the success of the block's application. The solvability of a manager operation notably requires that it is correctly signed: indeed, we can't take anything from a manager without having checked their signature.
A non-manager operation is only valid if its effects can be fully applied in an Alpha_context.t
without failure. Indeed, any error during the application of such an operation would cause the whole block to fail; unlike manager operations, there is no notion of failing to have an effect without impacting the application of the whole block. More detailled documentation on checks performed and potential errors can be found in the validate.ml
file for some non-manager operations.
Static information required to validate blocks and operations.
type operation_conflict_state
State used to keep track of previously validated operations and detect potential conflicts. This state is serializable which allows it to be exchanged with another source. See Mempool_validation
.
Encoding for the operation_conflict_state
.
State used to register global block properties which are relevant to the validity of a block, e.g. the total gas used in the block so far. This state is both used and updated by the validate_operation
function, and is also required by finalize_block
.
Validation state (see above).
Return the context stored in the state.
Note that this is the context at the beginning of the block / mempool: indeed, it is not modified by validate_operation
.
Initialize the validation_state
for the validation of an existing block, usually in preparation for its future application.
Initialize the validation_state
for the partial validation of an existing block.
The partial validation mode is intended for quickly assessing a series of blocks in a cousin branch (multipass validation). Therefore, it is the only mode in which the given context
may be based on any recent ancestor block of the block to validate, instead of only its predecessor (where recent means having a greater level than the last_allowed_fork_level
of the current head).
Initialize the validation_state
for the validation of operations outside of the process of validation of a block. The partial construction mode is mainly used to implement the mempool (see Mempool_validation
), but may also be used by some RPCs.
Similar to begin_partial_construction
but do not require predecessor information that is essential to the validation of preendorsement and endorsement operations. As a consequence, the validation of these operations will always fail.
This function is used by the plugin RPC run_operation
, which does not support consensus operations anyway.
Check the validity of the given operation and return the updated validation_state
.
See the documentation at the top of this module on operation validation.
Finish the validation of a block.
This function should only be used after validate_operation
has been called on every operation in the block. It checks the consistency of the block_header with the information computed while validating the block's operations (Endorsement power, payload hash, etc.) Checks vary depending on the mode (ie. which of the begin_...
functions above was used to initialize the validation_state
).
The remaining functions are intended for the mempool. See Mempool_validation
.
Check the operation validity, similarly to validate_operation
.
However, this function does not check for conflicts with previously validated operations, nor global block properties such as the respect of the block gas limit. This allows the function to only take an info
as input rather than a full validation_state
.
This function is intended for Mempool_validation
exclusively.
Remove a valid operation from the operation_conflict_state
.
Preconditions:
- The operation has already been validated and added to the
operation_conflict_state
. - The
operation_conflict_state
and other states used to validate the operation have been initialized by calling begin_partial_construction
.
This function is intended for Mempool_validation
, though it is also called by the plugin.