mirror of https://github.com/poanetwork/gecko.git
499 lines
13 KiB
Go
499 lines
13 KiB
Go
// (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.GetTxSubmitCh()
|
|
for {
|
|
select {
|
|
case <-vm.txSubmitChan:
|
|
vm.ctx.Log.Verbo("New tx detected, trying to generate a block")
|
|
vm.tryBlockGen()
|
|
case <-time.After(5 * time.Second):
|
|
vm.tryBlockGen()
|
|
}
|
|
}
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Shutdown implements the snowman.ChainVM interface
|
|
func (vm *VM) Shutdown() {
|
|
vm.writeBackMetadata()
|
|
vm.chain.Stop()
|
|
}
|
|
|
|
// BuildBlock implements the snowman.ChainVM interface
|
|
func (vm *VM) BuildBlock() (snowman.Block, error) {
|
|
vm.chain.GenBlock()
|
|
block := <-vm.newBlockChan
|
|
if block == nil {
|
|
return nil, errCreateBlock
|
|
}
|
|
// reset the min block time timer
|
|
vm.bdlock.Lock()
|
|
vm.bdTimerState = bdTimerStateMin
|
|
vm.bdGenWaitFlag = false
|
|
vm.bdGenFlag = false
|
|
vm.blockDelayTimer.SetTimeoutIn(minBlockTime)
|
|
vm.bdlock.Unlock()
|
|
|
|
vm.ctx.Log.Debug("built block 0x%x", block.ID().Bytes())
|
|
// make sure Tx Pool is updated
|
|
<-vm.txPoolStabilizedOk
|
|
return block, nil
|
|
}
|
|
|
|
// ParseBlock implements the snowman.ChainVM interface
|
|
func (vm *VM) ParseBlock(b []byte) (snowman.Block, error) {
|
|
vm.metalock.Lock()
|
|
defer vm.metalock.Unlock()
|
|
|
|
ethBlock := new(types.Block)
|
|
if err := rlp.DecodeBytes(b, ethBlock); err != nil {
|
|
return nil, err
|
|
}
|
|
block := &Block{
|
|
id: ids.NewID(ethBlock.Hash()),
|
|
ethBlock: ethBlock,
|
|
vm: vm,
|
|
}
|
|
vm.blockCache.Put(block.ID(), block)
|
|
return block, nil
|
|
}
|
|
|
|
// GetBlock implements the snowman.ChainVM interface
|
|
func (vm *VM) GetBlock(id ids.ID) (snowman.Block, error) {
|
|
vm.metalock.Lock()
|
|
defer vm.metalock.Unlock()
|
|
|
|
block := vm.getBlock(id)
|
|
if block == nil {
|
|
return nil, errUnknownBlock
|
|
}
|
|
return block, nil
|
|
}
|
|
|
|
// SetPreference sets what the current tail of the chain is
|
|
func (vm *VM) SetPreference(blkID ids.ID) {
|
|
err := vm.chain.SetTail(blkID.Key())
|
|
vm.ctx.Log.AssertNoError(err)
|
|
}
|
|
|
|
// LastAccepted returns the ID of the block that was last accepted
|
|
func (vm *VM) LastAccepted() ids.ID {
|
|
vm.metalock.Lock()
|
|
defer vm.metalock.Unlock()
|
|
|
|
return vm.lastAccepted.ID()
|
|
}
|
|
|
|
// CreateHandlers makes new http handlers that can handle API calls
|
|
func (vm *VM) CreateHandlers() map[string]*commonEng.HTTPHandler {
|
|
handler := vm.chain.NewRPCHandler()
|
|
vm.chain.AttachEthService(handler, []string{"eth", "personal", "txpool"})
|
|
handler.RegisterName("net", &NetAPI{vm})
|
|
handler.RegisterName("snowman", &SnowmanAPI{vm})
|
|
handler.RegisterName("web3", &Web3API{})
|
|
handler.RegisterName("debug", &DebugAPI{vm})
|
|
|
|
return map[string]*commonEng.HTTPHandler{
|
|
"/rpc": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler},
|
|
"/ws": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler.WebsocketHandler([]string{"*"})},
|
|
}
|
|
}
|
|
|
|
// CreateStaticHandlers makes new http handlers that can handle API calls
|
|
func (vm *VM) CreateStaticHandlers() map[string]*commonEng.HTTPHandler {
|
|
handler := rpc.NewServer()
|
|
handler.RegisterName("static", &StaticService{})
|
|
return map[string]*commonEng.HTTPHandler{
|
|
"/rpc": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler},
|
|
"/ws": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler.WebsocketHandler([]string{"*"})},
|
|
}
|
|
}
|
|
|
|
/*
|
|
******************************************************************************
|
|
*********************************** Helpers **********************************
|
|
******************************************************************************
|
|
*/
|
|
|
|
func (vm *VM) updateStatus(blockID ids.ID, status choices.Status) {
|
|
vm.metalock.Lock()
|
|
defer vm.metalock.Unlock()
|
|
|
|
if status == choices.Accepted {
|
|
vm.lastAccepted = vm.getBlock(blockID)
|
|
// TODO: improve this naive implementation
|
|
if atomic.SwapUint32(&vm.writingMetadata, 1) == 0 {
|
|
go vm.ctx.Log.RecoverAndPanic(vm.writeBackMetadata)
|
|
}
|
|
}
|
|
vm.blockStatusCache.Put(blockID, status)
|
|
}
|
|
|
|
func (vm *VM) getCachedBlock(blockID ids.ID) *types.Block {
|
|
return vm.chain.GetBlockByHash(blockID.Key())
|
|
}
|
|
|
|
func (vm *VM) tryBlockGen() error {
|
|
vm.bdlock.Lock()
|
|
defer vm.bdlock.Unlock()
|
|
if vm.bdGenFlag {
|
|
// skip if one call already generates a block in this round
|
|
return nil
|
|
}
|
|
vm.bdGenWaitFlag = true
|
|
|
|
vm.genlock.Lock()
|
|
defer vm.genlock.Unlock()
|
|
// get pending size
|
|
size, err := vm.chain.PendingSize()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if size == 0 {
|
|
return nil
|
|
}
|
|
|
|
switch vm.bdTimerState {
|
|
case bdTimerStateMin:
|
|
return nil
|
|
case bdTimerStateMax:
|
|
if size < batchSize {
|
|
return nil
|
|
}
|
|
case bdTimerStateLong:
|
|
// timeout; go ahead and generate a new block anyway
|
|
}
|
|
select {
|
|
case vm.networkChan <- commonEng.PendingTxs:
|
|
// successfully push out the notification; this round ends
|
|
vm.bdGenFlag = true
|
|
default:
|
|
return errBlockFrequency
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (vm *VM) getCachedStatus(blockID ids.ID) choices.Status {
|
|
vm.metalock.Lock()
|
|
defer vm.metalock.Unlock()
|
|
status := choices.Processing
|
|
|
|
if statusIntf, ok := vm.blockStatusCache.Get(blockID); ok {
|
|
status = statusIntf.(choices.Status)
|
|
} else {
|
|
blk := vm.chain.GetBlockByHash(blockID.Key())
|
|
if blk == nil {
|
|
return choices.Unknown
|
|
}
|
|
acceptedBlk := vm.lastAccepted.ethBlock
|
|
|
|
// TODO: There must be a better way of doing this.
|
|
// Traverse up the chain from the lower block until the indices match
|
|
highBlock := blk
|
|
lowBlock := acceptedBlk
|
|
if highBlock.Number().Cmp(lowBlock.Number()) < 0 {
|
|
highBlock, lowBlock = lowBlock, highBlock
|
|
}
|
|
for highBlock.Number().Cmp(lowBlock.Number()) > 0 {
|
|
highBlock = vm.chain.GetBlockByHash(highBlock.ParentHash())
|
|
}
|
|
|
|
if highBlock.Hash() == lowBlock.Hash() { // on the same branch
|
|
if blk.Number().Cmp(acceptedBlk.Number()) <= 0 {
|
|
status = choices.Accepted
|
|
}
|
|
} else { // on different branches
|
|
status = choices.Rejected
|
|
}
|
|
}
|
|
|
|
vm.blockStatusCache.Put(blockID, status)
|
|
return status
|
|
}
|
|
|
|
func (vm *VM) getBlock(id ids.ID) *Block {
|
|
if blockIntf, ok := vm.blockCache.Get(id); ok {
|
|
return blockIntf.(*Block)
|
|
}
|
|
ethBlock := vm.getCachedBlock(id)
|
|
if ethBlock == nil {
|
|
return nil
|
|
}
|
|
block := &Block{
|
|
id: ids.NewID(ethBlock.Hash()),
|
|
ethBlock: ethBlock,
|
|
vm: vm,
|
|
}
|
|
vm.blockCache.Put(id, block)
|
|
return block
|
|
}
|
|
|
|
func (vm *VM) issueRemoteTxs(txs []*types.Transaction) error {
|
|
errs := vm.chain.AddRemoteTxs(txs)
|
|
for _, err := range errs {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return vm.tryBlockGen()
|
|
}
|
|
|
|
func (vm *VM) writeBackMetadata() {
|
|
vm.metalock.Lock()
|
|
defer vm.metalock.Unlock()
|
|
|
|
b, err := rlp.EncodeToBytes(vm.lastAccepted.ethBlock.Hash())
|
|
if err != nil {
|
|
vm.ctx.Log.Error("snowman-eth: error while writing back metadata")
|
|
return
|
|
}
|
|
vm.ctx.Log.Debug("writing back metadata")
|
|
vm.chaindb.Put([]byte(lastAcceptedKey), b)
|
|
atomic.StoreUint32(&vm.writingMetadata, 0)
|
|
}
|
|
|
|
func (vm *VM) getLastAccepted() *Block {
|
|
vm.metalock.Lock()
|
|
defer vm.metalock.Unlock()
|
|
|
|
return vm.lastAccepted
|
|
}
|