Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limit finalized header gap #1156

Merged
merged 35 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 42 additions & 11 deletions relayer/relays/beacon/header/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,20 @@ var ErrSyncCommitteeLatency = errors.New("sync committee latency found")
var ErrExecutionHeaderNotImported = errors.New("execution header not imported")

type Header struct {
cache *cache.BeaconCache
writer *parachain.ParachainWriter
syncer *syncer.Syncer
cache *cache.BeaconCache
writer *parachain.ParachainWriter
syncer *syncer.Syncer
slotsInEpoch uint64
epochsPerSyncCommitteePeriod uint64
}

func New(writer *parachain.ParachainWriter, beaconEndpoint string, setting config.SpecSettings) Header {
return Header{
cache: cache.New(setting.SlotsInEpoch, setting.EpochsPerSyncCommitteePeriod),
writer: writer,
syncer: syncer.New(beaconEndpoint, setting),
cache: cache.New(setting.SlotsInEpoch, setting.EpochsPerSyncCommitteePeriod),
writer: writer,
syncer: syncer.New(beaconEndpoint, setting),
slotsInEpoch: setting.SlotsInEpoch,
epochsPerSyncCommitteePeriod: setting.EpochsPerSyncCommitteePeriod,
}
}

Expand Down Expand Up @@ -116,6 +120,21 @@ func (h *Header) SyncCommitteePeriodUpdate(ctx context.Context, period uint64) e
}
}

// If the gap between the last two finalized headers is more than the sync committee period, sync an interim
// finalized header
validGapSlot := h.cache.Finalized.LastSyncedSlot + (h.slotsInEpoch * h.epochsPerSyncCommitteePeriod)
if validGapSlot < uint64(update.Payload.FinalizedHeader.Slot) {
err, finalizedUpdate := h.syncer.GetFinalizedUpdateAtAttestedSlot(validGapSlot)
if err != nil {
return fmt.Errorf("fetch finalized update at slot: %w", err)
}
yrong marked this conversation as resolved.
Show resolved Hide resolved

err = h.updateFinalizedHeaderOnchain(ctx, finalizedUpdate)
if err != nil {
return fmt.Errorf("update interim finalized header on-chain: %w", err)
}
}

log.WithFields(log.Fields{
"finalized_header_slot": update.Payload.FinalizedHeader.Slot,
"period": period,
Expand Down Expand Up @@ -163,7 +182,13 @@ func (h *Header) SyncFinalizedHeader(ctx context.Context) error {
}
}

err = h.writer.WriteToParachainAndWatch(ctx, "EthereumBeaconClient.submit", update.Payload)
return h.updateFinalizedHeaderOnchain(ctx, update)
}

