State Manager
State manager aims at keeping the state of the node up to date by retrieving missing data and ensuring that it is
consistently stored in the DB. It services requests by other Wasp components (consensus, mempoolWhere transactions are kept in waiting before being processed and where all details of a transaction can be seen by anyone. ), which mainly
consist of ensuring that the required state is available in the node: that it may be retrieved from the permanent
store of the node (the database; DB). An obvious way to obtain the latest state is to obtain all of the blocks,
that resulted in making that state. So to obtain state index n
, state manager first must commit block index 0
(origin block), then block index 1
, then block index 2
etc. up to block index n
precisely in that order.
There are two ways for state manager to obtain blocks (other than origin block):
- Receive them directly from this node's consensus when the new state1 is decided. State manager has no influence to this process.
- Receive them from neighbouring nodes upon request, provided the block is available there.
Independently of the way the block is received, it is stored in state manager's cache (for quicker access) and WAL (to ensure availability). Therefore it may happen that the block can be retrieved from there.
1 A block is a difference between two consecutive states. To make state index n
, block index n
must be obtained
and committed on top of state index n-1
. Although state manager manipulates blocks, in this description sometimes
"state" and "block" will be used interchangeably as "obtaining block" or "committing block" is essentially the same as
"obtaining state" or "committing state" respectively, having in mind that previous state is already obtained or committed. Block
and state has some other common properties, e.g. block index n
, which applied to state index n-1
produces state index n
,
contains the same commitment as state index n
.
Snapshot
Once in a while there might be a need to add a new node to the network. This node has no knowledge of chain's history
and it still needs to have the newest state of the chain (to catch up the chain). If the chain has been running for a while,
it might have gone through many sate transitions and downloading that many blocks may take a long period of time. To avoid that,
some nodes in the network can be configured to dump a complete state of the chain at some time into a file periodically
(see snapshots.period
parameter). This file is called a snapshot. Loading a snapshot to DB produces the same state as downloading
and committing all the blocks that produced that state. However as those blocks aren't downloaded, they are not available in the DB,
except a block with the same state index as snapshot.
The snapshot format is as follows:
- number
4
in 4 byte unsigned integer little endian format representing the length of state index, - state index in 4 byte unsigned integer little endian format,
- number
40
in 4 byte unsigned integer little endian format representing the length of state commitment:20
bytes for trie root and20
bytes for hash of a block, which was last committed to make this state (block with the same index as state), - trie root in
20
bytes, - the mentioned block's hash in
20
bytes, - number
0
in 1 byte unsigned integer format representing snapshot version, - bytes representing mentioned block,
- bytes representing trie of the state.
The node that makes a snapshot can serve it over http and new nodes can use this to speed up the catch up. Serving the snapshots
over http is beyond the scope of Wasp and should be done in addition. Wasp is only responsible for making snapshots in local
(configurable by snapshots.localPath
parameter) folder and obtaining them on start when needed from the same local folder or from
configured (by snapshots.networkPaths
parameter) URLs. A folder, referenced in the snapshots.networkPaths
parameter must contain
INDEX
file with new line separated list of snapshot file names.
If a chain starts with an empty database (usually if the database hasn't been created yet or was deleted), the node checks if
it can load a snapshot: it scans the local folder and all the network addresses for available snapshot files. In the local folder, it reads
all the files with names that satisfy the search pattern *-*.snap
. In each network location, Wasp reads all the files listed in INDEX
file of that location. Wasp reads a state index and a commitment from the contents of these files. File names are not used to obtain
this information, and full snapshot files are not (down)loaded yet. The node chooses
the one with the largest state index and loads it to the store among all available snapshot files. If several files have the same largest state index,
the node loads them one by one, starting from the local ones until one snapshot is loaded correctly. If loading fails for all candidates,
the node will start with an empty database.
You can use the snapshots.snapshotsToLoad
parameter to load a specific snapshot. In that case, the node searches for snapshots with
the block hash provided in the parameter. Once again, if loading all found files fails, the node starts with an empty database.
After a snapshot is loaded, earlier blocks (ones with a smaller state index than the snapshot) cannot be retrieved and committed to the DB
(this is discussed in Obtaining blocks section). This constraint can cause problems (especially in reorg)
if the loaded snapshot is too recent. To avoid that, making snapshots is delayed by snapshots.delay
states. E.g., if snapshots.period
is 100
and snapshots.delay
is 20
, then snapshot index 100
will be produced. When block index 120
is committed, snapshot index
200
will be produced, when snapshot index 220
is committed, etc... For the data to be available after this delay, snapshot.delay
value must be considerably smaller than stateManager.pruningMinStatesToKeep
.
Obtaining blocks
Requests to the state manager contain the state commitment and the state manager must ensure, that block (state) with this commitment is present in the DB. It is possible that to satisfy the request state manager needs to retrieve several blocks. However this cannot be done in one step as only the commitment of the requested block is known. For this reason state (block) contains a commitment of the previous block. Previous block must be committed prior to committing the requested block. And this logic can be extended up to the block, which is already present in the DB, or until the origin state is reached.
E.g., let's say, that the last state in the DB is state index 10
and request to have state index 12
is received.
State manager does this in following steps:
- Block index
12
is obtained, and commitment of block index11
is known. - As the commitment of block (state) index
11
is known, the block may be requested and obtained. After obtaining block index11
commitment of block index10
is known. - Using block index
10
commitment the DB is checked to make sure that it is already present. - As block index
10
is already committed, block index11
is committed. This makes state11
present in the DB. - As state
11
is already committed, block index12
is committed. This makes state12
present in the DB and completes the request.
To obtain blocks, state manager sends requests to 5 other randomly chosen nodes. If the block is not received (either messages got lost or these nodes do not have the requested block), 5 other randomly chosen nodes are queried. This process is repeated until the block is received (usually from other node but may also be from this node's consensus) or the request is no longer valid.
If difference between state indexes of requested state and available in the DB state is large, this chain can get very long. In order to limit its length, if requested block index is a) smaller than state index of snapshot, which was loaded on node start, or b) smaller than largest state index among the pruned blocks (see Pruning), the node panics. If this panicking continues, the administrator may decide to delete the DB and start the node from (possibly configured) snapshot.
Block cache
Block cache is in memory block storage. It keeps a limited amount (configured by stateManager.blockCacheMaxSize
) of blocks
for limited amount of time (configured by stateManager.blockCacheBlocksInCacheDuration
) to make the retrieval
quicker. E.g., in the last step of example of the previous section block index 12
must be committed. It is obtained in
the step 1, but as several steps of the algorithm are spread over time with requests to other nodes in between, and
several requests to obtain the same block may be present, it is not feasible to store it in request. However it would
be wasteful to fetch it twice on the same request. So the block is stored in cache in step 1 of the algorithm and
retrieved from cache later in the last step.
The block is kept in the cache no longer that predetermined amount of time (configured by stateManager.blockCacheBlocksInCacheDuration
).
If upon writing to cache blocks in cache limit is exceeded, block, which is in cache the longest, is removed from cache.
Block write ahead log (WAL)
Upon receiving a block, its contents is dumped into a file and stored in a file system. The set of such files is WAL.
The primary motivation behind creating it was in order not to deadlock the chain. Upon deciding on next state committee nodes send the newest block to state manager and at the same time one of the nodes send the newest transaction to L1. In an unfavourable chain of events it might happen that state managers of the committee nodes are not fast enough to commit the block to the DB (see algorithm in Obtaining blocks section), before the node crashes. This leaves the nodes in the old state as none of the nodes had time to commit the block. However the L1 reports the new state as the latest although none of the nodes can be transferred to it. The solution is to put the block into WAL as soon as possible so it won't be lost.
Currently upon receiving the new confirmed block from node's consensus, state manager is sure that its predecessor is in the DB, because consensus sends other requests before sending the new block, so WAL isn't that crucial any more. However, it is useful in several occasions:
- Storing preliminary block, which is sent by consensus of other nodes.
- When the node is catching up many states and block cache limit is too small to store all the blocks, WAL is used to avoid fetching the same block twice.
- In case of adding new node to the network to avoid catch up taking a lot of time when snapshots are not available,
the new node can be configured (
wal.loadToStore=true
) to load the DB with blocks from WAL on startup. WAL can be copied from some other node. This is also true for any catch up over many states.
Pruning
In order to limit the DB size, old states are deleted (pruned) from it on a regular basis. The amount of states to keep is
configured by two parameters: one in the configuration of the node (stateManager.pruningMinStatesToKeep
) and one in the governance contract
(BlockKeepAmount
). The resulting limit of previous states to keep is the larger of the two. Every time a block is committed
to the DB, states which are over the limit are pruned. However, to avoid freezing State manager for too long, no more than
stateManager.pruningMaxStatesToDelete
blocks are pruned in a single run. The algorithm ensures that oldest states are pruned
first to avoid gaps between available states on the event of some failure.
Pruning may be disabled completely via node configuration to make an archive node: node that contains all the state ever obtained by the chain. Note, that such node will require a lot of resources to maintain: mainly disk storage.
Parameters
State manager
The following parameters may be provided in section stateManager
:
blockCacheMaxSize
: the limit of the blocks in block cache. Default is 1k.blockCacheBlocksInCacheDuration
: the limit of the time block stays in block cache. Default is 1 hour.blockCacheBlockCleaningPeriod
: how often state manager should find and delete blocks, that stayed in block cache for too long. Default is every minute.stateManagerGetBlockRetry
: how often requests to retrieve the needed blocks from other nodes should be repeated. Default is every 3 seconds.stateManagerRequestCleaningPeriod
: how often state manager should find and delete requests, that are no longer valid. Default is every second.stateManagerTimerTickPeriod
: how often state manager should check if some maintenance (cleaning requests or block cache, resending requests for blocks) is needed. Default is every second. There is no point in making this value larger than any ofblockCacheBlockCleaningPeriod
,stateManagerGetBlockRetry
orstateManagerRequestCleaningPeriod
.pruningMinStatesToKeep
: minimum number of old states to keep in the DB. Note that ifBlockKeepAmount
in governance contract is larger than this value, then more old states will be kept. Default is 10k. 0 (and below) disables pruning.pruningMaxStatesToDelete
: maximum number of states to prune in one run. This is needed in order not to grab state manager's time for pruning for too long. Default is 1k.
Snapshots
The following parameters may be provided in section snapshots
:
snapshotsToLoad
: the comma sepparated list of<chainID>:<hash>
pairs, where chain<chainID>
must be started using snapshot with block hash<hash>
. The list can also contain<hash>
entry. This hash will be used for other chains, which are not configured separately. There is no point in having several<hash>
or<chainID>:<hash>
entries with the same<chainID>
as only the last such entry is taken into account. Note that if the chain is configured to start from some snapshot and the snapshot is not available (or another error occurs during snapshot loading), the chain will start with an empty DB. The default is an empty list, which means that the newest available snapshot will be loaded for every chain.period
: how often state snapshots should be made: 1000 meaning "every 1000th state", 0 meaning "making snapshots is disabled". Snapshots are disabled by default.delay
: how many states to delay making the snapshot; it must be considerably smaller thanstateManager.pruningMinStatesToKeep
. The default is 20.localPath
: the path to the snapshots folder in this node's disk. Default iswaspdb/snap
.networkPaths
: the comma-separated list of URLs that serve snapshots. The URLs may have the HTTP (e.g.,http://server.org/path/
) or the HTTPS (e.g.,https://server.org/path/
) scheme for remote locations or a file path (e.g.,file://path/to/folder
) scheme for local snapshot locations. The scheme is compulsory in the URL. The list is empty by default.
WAL
The following parameters may be provided in section wal
:
loadToStore
: load blocks from WAL to the store on node start-up. This function is off (false
) by default.enabled
: whether the WAL is enabled. It is enabled by default.path
: the path to the WAL folder. Default iswaspdb/wal
.