Skip to content

Validator Set Contract Implementation Note

Atif Anowar edited this page Dec 4, 2020 · 3 revisions

Implementation Notes

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 -

  1. contract.go
  2. multi.go
  3. safe_contract.go
  4. simple_list.go
  5. validator_set.go

Responsibilities of these files -

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 -

  1. SignalToChange method is called with respected ValidatorSet instance like if current validatorSet is simpleList which is nothing but a static validator list then the SignalToChange 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.

  2. FinalizeChange is responsible for callilng fanilizeChange method of validator set contract.

  3. GetValidatorsByCaller is responsible for calling current validator set.

  4. PrepareBackend is reponsible for preparing simulated backend for interacting with validator set contract.

How to integrate validator set with AuRa consensus engine

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 -

  1. 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 named Transition 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 and SignalToChange 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.

  1. FinalizeChange method is responsible to call FinalizeChange method of vaildator set contract. It checks whether the current block height is same as finalizeBlock number which is set in SignalToChange 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
    }
    
  2. 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.

How does geth interact with validator set contract?

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 -

  1. the nonce of the message caller is correct
  2. caller has enough balance to cover transaction fee(gaslimit * gasprice)
  3. the amount of gas required is available in the block
  4. the purchased gas is enough to cover intrinsic usage
  5. there is no overflow when calculating intrinsic gas
  6. 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.