This folder contains an initial high-level specification of the 3SF protocol.
The current specification is just a start and therefore is still incomplete.
A list of TODOs and Known Issues is detailed in the file TODO_and_KNOWN_ISSUES.md.
This high-level specification aims to specify the external behavior of a node implementing the 3SF protocol.
Intuitively, the external behavior corresponds to the messages that a node sends and how a selected view of the state of a node (e.g. finalized and available chain) changes in response to a given sequence of input events (messages received and time updates).
This specification is not concerned with computational efficiency at all. However, every function must be computable within a finite, but potentially unbounded, amount of time.
Computational efficiency is intended to be handled by lower-level specifications implementing this high-level specification.
Intuitively, in the context of this work, a specification
Note that it is admitted for a specification
A more formal definition is provided below.
This section provides a quick overview of some of the most significant differences between this specification and the current Ethereum specification.
- This specification also encodes the expected behavior of honest nodes which in the Ethereum specification is mainly described through natural language in the Honest Validator Guide. By doing so, this specification eliminates possible ambiguities inherent to natural language. Additionally, it provides a more straightforward path to formal verification as formal verification of many properties requires unambiguous coding of the expected behavior of honest nodes.
NodeState
in this specification is the equivalent ofStore
in the Ethereum specification.- As anticipated above and detailed below, this specification is not concerned with computational efficiency.
- Redundant information in
NodeState
is reduced as much as possible, that is, the value of any field inNodeState
should not be derivable from the value of the other fields. This strategy is expected to help ensure a correct design, as it reduces the risk of discrepancies between the protocol designers' intended relationships among fields and their actual interrelations. Additionally, formal verification processes, which often require determining and proving the relationships between fields, are facilitated by this approach. - As a consequence of the above, there is no concept of block state in this specification.
- As mentioned in other parts of this document, computational efficiency can be addressed, if required, via lower-level specifications.
3sf_high_level.py
is the "entry" point of the specification. It includes all the event handlers and the functions specifying the external view of the state of a node (more on this below).helpers.py
contains all of the helper functions used by3sf_high_level.py
.data_structures.py
contains all the data structure definitions.stubs.pyi
contains functions that have not yet been defined.formal_verification_annotations.py
includes the definition of all the annotations used to aid the formal verification of this specification.pythonic_code_generic.py
is reserved for code heavily reliant on Python syntax and semantics. More on this below.
The dummy decorator @Event
is used to identify the Python functions that specify the behavior in response to specific external events.
For example, the following Python function specifies how the node should behave when the node receives a Propose message.
@Event
def on_received_propose(propose: SignedProposeMessage, node_state: NodeState) -> NewNodeStateAndMessagesToTx:
...
In the above, node_state
corresponds to the current state of a node.
The value returned is a @dataclass NewNodeStateAndMessagesToTx
instance with the following three fields:
state
: the new state of the node in response to receiving the Propose messagepropose
proposeMessagesToTx
: the set, possibly empty, of Propose messages that the node must send in response to receiving the Propose messagepropose
voteMessagesToTx
: the set, possibly empty, of Vote messages that the node must send in response to to receiving the Propose messagepropose
The dummy decorator @Init
is used to denote the function that returns the initial NodeState
.
The dummy decorator @View
is used the define which portion of the node state is externally visible.
As mentioned above, this spec does not specify how the state of a node evolves, but only how the externally visible state of a node evolves.
For example, the following Python code expresses that the externally visible state of a node corresponds to its finalized chain and available chain as returned by the functions finalized_chain
and available_chain
respectively.
@View
def finalized_chain(node_state: NodeState) -> PVector[Block]:
...
@View
def available_chain(node_state: NodeState) -> PVector[Block]:
...
Requires
are dummy statements are used to annotate functions with pre-conditions, that is, conditions that are assumed to always be true every time that the function is called.
It is the responsibility of the callers to ensure that this is the case.
For example, the Python code below states that every time that get_parent(block, node_state)
is called, the caller must ensure that has_parent(block, node_state) == True
and, hence, the function get_parent
can assume that this condition is always satisfied any time that it is executed.
def get_parent(block: Block, node_state: NodeState) -> Block:
Requires(has_parent(block, node_state))
return get_block_from_hash(block.parent_hash, node_state)
By translating the Python spec to a formal language that supports mechanized formal verification, it will be possible to have a mechanical formal proof that every time that get_parent(block, node_state)
is called, has_parent(block, node_state) == True
.
-
Use only immutable data structures. This is very helpful for formal verification purposes as one does not need to be concerned with the problem of aliasing.
- For sets, lists and maps use the types
PSet
,PVector
andPMap
, respectively, from thepyrsistent
library. - For composite data structures that never need to be manipulated during any function, use
@dataclass(frozen=True)
. - For the composite data structures that need to be manipulated during the execution of some of the functions, base them off the
PRecord
class from thepyrsistent
library. This is to usePRecord
s only when strictly necessary because, as of today, MyPy cannot typecheckPRecord
s, but it can typecheck@dataclass
es.
- For sets, lists and maps use the types
-
Reduce the usage of Pythonic code as much as possible. This is to have a spec with very simple semantics and therefore reduce the risk of possible misinterpretations. As a consequence of this principle, the following rules have been followed:
- For all the files, except for
data_structures.py
andpythonic_code_generic.py
, use only the following features of the Python language- Function definitions and function calls
if
statementsfor
statements- call to the
set
method ofPRecord
s - lambdas
- Relegate all of the code that needs features of the Python language outside of those listed above to the file
pythonic_code_generic.py
except for data structure definitions which are to be placed in the filedata_structures.py
.
- For all the files, except for
- Employ functions effectively to encapsulate the semantics of operations. This is to improve readability. For example, rather than using
node_state.blocks[hash]
throughout the code to retrieve a block with hashhash
, define and use the functionget_block_from_hash(Hash, NodeState)
which better captures the semantics.
- Add a field to
NodeState
only if it cannot be computed from the others. - Use fixed-size types only for message fields. For any other integer type, use
int
. Fixed-size types (except for messages) are for lower-level specifications.
Defined in FORMAL_SEMANTICS.md