gecko/vms/platformvm/common_blocks.go

249 lines
8.5 KiB
Go

// (c) 2019-2020, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package platformvm
import (
"errors"
"github.com/ava-labs/gecko/ids"
"github.com/ava-labs/gecko/vms/components/missing"
"github.com/ava-labs/gecko/database"
"github.com/ava-labs/gecko/database/versiondb"
"github.com/ava-labs/gecko/snow/choices"
"github.com/ava-labs/gecko/snow/consensus/snowman"
"github.com/ava-labs/gecko/vms/components/core"
)
// When one stakes, one must specify the time one will start to validate and
// the time one will stop validating. The latter must be after the former, and both
// times must be in the future.
//
// When one wants to start staking:
// * They issue a transaction to that effect to an existing staker.
// * The staker checks whether the specified "start staking time" is in the past relative to
// their wall clock.
// ** If so, the staker ignores the transaction
// ** If not, the staker issues a proposal block (see below) on behalf of the would-be staker.
//
// When one is done staking:
// * The staking set decides whether the staker should receive either:
// ** Only the $AVA that the staker put up as a bond
// ** The $AVA the staker put up as a bond, and also a reward for staking
//
// This chain has three types of blocks:
// 1. A proposal block
// - Contains a proposal to do one of the following:
// * Change the chain time from t to t'. (This doubles as
// proposing to update the staking set.)
// ** It must be that: t' > t
// ** It must be that: t' <= [time at which the next staker stops staking]
// * Reward a staker upon their leaving the staking pool
// ** It must be that chain time == [time for this staker to stop staking]
// ** It must be that this staker is the next staker to stop staking
// * Add a staker to the staking pool
// ** It must be that: staker.startTime < chain time
// - A proposal block is always followed by either a commit block or a rejection block
// 2. A commit block
// - Does one of the following:
// * Approve a proposal to change the chain time from t to t'
// ** This should be the initial preference if t' <= Wall clock time
// * Approve a proposal to reward for a staker upon their leaving the staking pool
// ** It must be that: chain time == [time for this staker to stop staking]
// ** It must be that: this staker is the next staker to stop staking
// * Approve a proposal to add a staker to the staking pool
// ** This should be the initial preference if staker.startTime > Wall clock
// time
// - A commit block must always be preceded on the chain by the proposal block whose
// proposal is being commited
// 3. A rejection block
// - Does one of the following:
// * Reject a proposal to change the chain time from t to t' (therefore keeping it at t)
// ** This should be the initial preference if t' > [this node's wall clock time + Delta],
// where Delta is our synchrony assumption
// * Reject a proposal to reward for a staker upon their leaving the staking pool.
// ** The staker only has their bond (locked tokens) returned
// ** This should be the initial preference if the staker has had < Chi uptime
// ** It must be that: t == [time for this staker to stop staking]
// ** It must be that: this staker is the next staker to stop staking
// * Reject a proposal to add a staker to the staking set.
// ** Increase the timestamp to the would-be staker's start time
// ** This should be the initial preference if staker.startTime <= Wall clock
// time
// - A rejection block must always be preceded on the chain by the proposal block whose
// proposal is being rejected
var (
errInvalidBlockType = errors.New("invalid block type")
errEmptyValidatingSet = errors.New("empty validating set")
)
// Block is the common interface that all staking blocks must have
type Block interface {
snowman.Block
// initialize this block's non-serialized fields.
// This method should be called when a block is unmarshaled from bytes.
// [vm] is the vm the block exists in
// [bytes] is the byte representation of this block
initialize(vm *VM, bytes []byte) error
conflicts(ids.Set) bool
// parentBlock returns the parent block, similarly to Parent. However, it
// provides the more specific staking.Block interface.
parentBlock() Block
// addChild notifies this block that it has a child block building on
// its database. When this block commits its database, it should set the
// child's database to the former's underlying database instance. This ensures that
// the database versions do not recurse the length of the chain.
addChild(Block)
// free all the references of this block from the vm's memory
free()
// Set the database underlying the block's versiondb's to [db]
setBaseDatabase(db database.Database)
}
// A decision block (either Commit, Abort, or DecisionBlock.) represents a
// decision to either commit (accept) or abort (reject) the changes specified in
// its parent, if its parent is a proposal. Otherwise, the changes are committed
// immediately.
type decision interface {
// This function should only be called after Verify is called.
// returns a database that contains the state of the chain if this block is
// accepted.
onAccept() database.Database
}
// CommonBlock contains the fields common to all blocks of the Platform Chain
type CommonBlock struct {
*core.Block `serialize:"true"`
vm *VM
// This block's parent.
// nil before parentBlock() is called on this block
parent Block
// This block's children
children []Block
}
// Reject implements the snowman.Block interface
func (cb *CommonBlock) Reject() {
defer cb.free() // remove this block from memory
cb.Block.Reject()
}
// free removes this block from memory
func (cb *CommonBlock) free() {
delete(cb.vm.currentBlocks, cb.ID().Key())
cb.parent = nil
cb.children = nil
}
// Reject implements the snowman.Block interface
func (cb *CommonBlock) conflicts(s ids.Set) bool {
if cb.Status() == choices.Accepted {
return false
}
return cb.parentBlock().conflicts(s)
}
// Parent returns this block's parent
func (cb *CommonBlock) Parent() snowman.Block {
parent := cb.parentBlock()
if parent != nil {
return parent
}
return &missing.Block{BlkID: cb.ParentID()}
}
// parentBlock returns this block's parent
func (cb *CommonBlock) parentBlock() Block {
// Check if the block already has a reference to its parent
if cb.parent != nil {
return cb.parent
}
// Get the parent from database
parentID := cb.ParentID()
parent, err := cb.vm.getBlock(parentID)
if err != nil {
cb.vm.Ctx.Log.Warn("could not get parent (ID %s) of block %s", parentID, cb.ID())
return nil
}
cb.parent = parent
return parent.(Block)
}
// addChild adds [child] as a child of this block
func (cb *CommonBlock) addChild(child Block) { cb.children = append(cb.children, child) }
// CommonDecisionBlock contains the fields and methods common to all decision blocks
// (ie *Commit and *Abort)
type CommonDecisionBlock struct {
CommonBlock `serialize:"true"`
// state of the chain if this block is accepted
onAcceptDB *versiondb.Database
// to be executed if this block is accepted
onAcceptFunc func()
}
// initialize this block
func (cdb *CommonDecisionBlock) initialize(vm *VM, bytes []byte) error {
cdb.vm = vm
cdb.Block.Initialize(bytes, vm.SnowmanVM)
return nil
}
// setBaseDatabase sets this block's base database to [db]
func (cdb *CommonDecisionBlock) setBaseDatabase(db database.Database) {
if err := cdb.onAcceptDB.SetDatabase(db); err != nil {
cdb.vm.Ctx.Log.Error("problem while setting base database: %s", err)
}
}
// onAccept returns:
// 1) The state of the chain if this block is accepted
// 2) The function to execute if this block is accepted
func (cdb *CommonDecisionBlock) onAccept() database.Database {
if cdb.Status().Decided() {
return cdb.vm.DB
}
return cdb.onAcceptDB
}
// Accept implements the snowman.Block interface
func (cdb *CommonDecisionBlock) Accept() {
cdb.VM.Ctx.Log.Verbo("Accepting block with ID %s", cdb.ID())
cdb.CommonBlock.Accept()
// Update the state of the chain in the database
if err := cdb.onAcceptDB.Commit(); err != nil {
cdb.vm.Ctx.Log.Warn("unable to commit onAcceptDB")
}
if err := cdb.vm.DB.Commit(); err != nil {
cdb.vm.Ctx.Log.Warn("unable to commit vm's DB")
}
for _, child := range cdb.children {
child.setBaseDatabase(cdb.vm.DB)
}
if cdb.onAcceptFunc != nil {
cdb.onAcceptFunc()
}
parent := cdb.parentBlock()
// remove this block and its parent from memory
parent.free()
cdb.free()
}