-
Notifications
You must be signed in to change notification settings - Fork 3
Validator Set Contract Implementation Note
For implementing validator set contract in geth AuRa, i have introduced a new sub-package, validatorset
under consensus
package. In validatorset
package, there are 5 go files -
- contract.go
- multi.go
- safe_contract.go
- simple_list.go
- validator_set.go
a. simple_list.go file is responsible for maintaining static validator list.
b. safe_contract.go file is responsible for maintaining validator set contract and it's methods.
c. contract.go file is responsible for maintaining validator set contract with reporting malicious validator functionalities.
d.multi.go file is parse the multi structure from genesis.json file and create different instances like simple_list, safe_contract, or contract structure and map them with certain block height.
e. validator_set.go basically parse authorities of genesis.json and defines an interface ValidatorSet which is implemented by other go files.
Here is the ValidatorSet
interface which is implemented by multi.go, safe_contract.go, simple_list.go, contract.go.
// A validator set.
type ValidatorSet interface {
// Whether the given block signals the end of an epoch, but change won't take effect
// until finality.
SignalToChange(first bool, receipts types.Receipts, blockNumber int64, simulatedBackend bind.ContractBackend) ([]common.Address, bool, bool)
// All calls here will be from the `SYSTEM_ADDRESS`: 2^160 - 2
// and will have an effect on the block's state.
FinalizeChange(header *types.Header, state *state.StateDB) error
// Draws validator list from static list or from validator set contract.
GetValidatorsByCaller(blockNumber int64) []common.Address
// Prepare simulated backend for interacting with smart contract
PrepareBackend(blockNumber int64, simulatedBackend bind.ContractBackend) error
}
In ValidatorSet
interface, there are 4 unimplemented methods -
-
SignalToChange
method is called with respectedValidatorSet
instance like if current validatorSet is simpleList which is nothing but a static validator list then theSignalToChange
method of simpleList validatorSet do nothing. If current validatorSet is safeContract then it parse the receipts from processed block and check for any changes in validator of validator set contract and return new set of validator. -
FinalizeChange
is responsible for callilng fanilizeChange method of validator set contract. -
GetValidatorsByCaller
is responsible for calling current validator set. -
PrepareBackend
is reponsible for preparing simulated backend for interacting with validator set contract.
Redeclared new interface AuraEngine
for integrating validator set contract with Aura engine. There are 3 new methods are defined in aura.go file. These methods are called from blockchain.go and worker.go file.
```
// Aura is
type AuraEngine interface {
Engine
// Signals for any changes in validator set contract
SignalToChange(receipts types.Receipts, blockNumber *big.Int)
// CallFinalizeChange calls finalizeChange method of the validator set contract
FinalizeChange(header *types.Header, chain ChainHeaderReader, state *state.StateDB) error
// Prepare contract backend for interacting with validator set contract
PrepareBackend(chain ChainHeaderReader)
}
```
Here is the implementation details about these methods -
-
SignalToChange
method is responsible to detect an event of validator set contract. When any changes has been occurred in validator set, this method gets acknowledged. It takes receipts from every block and parse it using validator set contract address and parse event. It returns new validator set and 2 boolean flag which indicates that whether any changes in validator set and another one is for whether it is a transition from static set to validator contract. If there are any changes in validator set, then there is a struct namedTransition
which is responsible to store current epoch and finalizeMethod calling epoch. There are 3 types of changes occurred in validator set contract- a. Transition from static validator set to validator set contract. When it happens at n-th block height, this method sets finalizeBlock at (n + 1)th block. b. When any validator is added from validator set contract at n-th block, this method sets finalizeBlock at (n + 1)th block. c. When any validator is removed from validator set contract at n-th block, the finalizeBlock will be (n_+ 2)th block. this transition structure is maintained in-memory andSignalToChange
method is called in each block. Here is the implementation of the method -// Signals for any changes in validator set contract func (a *Aura) SignalToChange(receipts types.Receipts, blockNumber *big.Int) { first := blockNumber.Cmp(big.NewInt(0)) == 0 newSet, hasChanged, isFirst := a.validators.SignalToChange(first, receipts, blockNumber.Int64(), a.simulatedBackend) // if changes in validator set, it gives true and set transition struct for finality if hasChanged { // Aura can not operate without any validator set if len(newSet) == 0 { panic("Cannot operate with an empty validator set.") } a.transition.blockNumber = blockNumber.Int64() // signal block a.transition.pendingValidatorSet = newSet // pending validator set for setting next validator set a.transition.finalizeBlock = blockNumber.Int64() + 1 // in which block the finalizeChange method will call // finalizeChange method calls after 2 block later when removes any validator from // validator set contract. if !isFirst && len(newSet) < len(a.validatorSet) { a.transition.finalizeBlock = blockNumber.Int64() + 2 } log.Info("Extracted epoch validator set. ", "number", a.transition.blockNumber, "finalizeNumber", a.transition.finalizeBlock, "newSet", newSet, "curSet", a.validatorSet) } }
This method is called from resultLoop in worker.go and also called from blockchain.go after downloaded block has been procesed.
-
FinalizeChange
method is responsible to callFinalizeChange
method of vaildator set contract. It checks whether the current block height is same as finalizeBlock number which is set inSignalToChange
method or not. If current block height is same as finalizeBlock number, it calls finalizeChange method of validator set contract and update validator set in consensus engine. Here is the implementation of the method -// FinalizeChange calls when any validator list changing transaction comes to the node. It calls // to contract for finality func (a *Aura) FinalizeChange(header *types.Header, chain consensus.ChainHeaderReader, state *state.StateDB) error { // if current block is same as finalizeBlock then calls finalizeChange method if header.Number.Cmp(big.NewInt(a.transition.finalizeBlock)) == 0 { if err := a.validators.FinalizeChange(header, state); err != nil { log.Warn("Encountered error in calling finalizeChange method", "err", err) return err } // update the current root hash of the state trie because FinalizeChange method update the state of // validator set contract header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number)) // update validator set for sealing with updated validator set from the current block a.validatorSet = a.transition.pendingValidatorSet log.Debug("Updating finality checker with new validator set extracted from epoch", "epochBlock", a.transition.blockNumber, "finalizeBlock", a.transition.finalizeBlock, "newSet", a.validatorSet) } return nil }
-
PrepareBackend
method is reponsible for preparing validatorSet contract when bootstrapping blockchian. It prepares simulated backend and assign it to validatorSafeContract instance. It initializes validator set at bootstrap. Here is the implementation of the method -// PrepareBackend prepares validator set contract caller and set initial validator set func (a *Aura) PrepareBackend(chain consensus.ChainHeaderReader) { // creates new instance for simulated backend a.simulatedBackend = backends.NewSimulatedBackendWithChain(chain.(*core.BlockChain), a.db, chain.Config()) // calling PrepareBackend of validator set a.validators.PrepareBackend(chain.CurrentHeader().Number.Int64(), a.simulatedBackend) a.validatorSet = a.validators.GetValidatorsByCaller(chain.CurrentHeader().Number.Int64()) log.Info("Initial validator set", "curSet", a.validatorSet) }
This method is called from blockchain.go file.
For interacting with validator set contract, need to change the following files -
a. `accounts/abi/bind/backend.go`
b. `accounts/abi/bind/backends/simulated.go`
c. `accounts/abi/bind/base.go`
d. `core/state_transition.go`
Here backend.go
is an interface of different client which are responsible for interacting with smart contract from geth like simulated.go and ethclient.go. Need to add a new method for interacting with smart contract, this method is implemented by simulatedBackend to set current stateDB and header.
```
// PrepareCurrentState set the current stateDB for calling finalizeChange method of
// validator set contract.
PrepareCurrentState(header *types.Header, stateDB *state.StateDB)
```
In base.go, added filtering based on from address. When calling method in validator set contract, we use system address which is nothing but a dummy address(0xfffffffffffffffffffffffffffffffffffffffe). If any method call to a contract comes from this address then, we call our customized method b.systemCallContract(call, b.header, b.stateDB)
for calling validator set contract.
In state_transition.go, need to add a new method for calling Call method of evm. No need to change in evm method. Just call the Call method of evm from state_transition.go. The new method in state_transition.go is -
```
// TransitionDBForSystemCall will transact when node calls to validator set contract from
// SYSTEM_ADDRESS. This transition does not need any gas.
func (st *StateTransition) TransitionDBForSystemCall() (*ExecutionResult, error) {
msg := st.msg
sender := vm.AccountRef(msg.From())
ret, _, vmerr := st.evm.Call(sender, st.to(), st.data, math.MaxUint64, st.value)
return &ExecutionResult{
UsedGas: 0,
Err: vmerr,
ReturnData: ret,
}, nil
}
```
I introduced this method because we do not need to satisfy these following rules -
- the nonce of the message caller is correct
- caller has enough balance to cover transaction fee(gaslimit * gasprice)
- the amount of gas required is available in the block
- the purchased gas is enough to cover intrinsic usage
- there is no overflow when calculating intrinsic gas
- caller has enough balance to cover asset transfer for topmost call
For skipping these checks, i introduced this TransitionDBForSystemCall
method for system call.
[NB]-I have followed the validator set implementation of OpenEthereum.
Documentation under construct