Files

337 lines
11 KiB
Go

// (c) 2020-2020, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package evm
import (
"bytes"
"context"
"fmt"
"time"
"github.com/luxfi/evm/commontype"
"github.com/luxfi/evm/core"
"github.com/luxfi/evm/core/rawdb"
"github.com/luxfi/evm/core/types"
evmids "github.com/luxfi/evm/ids"
"github.com/luxfi/evm/params/extras"
"github.com/luxfi/evm/plugin/evm/header"
"github.com/luxfi/evm/precompile/precompileconfig"
"github.com/luxfi/evm/predicate"
"github.com/luxfi/log"
"github.com/luxfi/geth/rlp"
"github.com/luxfi/node/v2/quasar/engine/chain/block"
"github.com/luxfi/node/v2/quasar/choices"
"github.com/luxfi/ids"
)
var (
_ block.Block = (*Block)(nil)
_ block.WithVerifyContext = (*Block)(nil)
)
// Block implements the chain.Block interface
type Block struct {
id ids.ID
ethBlock *types.Block
vm *VM
}
// newBlock returns a new Block wrapping the ethBlock type and implementing the chain.Block interface
func (vm *VM) newBlock(ethBlock *types.Block) *Block {
return &Block{
id: ids.ID(ethBlock.Hash()),
ethBlock: ethBlock,
vm: vm,
}
}
// ID implements the block.Block interface (choices.Decidable)
func (b *Block) ID() string { return b.id.String() }
// Accept implements the chain.Block interface
func (b *Block) Accept() error {
vm := b.vm
// Although returning an error from Accept is considered fatal, it is good
// practice to cleanup the batch we were modifying in the case of an error.
if vm.versiondb != nil {
defer vm.versiondb.Abort()
}
blkID := ids.ID(b.id)
log.Debug("accepting block",
"hash", blkID.String(),
"id", blkID,
"height", b.Height(),
)
// Call Accept for relevant precompile logs. Note we do this prior to
// calling Accept on the blockChain so any side effects (eg warp signatures)
// take place before the accepted log is emitted to subscribers. Use of the
// sharedMemoryWriter ensures shared memory requests generated by
// precompiles are committed atomically with the vm's lastAcceptedKey.
rules := b.vm.rules(b.ethBlock.Number(), b.ethBlock.Timestamp())
if err := b.handlePrecompileAccept(rules); err != nil {
return err
}
if err := vm.blockChain.Accept(b.ethBlock); err != nil {
return fmt.Errorf("chain could not accept %s: %w", blkID, err)
}
if err := vm.acceptedBlockDB.Put(lastAcceptedKey, blkID[:]); err != nil {
return fmt.Errorf("failed to put %s as the last accepted block: %w", blkID, err)
}
if b.vm.versiondb != nil {
return b.vm.versiondb.Commit()
}
return nil
}
// handlePrecompileAccept calls Accept on any logs generated with an active precompile address that implements
// contract.Accepter
func (b *Block) handlePrecompileAccept(rules extras.Rules) error {
// Short circuit early if there are no precompile accepters to execute
if len(rules.AccepterPrecompiles) == 0 {
return nil
}
// Read receipts from disk
receipts := rawdb.ReadReceipts(b.vm.chaindb, b.ethBlock.Hash(), b.ethBlock.NumberU64(), b.ethBlock.Time(), b.vm.chainConfig)
// If there are no receipts, ReadReceipts may be nil, so we check the length and confirm the ReceiptHash
// is empty to ensure that missing receipts results in an error on accept.
if len(receipts) == 0 && b.ethBlock.ReceiptHash() != types.EmptyRootHash {
return fmt.Errorf("failed to fetch receipts for accepted block with non-empty root hash (%s) (Block: %s, Height: %d)", b.ethBlock.ReceiptHash(), b.ethBlock.Hash(), b.ethBlock.NumberU64())
}
// Convert consensus.Context to commontype.ChainContext
chainCtx := &commontype.ChainContext{
NetworkID: b.vm.ctx.NetworkID,
SubnetID: evmids.SubnetID(b.vm.ctx.SubnetID),
ChainID: evmids.ChainID(b.vm.ctx.ChainID),
NodeID: func() evmids.NodeID {
var nodeID evmids.NodeID
copy(nodeID[:], b.vm.ctx.NodeID[:])
return nodeID
}(),
AppVersion: 0, // TODO: Get app version
ChainDataDir: "", // TODO: Get chain data dir
ValidatorState: nil, // TODO: Implement ValidatorState interface
}
acceptCtx := &precompileconfig.AcceptContext{
ConsensusCtx: chainCtx,
Warp: nil, // TODO: implement WarpMessageWriter wrapper
}
for _, receipt := range receipts {
for logIdx, log := range receipt.Logs {
accepter, ok := rules.AccepterPrecompiles[log.Address]
if !ok {
continue
}
if err := accepter.Accept(acceptCtx, log.BlockHash, log.BlockNumber, log.TxHash, logIdx, log.Topics, log.Data); err != nil {
return err
}
}
}
return nil
}
// Reject implements the chain.Block interface
func (b *Block) Reject() error {
blkID := ids.ID(b.id)
log.Debug("rejecting block",
"hash", blkID.String(),
"id", blkID,
"height", b.Height(),
)
return b.vm.blockChain.Reject(b.ethBlock)
}
// Parent implements the chain.Block interface
func (b *Block) Parent() ids.ID {
return ids.ID(b.ethBlock.ParentHash())
}
// Height implements the chain.Block interface
func (b *Block) Height() uint64 {
return b.ethBlock.NumberU64()
}
// Timestamp implements the chain.Block interface (deprecated - use Time())
func (b *Block) Timestamp() time.Time {
return time.Unix(int64(b.ethBlock.Time()), 0)
}
// Time implements the chain.Block interface
func (b *Block) Time() uint64 {
return b.ethBlock.Time()
}
// syntacticVerify verifies that a *Block is well-formed.
func (b *Block) syntacticVerify() error {
if b == nil || b.ethBlock == nil {
return errInvalidBlock
}
header := b.ethBlock.Header()
rules := b.vm.chainConfig.Rules(header.Number, header.Time)
return b.vm.syntacticBlockValidator.SyntacticVerify(b, rules)
}
// Verify implements the chain.Block interface
func (b *Block) Verify(context.Context) error {
// Convert consensus.Context to commontype.ChainContext
chainCtx := &commontype.ChainContext{
NetworkID: b.vm.ctx.NetworkID,
SubnetID: evmids.SubnetID(b.vm.ctx.SubnetID),
ChainID: evmids.ChainID(b.vm.ctx.ChainID),
NodeID: func() evmids.NodeID {
var nodeID evmids.NodeID
copy(nodeID[:], b.vm.ctx.NodeID[:])
return nodeID
}(),
AppVersion: 0, // TODO: Get app version
ChainDataDir: "", // TODO: Get chain data dir
ValidatorState: nil, // TODO: Implement ValidatorState interface
}
return b.verify(&precompileconfig.PredicateContext{
ConsensusCtx: chainCtx,
ProposerVMBlockCtx: nil,
}, true)
}
// ShouldVerifyWithContext implements the iface.WithVerifyContext interface
func (b *Block) ShouldVerifyWithContext(context.Context) (bool, error) {
rules := b.vm.rules(b.ethBlock.Number(), b.ethBlock.Timestamp())
predicates := rules.Predicaters
// Short circuit early if there are no predicates to verify
if len(predicates) == 0 {
return false, nil
}
// Check if any of the transactions in the block specify a precompile that enforces a predicate, which requires
// the ProposerVMBlockCtx.
for _, tx := range b.ethBlock.Transactions() {
for _, accessTuple := range tx.AccessList() {
if _, ok := predicates[accessTuple.Address]; ok {
log.Debug("Block verification requires proposerVM context", "block", b.ID(), "height", b.Height())
return true, nil
}
}
}
log.Debug("Block verification does not require proposerVM context", "block", b.ID(), "height", b.Height())
return false, nil
}
// VerifyWithContext implements the block.WithVerifyContext interface
func (b *Block) VerifyWithContext(ctx context.Context, proposerVMBlockCtx *block.Context) error {
// Convert consensus.Context to commontype.ChainContext
chainCtx := &commontype.ChainContext{
NetworkID: b.vm.ctx.NetworkID,
SubnetID: evmids.SubnetID(b.vm.ctx.SubnetID),
ChainID: evmids.ChainID(b.vm.ctx.ChainID),
NodeID: func() evmids.NodeID {
var nodeID evmids.NodeID
copy(nodeID[:], b.vm.ctx.NodeID[:])
return nodeID
}(),
AppVersion: 0, // TODO: Get app version
ChainDataDir: "", // TODO: Get chain data dir
ValidatorState: nil, // TODO: Implement ValidatorState interface
}
// Convert block.Context to commontype.BlockContext
var blockCtx *commontype.BlockContext
if proposerVMBlockCtx != nil {
blockCtx = &commontype.BlockContext{
PChainHeight: proposerVMBlockCtx.PChainHeight,
}
}
return b.verify(&precompileconfig.PredicateContext{
ConsensusCtx: chainCtx,
ProposerVMBlockCtx: blockCtx,
}, true)
}
// Verify the block is valid.
// Enforces that the predicates are valid within [predicateContext].
// Writes the block details to disk and the state to the trie manager iff writes=true.
func (b *Block) verify(predicateContext *precompileconfig.PredicateContext, writes bool) error {
if predicateContext.ProposerVMBlockCtx != nil {
log.Debug("Verifying block with context", "block", b.ID(), "height", b.Height())
} else {
log.Debug("Verifying block without context", "block", b.ID(), "height", b.Height())
}
if err := b.syntacticVerify(); err != nil {
return fmt.Errorf("syntactic block verification failed: %w", err)
}
// Only enforce predicates if the chain has already bootstrapped.
// If the chain is still bootstrapping, we can assume that all blocks we are verifying have
// been accepted by the network (so the predicate was validated by the network when the
// block was originally verified).
if b.vm.bootstrapped.Get() {
if err := b.verifyPredicates(predicateContext); err != nil {
return fmt.Errorf("failed to verify predicates: %w", err)
}
}
// The engine may call VerifyWithContext multiple times on the same block with different contexts.
// Since the engine will only call Accept/Reject once, we should only call InsertBlockManual once.
// Additionally, if a block is already in processing, then it has already passed verification and
// at this point we have checked the predicates are still valid in the different context so we
// can return nil.
if b.vm.State.IsProcessing(b.id) {
return nil
}
return b.vm.blockChain.InsertBlockManual(b.ethBlock, writes)
}
// verifyPredicates verifies the predicates in the block are valid according to predicateContext.
func (b *Block) verifyPredicates(predicateContext *precompileconfig.PredicateContext) error {
// For v2.0.0, all upgrades are active including Durango
// Skip the upgrade check since we always support predicates
predicateResults := predicate.NewResults()
for _, tx := range b.ethBlock.Transactions() {
// Convert to params.Rules for CheckPredicates
rules := b.vm.chainConfig.Rules(b.ethBlock.Number(), b.ethBlock.Timestamp())
results, err := core.CheckPredicates(rules, predicateContext, tx)
if err != nil {
return err
}
predicateResults.SetTxResults(tx.Hash(), results)
}
// TODO: document required gas constraints to ensure marshalling predicate results does not error
predicateResultsBytes, err := predicateResults.Bytes()
if err != nil {
return fmt.Errorf("failed to marshal predicate results: %w", err)
}
extraData := b.ethBlock.Extra()
headerPredicateResultsBytes := header.PredicateBytesFromExtra(extraData)
if !bytes.Equal(headerPredicateResultsBytes, predicateResultsBytes) {
return fmt.Errorf("%w (remote: %x local: %x)", errInvalidHeaderPredicateResults, headerPredicateResultsBytes, predicateResultsBytes)
}
return nil
}
// Bytes implements the chain.Block interface
func (b *Block) Bytes() []byte {
res, err := rlp.EncodeToBytes(b.ethBlock)
if err != nil {
panic(err)
}
return res
}
// Status implements the choices.Decidable interface
func (b *Block) Status() choices.Status {
// TODO: Implement proper status tracking
// For now, return Processing as blocks are being processed
return choices.Processing
}
func (b *Block) String() string { return fmt.Sprintf("EVM block, ID = %s", b.ID()) }