# Staking states and transitions
struct StakedState {
address: StakedStateAddress,
nonce: u64,
bonded: Coin,
unbonded: Coin,
unbonded_from: Timespec,
validator: Option<Validator>,
}
struct Validator {
council_node: CouncilNode,
jailed_until: Option<Timespec>,
inactive_time: Option<Timespec>,
used_validator_keys: Vec<(TendermintValidatorPubKey, Timespec)>,
}
# States
# Clean staking
validator.is_none()
# Validator
validator.is_some()
There are several variants of it:
# Active
validator.inactive_time.is_none()
NOTE: Active validator doesn't necessarily mean the final validator take effect in tendermint, please refer to Choose final validators
# Inactive
validator.inactive_time.is_some()
# Jailed
validator.jailed_until.is_some()
NOTE: Jailed implies inactive, but not vice versa
# State transitions
# From "clean staking" or "inactive(unjailed) validator" to active validator
# Node join
The only way to transit to active validator is by executing NodeJoinTx, the preconditions are:
bonded >= min_required_staking- The validator pubkey/address is not already used by others, it's ok to re-use the old keys used by itself if it's a re-join from an inactive validator.
- Not jailed if transiting from inactive validator
# From "active validator" to "inactive validator"
There are several cases for this:
# Bonded coins become lower than required
When bonded < min_required_staking, this transition happens.
The reasons for dropping of bonded coins maybe:
- Execute
UnbondTxatdeliver_txevent - Slashed for non-live or byzantine faults at
begin_blockevent
NOTE: The transition happens immediately in
deliver_txorbegin_blockevents, won't reverse automatically when bonded coins become enough again even in the same block, so the activeness is always well-defined during the whole process.
# Jailed for byzantine faults
Jailed always implies inactive.
This happens in begin_block event.
# From "jailed validator" to "inactive(unjailed) validator"
# Unjail
The only way to leave jailed validator state is by executing UnjailTx, the preconditions are:
- Already jailed
block_time >= jailed_until
# From "inactive validator" to "clean staking"
# Clean up
The clean up procedure will remove the validator record if:
- Not jailed
block_time >= inactive_time + cleanup_period
NOTE:
cleanup_periodCurrently
cleanup_period = unbonding_period, but logically,cleanup_periodonly needs such constraints:
> max_evidence_age, so we can handle delayed byzantine evidences (inactive validator can still be slashed for later detected byzantine faults)> 2 blocks, so we don't panic when seeing signing vote of inactivated validators
# Appendix
# Choose final validators
The final validator set that take effect in tendermint is chosen at end_block event by:
- Sort all the active validators by
voting_power desc, staking_address - Take the first
max_validatorsones
The abci protocol of end_block event expect validator set updates in response, so we need to diff the new set against
the current set to get the updates.
For example, assuming max_validators = 3, if you are the fourth active validator, so you are not chosen yet, but in
the future if any validator in the top 3 quit, you will be chosen automatically at the next end_block event:
max_validators = 3
genesis:
validators (map of validator address to voting power):
- addr1 -> 10
- addr2 -> 9
block1:
deliver_tx
- join_node(addr3, 8)
- join_node(addr4, 7)
active validators:
- addr1 -> 10
- addr2 -> 9
- addr3 -> 8
- addr4 -> 7
end_block validator updates:
- addr3 -> 8
block2:
deliver_tx:
- unbond_all(addr1)
active validators:
- addr2 -> 9
- addr3 -> 8
- addr4 -> 7
end_block validator updates:
- addr1 -> 0
- addr4 -> 7
# Implications of jailing
# Transactions
Only UnjailTx is allowed to be executed on a jailed staking if the jailed_until time is passed.
Disallowed transactions are:
DepositTxWithdrawTxUnbondTxNodeJoinTx
# Reward distribution
It won't distribute rewards to jailed validators, inactive(unjailed) validators will get the rewards as normal.
When a validator is jailed, it's reward participation tracking records are removed immediately.
# Process byzantine faults
Jailed validators won't be slashed again for byzantine faults detected in jailing period.
# Nonce
The nonce is the number of transactions that have the witness of the staking address, which includes:
WithdrawTxUnbondTxUnjailTxNodeJoinTx
# Liveness tracking
All active validators's liveness trackers are maintained no matter if it's chosen into the final validator set.
If it doesn't appear in the votes reported by tendermint, it's recorded as a
truewhich means live by default.Inactive validator's liveness trackers are also maintained, and recorded as a
truefor each block, this serves two purposes:- After a validator inactivated, the signing vote might still arrive for the next two blocks, we don't want to issue a false warning in this case.
- Validator might quit and re-join very fast (by
UnbondTx/DepositTx/NodeJoinTx), in this case it's liveness tracking record is preserved.
It means the liveness tracker is only removed when validator record get cleaned up.
# Inactive validator re-join with different validator key
When an inactive validator re-join, it can provide different validator key, but it still needs to be hold responsible for
byzantine fault committed before for as long as max_evidence_age. So we need to keep the old validator keys for
sometime.
Whenever validator change consensus key, the old key and current block time are pushed into used_validator_keys, before that, the used keys older than max_evidence_age are removed.
There is a maximum bound (max_used_validator_keys) on the size of used_validator_keys to prevent attack. After the maximum bound reached, re-join with new validator key is not allowed.
# Non exists and empty staking
Empty staking is defined as:
StakedState {
address,
nonce: 0,
bonded: 0,
unbonded: 0,
unbonded_from: 0,
validator: None,
}
For all the logic processing, the result of success execution should be the same for both non exists staking and empty staking.
The error message for failed execution maybe different, for example WithdrawTx might report StakingNotExists on non
exists staking, but CoinError on empty staking.
So the implementor should be free to choose either semantics.