This document outlines the components of implementing an FFG Validator. These include the:
- Validator workflow overview
- Validator States
- Validator Code
- Vote format and generation
- Logout format
simple_casper
contract overview- Casper transaction gas refunds
- Simple validator voting logic.
- Create valcode:
- Deploy a new contract which is used to validate a validator's signature.
- Submit Deposit:
- Call
casper.deposit(validation_addr, withdrawal_addr)
passing in the validation code contract address from step (1) and your withdrawal address.
- Call
- [Once each Epoch] Submit new vote message:
- Wait to vote until the checkpoint is at least
EPOCH_LENGTH/4
blocks deep in the main chain. This ensures all validators vote on the same block. - Generate unsigned vote message based on your chain's current head.
- Broadcast the unsigned vote transaction to the network.
- Wait to vote until the checkpoint is at least
- Logout:
- Submit logout message.
- Call
casper.logout(logout_msg)
passing in your newly generated logout message.
- Withdraw:
- Call
casper.withdraw(validator_index)
and your funds will be sent to your validator's withdrawal address specified in step (2).
- Call
Validators are highly stateful. They must handle valcode creation, depositing, voting, and logging out. Each stage also requires waiting for transaction confirmations. Because of this complexity, a mapping of state to handler is used.
The validator state mapping implemented in pyethapp is as follows:
uninitiated: self.check_status,
waiting_for_valcode: self.check_valcode,
waiting_for_login: self.check_status,
voting: self.vote,
waiting_for_log_out: self.vote_then_logout,
waiting_for_withdrawable: self.check_withdrawable,
waiting_for_withdrawn: self.check_withdrawn,
logged_out: self.check_status
Arrows are the logic followed upon receiving a new block while in a given state. For example, if the validator is in state voting
and receives a new block whose height is in_first_quarter_of_epoch
, then the validator follows the arrow to remain in the state voting
.
Validators must deploy their own signature validation contract. This will be used to check the signatures attached to their votes. This validation code must be a pure function. This means no storage reads/writes, environment variable reads OR external calls (except to other contracts that have already been purity-verified, or to precompiles) allowed.
For basic signature verification, ecdsa signatures are currently being used. The validation code for these ecdsa signatures can be found here. Note, this LLL code uses the elliptic curve public key recovery precompile.
The validation code contract is currently being deployed as a part of the induct_validator()
function found here:
def induct_validator(chain, casper, key, value):
sender = utils.privtoaddr(key)
valcode_addr = chain.tx(key, "", 0, mk_validation_code(sender)) # Create a new validation code contract based on the validator's Ethereum address
assert utils.big_endian_to_int(chain.tx(key, purity_checker_address, 0, purity_translator.encode('submit', [valcode_addr]))) == 1
casper.deposit(valcode_addr, sender, value=value) # Submit deposit specifying the validation code contract address
A Casper vote is an RLP-encoded list with the elements:
[
validator_index: number, # Index of the validator sending this vote
target_hash: bytes32, # Hash of the target checkpoint block for this vote
target_epoch: number, # Epoch number of the target checkpoint
source_epoch: number, # Epoch number of the source checkpoint
signature # A signed hash of the first four elements in this list, RLP encoded. (ie. RLP([validator_index, target_hash, target_epoch, source_epoch])
]
Casper vote messages are simply included in normal transactions sent to the Casper contract's casper.vote(vote_msg)
function.
To generate a Casper vote which votes on your chain's current head, first get the vote message contents. To do this, using the Casper contract call:
casper.validator_indexes(WITHDRAWAL_ADDRESS)
for thevalidator_index
casper.recommended_target_hash()
for thetarget_hash
casper.current_epoch()
for thetarget_epoch
casper.recommended_source_epoch()
for thesource_epoch
Next, RLP encode all these elements. To compute your signature, compute the sha3
hash of your vote's RLP encoded list, and sign the hash. Your signature must be valid when checked against your validator's validation_code
contract. Finally, append your signature to the end of the vote message contents.
This is implemented in Pyethereum as follows:
def mk_vote(validator_index, target_hash, target_epoch, source_epoch, key):
msg_hash = utils.sha3(rlp.encode([validator_index, target_hash, target_epoch, source_epoch]))
v, r, s = utils.ecdsa_raw_sign(msg_hash, key)
sig = utils.encode_int32(v) + utils.encode_int32(r) + utils.encode_int32(s)
return rlp.encode([validator_index, target_hash, target_epoch, source_epoch, sig])
Like the Casper vote messages, a logout message is an RLP encoded list where the last element is the validator's signature. The elements of the unsigned list are the validator_index
and epoch
where epoch is the current epoch. A signature is generated in the same way it is done with votes above.
This is implemented in Pyethereum as follows:
def mk_logout(validator_index, epoch, key):
msg_hash = utils.sha3(rlp.encode([validator_index, epoch]))
v, r, s = utils.ecdsa_raw_sign(msg_hash, key)
sig = utils.encode_int32(v) + utils.encode_int32(r) + utils.encode_int32(s)
return rlp.encode([validator_index, epoch, sig])
The Casper smart contract contains Casper's core logic. It is written in Vyper & can be deployed to the blockchain like any other contract to CASPER_ADDR
. Casper messages are then sent to the contract by calling vote(vote_msg)
where vote_msg
is a Casper vote messaged as outlined above.
The Casper contract constructor takes in key settings. Most notably:
epoch_length
withdrawal_delay
dynasty_logout_delay
min_deposit_size
base_interest_factor
base_penalty_factor
These settings cannot be changed after deployment.
Calculates the interest rate & penalty factor for this epoch based on the time since finality.
Once a new epoch begins, this function is immediately called as the first transaction applied to the state. See Epoch initialization for more details.
Accepts deposits from prospective validators & adds them to the next validator set.
Initiates validator logout. The validator must continue to validate for dynasty_logout_delay
dynasties before entering the withdrawal_delay
waiting period.
If the validator has waited for a period greater than withdrawal_delay
epochs past their end_dynasty
, then send them ETH equivalent to their deposit.
Called once by each validator each epoch. The vote message contains the fields presented in Casper Vote Format.
Can be called by anyone who detects a slashing condition violation. Sends 4% of slashed validator's funds to the caller as a finder's fee and burns the remaining 96%.
Casper votes do not pay gas if successful, and will be considered invalid transactions and not be included in the block if failed. This avoids a large gas burden on validators.
Once a validator is logged in, they can use the following logic to determine when to send a vote:
- When a new block is received & replaces our chain's current head, call
validate(block)
- Inside
validate(block)
check:- The block is at least
EPOCH_LENGTH/4
blocks deep to ensure that the checkpoint hash is safe to vote on. - [NO_DBL_VOTE] The block's epoch has not been voted on before.
- [NO_SURROUND] The block's
target_epoch
>=self.latest_target_epoch
andsource_epoch
>=self.latest_source_epoch
. NOTE: This check is very simple, but it excludes a couple cases where it would be safe to vote.
- The block is at least
- If all of the checks pass, generate & send a new vote!
NOTE: To check if a validator is logged in, one can use:
return casper.validators__start_dynasty(validator_index) <= casper.dynasty()