287 lines
8.2 KiB
Go
287 lines
8.2 KiB
Go
package validator
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/consensus"
|
|
"github.com/ethereum/go-ethereum/consensus/clique"
|
|
"github.com/ethereum/go-ethereum/consensus/ethash"
|
|
"github.com/ethereum/go-ethereum/core"
|
|
"github.com/ethereum/go-ethereum/core/state"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/core/vm"
|
|
"github.com/ethereum/go-ethereum/params"
|
|
"github.com/ethereum/go-ethereum/rpc"
|
|
"github.com/ethereum/go-ethereum/statediff/indexer/node"
|
|
"github.com/ethereum/go-ethereum/statediff/indexer/postgres"
|
|
"github.com/jmoiron/sqlx"
|
|
log "github.com/sirupsen/logrus"
|
|
ipfsethdb "github.com/vulcanize/ipfs-ethdb/postgres"
|
|
ipldEth "github.com/vulcanize/ipld-eth-server/pkg/eth"
|
|
ethServerShared "github.com/vulcanize/ipld-eth-server/pkg/shared"
|
|
)
|
|
|
|
var (
|
|
big8 = big.NewInt(8)
|
|
big32 = big.NewInt(32)
|
|
testHash = common.HexToHash("0x1283a0bca5cce009bcf3e5a860eccdc202d1345f464024f2ee8ea1e1254349e7")
|
|
)
|
|
|
|
type service struct {
|
|
db *postgres.DB
|
|
blockNum, trail uint64
|
|
logger *log.Logger
|
|
}
|
|
|
|
func NewService(db *postgres.DB, blockNum, trailNum uint64) *service {
|
|
return &service{
|
|
db: db,
|
|
blockNum: blockNum,
|
|
trail: trailNum,
|
|
logger: log.New(),
|
|
}
|
|
}
|
|
|
|
func NewEthBackend(db *postgres.DB, c *ipldEth.Config) (*ipldEth.Backend, error) {
|
|
gcc := c.GroupCacheConfig
|
|
|
|
groupName := gcc.StateDB.Name
|
|
if groupName == "" {
|
|
groupName = ipldEth.StateDBGroupCacheName
|
|
}
|
|
|
|
r := ipldEth.NewCIDRetriever(db)
|
|
ethDB := ipfsethdb.NewDatabase(db.DB, ipfsethdb.CacheConfig{
|
|
Name: groupName,
|
|
Size: gcc.StateDB.CacheSizeInMB * 1024 * 1024,
|
|
ExpiryDuration: time.Minute * time.Duration(gcc.StateDB.CacheExpiryInMins),
|
|
})
|
|
|
|
customEthDB := newDatabase(ethDB)
|
|
|
|
return &ipldEth.Backend{
|
|
DB: db,
|
|
Retriever: r,
|
|
Fetcher: ipldEth.NewIPLDFetcher(db),
|
|
IPLDRetriever: ipldEth.NewIPLDRetriever(db),
|
|
EthDB: customEthDB,
|
|
StateDatabase: state.NewDatabase(customEthDB),
|
|
Config: c,
|
|
}, nil
|
|
}
|
|
|
|
func NewDB(connectString string, config postgres.ConnectionConfig, node node.Info) (*postgres.DB, error) {
|
|
db, connectErr := sqlx.Connect("postgres", connectString)
|
|
if connectErr != nil {
|
|
return &postgres.DB{}, postgres.ErrDBConnectionFailed(connectErr)
|
|
}
|
|
if config.MaxOpen > 0 {
|
|
db.SetMaxOpenConns(config.MaxOpen)
|
|
}
|
|
if config.MaxIdle > 0 {
|
|
db.SetMaxIdleConns(config.MaxIdle)
|
|
}
|
|
if config.MaxLifetime > 0 {
|
|
lifetime := time.Duration(config.MaxLifetime) * time.Second
|
|
db.SetConnMaxLifetime(lifetime)
|
|
}
|
|
pg := postgres.DB{DB: db, Node: node}
|
|
return &pg, nil
|
|
}
|
|
|
|
// Start is used to begin the service
|
|
func (s *service) Start(ctx context.Context) (uint64, error) {
|
|
api, err := ethAPI(ctx, s.db)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
idxBlockNum := s.blockNum
|
|
headBlock, _ := api.B.BlockByNumber(ctx, rpc.LatestBlockNumber)
|
|
headBlockNum := headBlock.NumberU64()
|
|
|
|
for headBlockNum-s.trail >= idxBlockNum {
|
|
validateBlock, err := api.B.BlockByNumber(ctx, rpc.BlockNumber(idxBlockNum))
|
|
if err != nil {
|
|
return idxBlockNum, err
|
|
}
|
|
|
|
stateDB, err := applyTransaction(validateBlock, api.B)
|
|
if err != nil {
|
|
return idxBlockNum, err
|
|
}
|
|
|
|
blockStateRoot := validateBlock.Header().Root.String()
|
|
|
|
dbStateRoot := stateDB.IntermediateRoot(true).String()
|
|
if blockStateRoot != dbStateRoot {
|
|
s.logger.Errorf("failed to verify state root at block %d", idxBlockNum)
|
|
return idxBlockNum, fmt.Errorf("failed to verify state root at block")
|
|
}
|
|
|
|
s.logger.Infof("state root verified for block= %d", idxBlockNum)
|
|
|
|
// again fetch head block
|
|
headBlock, err = api.B.BlockByNumber(ctx, rpc.LatestBlockNumber)
|
|
if err != nil {
|
|
return idxBlockNum, err
|
|
}
|
|
|
|
headBlockNum = headBlock.NumberU64()
|
|
idxBlockNum++
|
|
}
|
|
|
|
s.logger.Infof("last validated block %v", idxBlockNum)
|
|
|
|
return idxBlockNum, nil
|
|
}
|
|
|
|
func ethAPI(ctx context.Context, db *postgres.DB) (*ipldEth.PublicEthAPI, error) {
|
|
// TODO: decide network for custom chainConfig.
|
|
backend, err := NewEthBackend(db, &ipldEth.Config{
|
|
GroupCacheConfig: ðServerShared.GroupCacheConfig{
|
|
StateDB: ethServerShared.GroupConfig{
|
|
Name: "vulcanize_validator",
|
|
},
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var genesisBlock *types.Block
|
|
if backend.Config.ChainConfig == nil {
|
|
genesisBlock, err = backend.BlockByNumber(ctx, rpc.BlockNumber(0))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
backend.Config.ChainConfig = setChainConfig(genesisBlock.Hash())
|
|
}
|
|
|
|
return ipldEth.NewPublicEthAPI(backend, nil, false, false, false)
|
|
}
|
|
|
|
// applyTransaction attempts to apply a transaction to the given state database
|
|
// and uses the input parameters for its environment. It returns the stateDB of parent with applied transa
|
|
func applyTransaction(block *types.Block, backend *ipldEth.Backend) (*state.StateDB, error) {
|
|
if block.NumberU64() == 0 {
|
|
return nil, errors.New("no transaction in genesis")
|
|
}
|
|
|
|
// Create the parent state database
|
|
parentHash := block.ParentHash()
|
|
parentRPCBlockHash := rpc.BlockNumberOrHash{
|
|
BlockHash: &parentHash,
|
|
RequireCanonical: false,
|
|
}
|
|
|
|
parent, _ := backend.BlockByNumberOrHash(context.Background(), parentRPCBlockHash)
|
|
if parent == nil {
|
|
return nil, fmt.Errorf("parent %#x not found", block.ParentHash())
|
|
}
|
|
|
|
stateDB, _, err := backend.StateAndHeaderByNumberOrHash(context.Background(), parentRPCBlockHash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
signer := types.MakeSigner(backend.Config.ChainConfig, block.Number())
|
|
|
|
for idx, tx := range block.Transactions() {
|
|
// Assemble the transaction call message and return if the requested offset
|
|
msg, _ := tx.AsMessage(signer, block.BaseFee())
|
|
txContext := core.NewEVMTxContext(msg)
|
|
ctx := core.NewEVMBlockContext(block.Header(), backend, getAuthor(backend, block.Header()))
|
|
|
|
// Not yet the searched for transaction, execute on top of the current state
|
|
newEVM := vm.NewEVM(ctx, txContext, stateDB, backend.Config.ChainConfig, vm.Config{})
|
|
|
|
stateDB.Prepare(tx.Hash(), idx)
|
|
if _, err := core.ApplyMessage(newEVM, msg, new(core.GasPool).AddGas(block.GasLimit())); err != nil {
|
|
return nil, fmt.Errorf("transaction %#x failed: %w", tx.Hash(), err)
|
|
}
|
|
}
|
|
|
|
if backend.Config.ChainConfig.Ethash != nil {
|
|
accumulateRewards(backend.Config.ChainConfig, stateDB, block.Header(), block.Uncles())
|
|
}
|
|
|
|
return stateDB, nil
|
|
}
|
|
|
|
// accumulateRewards credits the coinbase of the given block with the mining
|
|
// reward. The total reward consists of the static block reward and rewards for
|
|
// included uncles. The coinbase of each uncle block is also rewarded.
|
|
func accumulateRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) {
|
|
// Select the correct block reward based on chain progression
|
|
blockReward := ethash.FrontierBlockReward
|
|
if config.IsByzantium(header.Number) {
|
|
blockReward = ethash.ByzantiumBlockReward
|
|
}
|
|
|
|
if config.IsConstantinople(header.Number) {
|
|
blockReward = ethash.ConstantinopleBlockReward
|
|
}
|
|
|
|
// Accumulate the rewards for the miner and any included uncles
|
|
reward := new(big.Int).Set(blockReward)
|
|
r := new(big.Int)
|
|
for _, uncle := range uncles {
|
|
r.Add(uncle.Number, big8)
|
|
r.Sub(r, header.Number)
|
|
r.Mul(r, blockReward)
|
|
r.Div(r, big8)
|
|
state.AddBalance(uncle.Coinbase, r)
|
|
|
|
r.Div(blockReward, big32)
|
|
reward.Add(reward, r)
|
|
}
|
|
|
|
state.AddBalance(header.Coinbase, reward)
|
|
}
|
|
|
|
func setChainConfig(ghash common.Hash) *params.ChainConfig {
|
|
switch {
|
|
case ghash == params.MainnetGenesisHash:
|
|
return params.MainnetChainConfig
|
|
case ghash == params.RopstenGenesisHash:
|
|
return params.RopstenChainConfig
|
|
case ghash == params.SepoliaGenesisHash:
|
|
return params.SepoliaChainConfig
|
|
case ghash == params.RinkebyGenesisHash:
|
|
return params.RinkebyChainConfig
|
|
case ghash == params.GoerliGenesisHash:
|
|
return params.GoerliChainConfig
|
|
case ghash == testHash:
|
|
return testChainConfig
|
|
default:
|
|
return params.AllEthashProtocolChanges
|
|
}
|
|
}
|
|
|
|
func getAuthor(b *ipldEth.Backend, header *types.Header) *common.Address {
|
|
author, err := getEngine(b).Author(header)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
return &author
|
|
}
|
|
|
|
func getEngine(b *ipldEth.Backend) consensus.Engine {
|
|
// TODO: add logic for other engines
|
|
if b.Config.ChainConfig.Clique != nil {
|
|
engine := clique.New(b.Config.ChainConfig.Clique, nil)
|
|
return engine
|
|
}
|
|
|
|
return ethash.NewFaker()
|
|
}
|