// (c) 2019-2020, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"math/big"
"sync"
"sync/atomic"
"time"
"github.com/ava-labs/coreth"
"github.com/ava-labs/coreth/core"
"github.com/ava-labs/coreth/eth"
"github.com/ava-labs/coreth/node"
"github.com/ava-labs/go-ethereum/common"
"github.com/ava-labs/go-ethereum/core/types"
"github.com/ava-labs/go-ethereum/rlp"
"github.com/ava-labs/go-ethereum/rpc"
"github.com/ava-labs/gecko/cache"
"github.com/ava-labs/gecko/database"
"github.com/ava-labs/gecko/ids"
"github.com/ava-labs/gecko/snow"
"github.com/ava-labs/gecko/snow/choices"
"github.com/ava-labs/gecko/snow/consensus/snowman"
"github.com/ava-labs/gecko/utils/timer"
commonEng "github.com/ava-labs/gecko/snow/engine/common"
)
const (
lastAcceptedKey = "snowman_lastAccepted"
)
const (
minBlockTime = 250 * time.Millisecond
maxBlockTime = 1000 * time.Millisecond
batchSize = 250
)
const (
bdTimerStateMin = iota
bdTimerStateMax
bdTimerStateLong
)
var (
errEmptyBlock = errors.New("empty block")
errCreateBlock = errors.New("couldn't create block")
errUnknownBlock = errors.New("unknown block")
errBlockFrequency = errors.New("too frequent block issuance")
errUnsupportedFXs = errors.New("unsupported feature extensions")
)
func maxDuration(x, y time.Duration) time.Duration {
if x > y {
return x
}
return y
}
// VM implements the snowman.ChainVM interface
type VM struct {
ctx *snow.Context
chainID *big.Int
networkID uint64
chain *coreth.ETHChain
chaindb Database
newBlockChan chan *Block
networkChan chan<- commonEng.Message
newTxPoolHeadChan chan core.NewTxPoolHeadEvent
txPoolStabilizedHead common.Hash
txPoolStabilizedOk chan struct{}
txPoolStabilizedLock sync.Mutex
metalock sync.Mutex
blockCache, blockStatusCache cache.LRU
lastAccepted *Block
writingMetadata uint32
bdlock sync.Mutex
blockDelayTimer *timer.Timer
bdTimerState int8
bdGenWaitFlag bool
bdGenFlag bool
genlock sync.Mutex
txSubmitChan <-chan struct{}
}
/*
******************************************************************************
********************************* Snowman API ********************************
******************************************************************************
*/
// Initialize implements the snowman.ChainVM interface
func (vm *VM) Initialize(
ctx *snow.Context,
db database.Database,
b []byte,
toEngine chan<- commonEng.Message,
fxs []*commonEng.Fx,
) error {
if len(fxs) > 0 {
return errUnsupportedFXs
}
vm.ctx = ctx
vm.chaindb = Database{db}
g := new(core.Genesis)
err := json.Unmarshal(b, g)
if err != nil {
return err
}
vm.chainID = g.Config.ChainID
config := eth.DefaultConfig
config.ManualCanonical = true
config.Genesis = g
config.Miner.ManualMining = true
config.Miner.DisableUncle = true
if err := config.SetGCMode("archive"); err != nil {
panic(err)
}
nodecfg := node.Config{NoUSB: true}
chain := coreth.NewETHChain(&config, &nodecfg, nil, vm.chaindb)
vm.chain = chain
vm.networkID = config.NetworkId
chain.SetOnHeaderNew(func(header *types.Header) {
hid := make([]byte, 32)
_, err := rand.Read(hid)
if err != nil {
panic("cannot generate hid")
}
header.Extra = append(header.Extra, hid...)
})
chain.SetOnSeal(func(block *types.Block) error {
if len(block.Transactions()) == 0 {
// this could happen due to the async logic of geth tx pool
vm.newBlockChan <- nil
return errEmptyBlock
}
return nil
})
chain.SetOnSealFinish(func(block *types.Block) error {
vm.ctx.Log.Verbo("EVM sealed a block")
blk := &Block{
id: ids.NewID(block.Hash()),
ethBlock: block,
vm: vm,
}
vm.newBlockChan <- blk
vm.updateStatus(ids.NewID(block.Hash()), choices.Processing)
vm.txPoolStabilizedLock.Lock()
vm.txPoolStabilizedHead = block.Hash()
vm.txPoolStabilizedLock.Unlock()
return nil
})
chain.SetOnQueryAcceptedBlock(func() *types.Block {
return vm.getLastAccepted().ethBlock
})
vm.blockCache = cache.LRU{Size: 2048}
vm.blockStatusCache = cache.LRU{Size: 1024}
vm.newBlockChan = make(chan *Block)
vm.networkChan = toEngine
vm.blockDelayTimer = timer.NewTimer(func() {
vm.bdlock.Lock()
switch vm.bdTimerState {
case bdTimerStateMin:
vm.bdTimerState = bdTimerStateMax
vm.blockDelayTimer.SetTimeoutIn(maxDuration(maxBlockTime-minBlockTime, 0))
case bdTimerStateMax:
vm.bdTimerState = bdTimerStateLong
}
tryAgain := vm.bdGenWaitFlag
vm.bdlock.Unlock()
if tryAgain {
vm.tryBlockGen()
}
})
go ctx.Log.RecoverAndPanic(vm.blockDelayTimer.Dispatch)
vm.bdTimerState = bdTimerStateLong
vm.bdGenWaitFlag = true
vm.newTxPoolHeadChan = make(chan core.NewTxPoolHeadEvent, 1)
vm.txPoolStabilizedOk = make(chan struct{}, 1)
chain.GetTxPool().SubscribeNewHeadEvent(vm.newTxPoolHeadChan)
// TODO: shutdown this go routine
go ctx.Log.RecoverAndPanic(func() {
for {
select {
case h := <-vm.newTxPoolHeadChan:
vm.txPoolStabilizedLock.Lock()
if vm.txPoolStabilizedHead == h.Block.Hash() {
vm.txPoolStabilizedOk <- struct{}{}
vm.txPoolStabilizedHead = common.Hash{}
}
vm.txPoolStabilizedLock.Unlock()
}
}
})
chain.Start()
var lastAccepted *types.Block
if b, err := vm.chaindb.Get([]byte(lastAcceptedKey)); err == nil {
var hash common.Hash
if err = rlp.DecodeBytes(b, &hash); err == nil {
if block := chain.GetBlockByHash(hash); block == nil {
vm.ctx.Log.Debug("lastAccepted block not found in chaindb")
} else {
lastAccepted = block
}
}
}
if lastAccepted == nil {
vm.ctx.Log.Debug("lastAccepted is unavailable, setting to the genesis block")
lastAccepted = chain.GetGenesisBlock()
}
vm.lastAccepted = &Block{
id: ids.NewID(lastAccepted.Hash()),
ethBlock: lastAccepted,
vm: vm,
}
vm.ctx.Log.Info(fmt.Sprintf("lastAccepted = %s", vm.lastAccepted.ethBlock.Hash().Hex()))
// TODO: shutdown this go routine
go vm.ctx.Log.RecoverAndPanic(func() {
vm.txSubmitChan = vm.chain