// Write the provided finalized header update (possibly containing a sync committee) on-chain and check if it was
// imported successfully. Update the cache if it has and add the finalized header to the checkpoint cache.
func (h *Header) updateFinalizedHeaderOnchain(ctx context.Context, update scale.Update) error {
err := h.writer.WriteToParachainAndWatch(ctx, "EthereumBeaconClient.submit", update.Payload)
if err != nil {
return fmt.Errorf("write to parachain: %w", err)
}
Expand Down Expand Up @@ -279,10 +304,11 @@ func (h *Header) SyncExecutionHeaders(ctx context.Context) error {
}

func (h *Header) syncLaggingSyncCommitteePeriods(ctx context.Context, latestSyncedPeriod, currentSyncPeriod uint64) error {
// sync for the next period
// Sync the next period's committee.
periodsToSync := []uint64{latestSyncedPeriod + 1}

// For initialPeriod special handling here to sync it again for nextSyncCommittee which is not included in InitCheckpoint
// Special handling here for the initial checkpoint to sync the next sync committee which is not included in initial
// checkpoint.
if h.isInitialSyncPeriod() {
periodsToSync = append([]uint64{latestSyncedPeriod}, periodsToSync...)
}
Expand Down Expand Up @@ -327,7 +353,7 @@ func (h *Header) populateFinalizedCheckpoint(slot uint64) error {
// Always check slot finalized on chain before populating checkpoint
onChainFinalizedHeader, err := h.writer.GetFinalizedHeaderStateByBlockRoot(blockRoot)
if err != nil {
return err
return fmt.Errorf("get finalized header state by block root: %w", err)
}
if onChainFinalizedHeader.BeaconSlot != slot {
return fmt.Errorf("on chain finalized header inconsistent at slot %d", slot)
Expand Down Expand Up @@ -357,7 +383,12 @@ func (h *Header) populateClosestCheckpoint(slot uint64) (cache.Proof, error) {
}
err := h.populateFinalizedCheckpoint(checkpointSlot)
if err != nil {
return cache.Proof{}, fmt.Errorf("populate closest checkpoint: %w", err)
// 4513792 finalized slot
attestedHeaderSlot := checkpointSlot + (32 * 2.5)
yrong marked this conversation as resolved.
Show resolved Hide resolved
finalizedUpdate := h.syncer.GetFinalizedUpdateAtAttestedSlot(attestedHeaderSlot)
if err != nil {
return cache.Proof{}, fmt.Errorf("get finalized update at slotr: %w", err)
}
}

log.Info("populated finalized checkpoint")
Expand Down
4 changes: 2 additions & 2 deletions relayer/relays/beacon/header/syncer/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ func (b *BeaconClient) GetLatestFinalizedUpdate() (LatestFinalisedUpdateResponse
return response, nil
}

func (b *BeaconClient) DownloadBeaconState(stateIdOrSlot string) ([]byte, error) {
func (b *BeaconClient) GetBeaconState(stateIdOrSlot string) ([]byte, error) {
var data []byte
req, err := http.NewRequest("GET", fmt.Sprintf("%s/eth/v2/debug/beacon/states/%s", b.endpoint, stateIdOrSlot), nil)
if err != nil {
Expand All @@ -422,7 +422,7 @@ func (b *BeaconClient) DownloadBeaconState(stateIdOrSlot string) ([]byte, error)
return data, nil
}

func (b *BeaconClient) GetBeaconBlockResponse(blockID common.Hash) (BeaconBlockResponse, error) {
func (b *BeaconClient) GetBeaconBlock(blockID common.Hash) (BeaconBlockResponse, error) {
var beaconBlockResponse BeaconBlockResponse

req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/eth/v2/beacon/blocks/%s", b.endpoint, blockID), nil)
Expand Down
124 changes: 120 additions & 4 deletions relayer/relays/beacon/header/syncer/syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import (
)

const (
BlockRootGeneralizedIndex = 37
ExecutionPayloadGeneralizedIndex = 25
BlockRootGeneralizedIndex = 37
FinalizedCheckpointGeneralizedIndex = 105
ExecutionPayloadGeneralizedIndex = 25
)

var (
Expand Down Expand Up @@ -159,7 +160,7 @@ func (s *Syncer) GetBlockRoots(slot uint64) (scale.BlockRootProof, error) {
var beaconState state.BeaconState
var blockRootsContainer state.BlockRootsContainer

data, err := s.Client.DownloadBeaconState(strconv.FormatUint(slot, 10))
data, err := s.Client.GetBeaconState(strconv.FormatUint(slot, 10))
if err != nil {
return blockRootProof, fmt.Errorf("download beacon state failed: %w", err)
}
Expand Down Expand Up @@ -366,7 +367,7 @@ func (s *Syncer) GetNextHeaderUpdateBySlotWithCheckpoint(slot uint64, checkpoint

func (s *Syncer) GetHeaderUpdate(blockRoot common.Hash, checkpoint *cache.Proof) (scale.HeaderUpdatePayload, error) {
var update scale.HeaderUpdatePayload
blockResponse, err := s.Client.GetBeaconBlockResponse(blockRoot)
blockResponse, err := s.Client.GetBeaconBlock(blockRoot)
if err != nil {
return update, fmt.Errorf("fetch block: %w", err)
}
Expand Down Expand Up @@ -447,6 +448,121 @@ func (s *Syncer) GetHeaderUpdate(blockRoot common.Hash, checkpoint *cache.Proof)
}, nil
}

func (s *Syncer) getBeaconStateAtSlot(slot uint64) (state.BeaconState, error) {
var beaconState state.BeaconState
beaconData, err := s.Client.GetBeaconState(strconv.FormatUint(slot, 10))
if err != nil {
return beaconState, fmt.Errorf("fetch beacon state: %w", err)
}

isDeneb := s.DenebForked(slot)

if isDeneb {
beaconState = &state.BeaconStateDenebMainnet{}
} else {
beaconState = &state.BeaconStateCapellaMainnet{}
}

err = beaconState.UnmarshalSSZ(beaconData)
if err != nil {
return beaconState, fmt.Errorf("unmarshal beacon state: %w", err)
}

return beaconState, nil
}

func (s *Syncer) GetFinalizedUpdateAtAttestedSlot(attestedSlot uint64) (scale.Update, error) {
var update scale.Update

// Get the beacon data first since it is mostly likely to fail
beaconState, err := s.getBeaconStateAtSlot(attestedSlot)
if err != nil {
return update, fmt.Errorf("fetch beacon state: %w", err)
}

finalizedCheckpoint := beaconState.GetFinalizedCheckpoint()

// Get the finalized header at the given slot state
finalizedHeader, err := s.Client.GetHeader(common.BytesToHash(finalizedCheckpoint.Root))
if err != nil {
return update, fmt.Errorf("fetch block: %w", err)
}

// Finalized header proof
stateTree, err := beaconState.GetTree()
if err != nil {
return update, fmt.Errorf("get state tree: %w", err)
}
_ = stateTree.Hash() // necessary to populate the proof tree values
finalizedHeaderProof, err := stateTree.Prove(FinalizedCheckpointGeneralizedIndex)
yrong marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return update, fmt.Errorf("get finalized header proof: %w", err)
}

blockRootsProof, err := s.GetBlockRoots(finalizedHeader.Slot)
if err != nil {
return scale.Update{}, fmt.Errorf("fetch block roots: %w", err)
}

// Get the header at the slot
header, err := s.Client.GetHeaderBySlot(attestedSlot)
if err != nil {
return update, fmt.Errorf("fetch block: %w", err)
}

// Get the next block for the sync aggregate
nextHeader, err := s.FindBeaconHeaderWithBlockIncluded(attestedSlot + 1)
if err != nil {
return update, fmt.Errorf("fetch block: %w", err)
}

nextBlock, err := s.Client.GetBeaconBlockBySlot(nextHeader.Slot)
if err != nil {
return update, fmt.Errorf("fetch block: %w", err)
}

nextBlockSlot, err := util.ToUint64(nextBlock.Data.Message.Slot)
if err != nil {
return update, fmt.Errorf("parse next block slot: %w", err)
}

scaleHeader, err := header.ToScale()
if err != nil {
return update, fmt.Errorf("convert header to scale: %w", err)
}

scaleFinalizedHeader, err := finalizedHeader.ToScale()
if err != nil {
return update, fmt.Errorf("convert finalized header to scale: %w", err)
}

syncAggregate := nextBlock.Data.Message.Body.SyncAggregate

scaleSyncAggregate, err := syncAggregate.ToScale()
if err != nil {
return update, fmt.Errorf("convert sync aggregate to scale: %w", err)
}

payload := scale.UpdatePayload{
AttestedHeader: scaleHeader,
SyncAggregate: scaleSyncAggregate,
SignatureSlot: types.U64(nextBlockSlot),
NextSyncCommitteeUpdate: scale.OptionNextSyncCommitteeUpdatePayload{
HasValue: false,
},
FinalizedHeader: scaleFinalizedHeader,
FinalityBranch: util.BytesBranchToScale(finalizedHeaderProof.Hashes),
BlockRootsRoot: blockRootsProof.Leaf,
BlockRootsBranch: blockRootsProof.Proof,
}

return scale.Update{
Payload: payload,
FinalizedHeaderBlockRoot: common.BytesToHash(finalizedCheckpoint.Root),
BlockRootsTree: blockRootsProof.Tree,
}, nil
}

func (s *Syncer) getBlockHeaderAncestryProof(slot int, blockRoot common.Hash, blockRootTree *ssz.Node) ([]types.H256, error) {
maxSlotsPerHistoricalRoot := int(s.setting.SlotsInEpoch * s.setting.EpochsPerSyncCommitteePeriod)
indexInArray := slot % maxSlotsPerHistoricalRoot
Expand Down
53 changes: 53 additions & 0 deletions relayer/relays/beacon/header/syncer/syncer_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package syncer

import (
"encoding/json"
"fmt"
"testing"

"github.com/snowfork/snowbridge/relayer/relays/beacon/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const TestUrl = "https://lodestar-sepolia.chainsafe.io"

func TestCalculateNextCheckpointSlot(t *testing.T) {
values := []struct {
name string
Expand Down Expand Up @@ -38,3 +44,50 @@ func TestCalculateNextCheckpointSlot(t *testing.T) {
assert.Equal(t, tt.expected, result, "expected %t but found %t for slot %d", tt.expected, result, tt.slot)
}
}

func newTestRunner() *Syncer {
return New(TestUrl, config.SpecSettings{
SlotsInEpoch: 32,
EpochsPerSyncCommitteePeriod: 256,
DenebForkEpoch: 0,
})
}

// Verifies that the Lodestar provided finalized endpoint matches the manually constructed finalized endpoint
func TestGetFinalizedUpdateAtSlot(t *testing.T) {
t.Skip("skip testing utility test")

syncer := newTestRunner()

// Get lodestar finalized update
lodestarUpdate, err := syncer.GetFinalizedUpdate()
require.NoError(t, err)
lodestarUpdateJSON := lodestarUpdate.Payload.ToJSON()

// Manually construct the finalized update for the same block
manualUpdate, err := syncer.GetFinalizedUpdateAtAttestedSlot(uint64(lodestarUpdate.Payload.AttestedHeader.Slot))
require.NoError(t, err)
manualUpdateJSON := manualUpdate.Payload.ToJSON()

lodestarPayload, err := json.Marshal(lodestarUpdateJSON)
require.NoError(t, err)
manualPayload, err := json.Marshal(manualUpdateJSON)
require.NoError(t, err)

// The JSON should be same
require.JSONEq(t, string(lodestarPayload), string(manualPayload))
}

func TestGetInitialCheckpoint(t *testing.T) {
t.Skip("skip testing utility test")

syncer := newTestRunner()

response, err := syncer.GetCheckpoint()
assert.NoError(t, err)
jsonUpdate := response.ToJSON()

j, err := json.MarshalIndent(jsonUpdate, "", " ")
assert.NoError(t, err)
fmt.Println(string(j))
}
17 changes: 11 additions & 6 deletions relayer/relays/beacon/state/beacon.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ type BeaconState interface {
GetLatestBlockHeader() *BeaconBlockHeader
GetBlockRoots() [][]byte
GetTree() (*ssz.Node, error)
GetFinalizedCheckpoint() *Checkpoint
}

type SyncAggregate interface {
Expand Down Expand Up @@ -288,6 +289,14 @@ func (b *BeaconBlockCapellaMainnet) GetBlockBodyTree() (*ssz.Node, error) {
return b.Body.GetTree()
}

func (b *BeaconBlockCapellaMainnet) ExecutionPayloadCapella() *ExecutionPayloadCapella {
return b.Body.ExecutionPayload
}

func (b *BeaconBlockCapellaMainnet) ExecutionPayloadDeneb() *ExecutionPayloadDeneb {
return nil
}

func (b *BeaconStateCapellaMainnet) GetSlot() uint64 {
return b.Slot
}
Expand All @@ -304,10 +313,6 @@ func (b *BeaconStateCapellaMainnet) SetBlockRoots(blockRoots [][]byte) {
b.BlockRoots = blockRoots
}

func (b *BeaconBlockCapellaMainnet) ExecutionPayloadCapella() *ExecutionPayloadCapella {
return b.Body.ExecutionPayload
}

func (b *BeaconBlockCapellaMainnet) ExecutionPayloadDeneb() *ExecutionPayloadDeneb {
return nil
func (b *BeaconStateCapellaMainnet) GetFinalizedCheckpoint() *Checkpoint {
return b.FinalizedCheckpoint
}
Loading