mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-08-20 10:52:25 +00:00
286 lines
9.3 KiB
Go
286 lines
9.3 KiB
Go
// Copyright 2024 The go-ethereum Authors
|
|
// This file is part of the go-ethereum library.
|
|
//
|
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package light
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/big"
|
|
"time"
|
|
|
|
eth2spec "github.com/attestantio/go-eth2-client/spec"
|
|
"github.com/ethereum/go-ethereum/beacon/beaclient"
|
|
"github.com/ethereum/go-ethereum/beacon/engine"
|
|
"github.com/ethereum/go-ethereum/beacon/params"
|
|
"github.com/ethereum/go-ethereum/beacon/types"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
ctypes "github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/event"
|
|
"github.com/ethereum/go-ethereum/log"
|
|
)
|
|
|
|
// LightClient tracks the head of the chain using the light client protocol,
|
|
// which assumes the majority of beacon chain sync committe is honest.
|
|
type LightClient struct {
|
|
beacon *beaclient.Client
|
|
store *store
|
|
|
|
chainHeadFeed event.Feed
|
|
quitCh chan struct{}
|
|
}
|
|
|
|
// Bootstrap retrieves a light client bootstrap and authenticates it against the
|
|
// provided trusted root.
|
|
func Bootstrap(ctx context.Context, server string, root common.Hash) (*LightClient, error) {
|
|
api, err := beaclient.NewClient(ctx, server)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to beacon server: %w", err)
|
|
}
|
|
bs, err := api.Bootstrap(root)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get bootstrap data: %w", err)
|
|
}
|
|
if err := bs.Valid(); err != nil {
|
|
return nil, fmt.Errorf("failed to validate bootstrap data: %w", err)
|
|
}
|
|
current, err := bs.Committee.Deserialize()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to deserialize committee")
|
|
}
|
|
return &LightClient{
|
|
beacon: api,
|
|
store: &store{
|
|
config: params.SepoliaChainConfig,
|
|
current: current,
|
|
optimistic: &bs.Header.Header,
|
|
finalized: &bs.Header.Header,
|
|
},
|
|
quitCh: make(chan struct{}),
|
|
}, nil
|
|
}
|
|
|
|
// ChainHeadEvent returns an authenticated execution payload associated with the
|
|
// latest accepted head of the beacon chain.
|
|
type ChainHeadEvent struct {
|
|
Data *engine.ExecutableData
|
|
}
|
|
|
|
// SubscribeChainHeadEvent allows callers to subscribe a provided channel to new
|
|
// head updates.
|
|
func (c *LightClient) SubscribeChainHeadEvent(ch chan<- ChainHeadEvent) event.Subscription {
|
|
return c.chainHeadFeed.Subscribe(ch)
|
|
}
|
|
|
|
// Finalized returns the latest finalized head known to the light client.
|
|
func (c *LightClient) Finalized() *types.Header {
|
|
return c.store.finalized
|
|
}
|
|
|
|
// Start executes the main active loop of the light client which drives the
|
|
// underlying light client store.
|
|
func (c *LightClient) Start() error {
|
|
var (
|
|
ticker = time.NewTicker(params.SlotLength * time.Second)
|
|
lastFinality = time.Now()
|
|
)
|
|
for {
|
|
select {
|
|
case <-c.quitCh:
|
|
return nil
|
|
case <-ticker.C:
|
|
if c.store.next == nil {
|
|
log.Debug("Fetching committee update", "period", c.store.finalizedPeriod())
|
|
updates, err := c.beacon.GetRangeUpdate(c.store.finalizedPeriod(), 1)
|
|
if err != nil {
|
|
log.Error("Failed to fetch next committee", "err", err)
|
|
} else {
|
|
for _, update := range updates {
|
|
if err := c.store.Insert(update); err != nil {
|
|
log.Error("Failed to insert committee update", "err", err)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var (
|
|
update *types.LightClientUpdate
|
|
err error
|
|
)
|
|
if time.Since(lastFinality) > time.Minute*5 {
|
|
lastFinality = time.Now()
|
|
update, err = c.beacon.GetFinalityUpdate()
|
|
} else {
|
|
update, err = c.beacon.GetOptimisticUpdate()
|
|
}
|
|
if err != nil {
|
|
log.Error("Failed to retrieve update", "err", err)
|
|
continue
|
|
}
|
|
log.Trace("New beacon update", "slot", update.AttestedHeader.Slot, "root", update.AttestedHeader.Hash(), "sigslot", update.SignatureSlot, "period", update.AttestedHeader.SyncPeriod(), "hasFinalized", update.FinalizedHeader != nil, "hasNext", update.NextSyncCommittee != nil)
|
|
if err := c.store.Insert(update); err != nil {
|
|
log.Error("Failed to insert update", "err", err)
|
|
continue
|
|
}
|
|
head := update.AttestedHeader
|
|
log.Info("Beacon head updated", "slot", head.Slot, "root", head.Hash(), "finalized", c.Finalized().Hash(), "signers", update.SyncAggregate.SignerCount())
|
|
|
|
// Fetch full execution payload from beacon provider and send to head feed.
|
|
data, err := c.fetchExecutableData(head.Hash())
|
|
if err != nil {
|
|
log.Error("Failed to insert update", "err", err)
|
|
continue
|
|
}
|
|
c.chainHeadFeed.Send(ChainHeadEvent{Data: data})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *LightClient) Stop() error {
|
|
close(c.quitCh)
|
|
return nil
|
|
}
|
|
|
|
// fetchExecutableData retrieves the full beacon block associated with the beacon
|
|
// block root and returns the inner execution payload.
|
|
func (c *LightClient) fetchExecutableData(head common.Hash) (*engine.ExecutableData, error) {
|
|
block, err := c.beacon.GetBlock(head)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get execution payload: %w", err)
|
|
}
|
|
// Compute the root of the block and verify it matches the root the sync
|
|
// committee signed.
|
|
root, err := block.Root()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to compute root for beacon block: %w", err)
|
|
}
|
|
if common.Hash(root) != head {
|
|
return nil, fmt.Errorf("unable to verify block body against sync committee update")
|
|
}
|
|
return versionedBlockToExecutableData(block), nil
|
|
}
|
|
|
|
// versionedBlockToExecutableData parses versioned blocks and returns a generic
|
|
// execution payload object.
|
|
func versionedBlockToExecutableData(block *eth2spec.VersionedSignedBeaconBlock) *engine.ExecutableData {
|
|
var ep *engine.ExecutableData
|
|
switch block.Version {
|
|
case eth2spec.DataVersionPhase0:
|
|
panic("phase0 block has no execution payload to send")
|
|
case eth2spec.DataVersionAltair:
|
|
panic("altair block has no execution payload to send")
|
|
case eth2spec.DataVersionBellatrix:
|
|
p := block.Bellatrix.Message.Body.ExecutionPayload
|
|
ep = &engine.ExecutableData{
|
|
ParentHash: common.Hash(p.ParentHash),
|
|
FeeRecipient: common.Address(p.FeeRecipient),
|
|
StateRoot: p.StateRoot,
|
|
ReceiptsRoot: p.ReceiptsRoot,
|
|
LogsBloom: p.LogsBloom[:],
|
|
Random: p.PrevRandao,
|
|
Number: p.BlockNumber,
|
|
GasLimit: p.GasLimit,
|
|
GasUsed: p.GasUsed,
|
|
Timestamp: p.Timestamp,
|
|
ExtraData: p.ExtraData,
|
|
BaseFeePerGas: new(big.Int).SetBytes(reverse(p.BaseFeePerGas[:])),
|
|
BlockHash: common.Hash(p.BlockHash),
|
|
Transactions: [][]byte{},
|
|
Withdrawals: nil,
|
|
BlobGasUsed: nil,
|
|
ExcessBlobGas: nil,
|
|
}
|
|
for _, tx := range p.Transactions {
|
|
ep.Transactions = append(ep.Transactions, tx)
|
|
}
|
|
case eth2spec.DataVersionCapella:
|
|
p := block.Capella.Message.Body.ExecutionPayload
|
|
ep = &engine.ExecutableData{
|
|
ParentHash: common.Hash(p.ParentHash),
|
|
FeeRecipient: common.Address(p.FeeRecipient),
|
|
StateRoot: p.StateRoot,
|
|
ReceiptsRoot: p.ReceiptsRoot,
|
|
LogsBloom: p.LogsBloom[:],
|
|
Random: p.PrevRandao,
|
|
Number: p.BlockNumber,
|
|
GasLimit: p.GasLimit,
|
|
GasUsed: p.GasUsed,
|
|
Timestamp: p.Timestamp,
|
|
ExtraData: p.ExtraData,
|
|
BaseFeePerGas: new(big.Int).SetBytes(reverse(p.BaseFeePerGas[:])),
|
|
BlockHash: common.Hash(p.BlockHash),
|
|
Transactions: [][]byte{},
|
|
Withdrawals: nil,
|
|
BlobGasUsed: nil,
|
|
ExcessBlobGas: nil,
|
|
}
|
|
for _, tx := range p.Transactions {
|
|
ep.Transactions = append(ep.Transactions, tx)
|
|
}
|
|
for _, wx := range p.Withdrawals {
|
|
ep.Withdrawals = append(ep.Withdrawals, &ctypes.Withdrawal{
|
|
Index: uint64(wx.Index),
|
|
Validator: uint64(wx.ValidatorIndex),
|
|
Address: common.Address(wx.Address),
|
|
Amount: uint64(wx.Amount),
|
|
})
|
|
}
|
|
case eth2spec.DataVersionDeneb:
|
|
p := block.Deneb.Message.Body.ExecutionPayload
|
|
ep = &engine.ExecutableData{
|
|
ParentHash: common.Hash(p.ParentHash),
|
|
FeeRecipient: common.Address(p.FeeRecipient),
|
|
StateRoot: common.Hash(p.StateRoot),
|
|
ReceiptsRoot: common.Hash(p.ReceiptsRoot),
|
|
LogsBloom: p.LogsBloom[:],
|
|
Random: p.PrevRandao,
|
|
Number: p.BlockNumber,
|
|
GasLimit: p.GasLimit,
|
|
GasUsed: p.GasUsed,
|
|
Timestamp: p.Timestamp,
|
|
ExtraData: p.ExtraData,
|
|
BaseFeePerGas: nil, // TODO: convert this []uint64 correctly to big.Int
|
|
BlockHash: common.Hash(p.BlockHash),
|
|
Transactions: [][]byte{},
|
|
Withdrawals: nil,
|
|
BlobGasUsed: &p.BlobGasUsed,
|
|
ExcessBlobGas: &p.ExcessBlobGas,
|
|
}
|
|
for _, tx := range p.Transactions {
|
|
ep.Transactions = append(ep.Transactions, tx)
|
|
}
|
|
for _, wx := range p.Withdrawals {
|
|
ep.Withdrawals = append(ep.Withdrawals, &ctypes.Withdrawal{
|
|
Index: uint64(wx.Index),
|
|
Validator: uint64(wx.ValidatorIndex),
|
|
Address: common.Address(wx.Address),
|
|
Amount: uint64(wx.Amount),
|
|
})
|
|
}
|
|
default:
|
|
panic("unknown beacon block version")
|
|
}
|
|
return ep
|
|
}
|
|
|
|
func reverse(b []byte) []byte {
|
|
for i := 0; i < len(b)/2; i++ {
|
|
j := len(b) - i - 1
|
|
b[i], b[j] = b[j], b[i]
|
|
}
|
|
return b
|
|
}
|