Skip to content

Commit

Permalink
Fix find on-chain checkpoint (#1294)
Browse files Browse the repository at this point in the history
* fix wrap around

* logs

* doesnt have to be in the same period

* testing something

* fix

* adds test and config

* writer

* fix compilation

* remove temp building relayer

* comment
  • Loading branch information
claravanstaden authored Oct 1, 2024
1 parent c7421bd commit 02a325b
Show file tree
Hide file tree
Showing 18 changed files with 178 additions and 48 deletions.
1 change: 0 additions & 1 deletion .github/workflows/release-relayer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches:
- main
- release-v1.0.0
workflow_dispatch:

env:
Expand Down
8 changes: 4 additions & 4 deletions relayer/cmd/generate_beacon_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func generateBeaconCheckpoint(cmd *cobra.Command, _ []string) error {
return err
}

p := protocol.New(conf.Source.Beacon.Spec)
p := protocol.New(conf.Source.Beacon.Spec, conf.Sink.Parachain.HeaderRedundancy)
store := store.New(conf.Source.Beacon.DataStore.Location, conf.Source.Beacon.DataStore.MaxEntries, *p)
store.Connect()
defer store.Close()
Expand Down Expand Up @@ -193,7 +193,7 @@ func generateBeaconTestFixture(cmd *cobra.Command, _ []string) error {
return err
}

p := protocol.New(conf.Source.Beacon.Spec)
p := protocol.New(conf.Source.Beacon.Spec, conf.Sink.Parachain.HeaderRedundancy)

store := store.New(conf.Source.Beacon.DataStore.Location, conf.Source.Beacon.DataStore.MaxEntries, *p)
err = store.Connect()
Expand Down Expand Up @@ -504,7 +504,7 @@ func generateExecutionUpdate(cmd *cobra.Command, _ []string) error {
}
log.WithFields(log.Fields{"endpoint": conf.Source.Beacon.Endpoint}).Info("connecting to beacon API")

p := protocol.New(conf.Source.Beacon.Spec)
p := protocol.New(conf.Source.Beacon.Spec, conf.Sink.Parachain.HeaderRedundancy)

store := store.New(conf.Source.Beacon.DataStore.Location, conf.Source.Beacon.DataStore.MaxEntries, *p)
store.Connect()
Expand Down Expand Up @@ -695,7 +695,7 @@ func generateInboundFixture(cmd *cobra.Command, _ []string) error {
return err
}

p := protocol.New(beaconConf.Source.Beacon.Spec)
p := protocol.New(beaconConf.Source.Beacon.Spec, beaconConf.Sink.Parachain.HeaderRedundancy)

store := store.New(beaconConf.Source.Beacon.DataStore.Location, beaconConf.Source.Beacon.DataStore.MaxEntries, *p)
store.Connect()
Expand Down
2 changes: 1 addition & 1 deletion relayer/cmd/import_beacon_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func importBeaconState(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("open finalized state file: %w", err)
}

p := protocol.New(conf.Source.Beacon.Spec)
p := protocol.New(conf.Source.Beacon.Spec, conf.Sink.Parachain.HeaderRedundancy)
store := store.New(conf.Source.Beacon.DataStore.Location, conf.Source.Beacon.DataStore.MaxEntries, *p)
beaconClient := api.NewBeaconClient(conf.Source.Beacon.Endpoint, conf.Source.Beacon.StateEndpoint)
syncer := syncer.New(beaconClient, &store, p)
Expand Down
2 changes: 1 addition & 1 deletion relayer/cmd/import_execution_header.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func importExecutionHeaderFn(cmd *cobra.Command, _ []string) error {

log.WithField("hash", beaconHeader).Info("will be syncing execution header for beacon hash")

p := protocol.New(conf.Source.Beacon.Spec)
p := protocol.New(conf.Source.Beacon.Spec, conf.Sink.Parachain.HeaderRedundancy)
store := store.New(conf.Source.Beacon.DataStore.Location, conf.Source.Beacon.DataStore.MaxEntries, *p)
store.Connect()
defer store.Close()
Expand Down
2 changes: 1 addition & 1 deletion relayer/cmd/list_beacon_states.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func listBeaconState(cmd *cobra.Command, _ []string) error {
return err
}

p := protocol.New(conf.Source.Beacon.Spec)
p := protocol.New(conf.Source.Beacon.Spec, conf.Sink.Parachain.HeaderRedundancy)
store := store.New(conf.Source.Beacon.DataStore.Location, conf.Source.Beacon.DataStore.MaxEntries, *p)

err = store.Connect()
Expand Down
2 changes: 1 addition & 1 deletion relayer/cmd/store_beacon_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func storeBeaconState(cmd *cobra.Command, _ []string) error {
return err
}

p := protocol.New(conf.Source.Beacon.Spec)
p := protocol.New(conf.Source.Beacon.Spec, conf.Sink.Parachain.HeaderRedundancy)
store := store.New(conf.Source.Beacon.DataStore.Location, conf.Source.Beacon.DataStore.MaxEntries, *p)
beaconClient := api.NewBeaconClient(conf.Source.Beacon.Endpoint, conf.Source.Beacon.StateEndpoint)
syncer := syncer.New(beaconClient, &store, p)
Expand Down
26 changes: 23 additions & 3 deletions relayer/relays/beacon/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package config
import (
"errors"
"fmt"
"github.com/snowfork/snowbridge/relayer/config"
)

type Config struct {
Expand Down Expand Up @@ -35,8 +34,16 @@ type BeaconConfig struct {
}

type SinkConfig struct {
Parachain config.ParachainConfig `mapstructure:"parachain"`
UpdateSlotInterval uint64 `mapstructure:"updateSlotInterval"`
Parachain ParachainConfig `mapstructure:"parachain"`
UpdateSlotInterval uint64 `mapstructure:"updateSlotInterval"`
}

type ParachainConfig struct {
Endpoint string `mapstructure:"endpoint"`
MaxWatchedExtrinsics int64 `mapstructure:"maxWatchedExtrinsics"`
// The max number of header in the FinalizedBeaconStateBuffer on-chain.
// https://github.com/paritytech/polkadot-sdk/blob/master/bridges/snowbridge/pallets/ethereum-client/src/types.rs#L23
HeaderRedundancy uint64 `mapstructure:"headerRedundancy"`
}

func (c Config) Validate() error {
Expand Down Expand Up @@ -81,3 +88,16 @@ func (b BeaconConfig) Validate() error {
}
return nil
}

func (p ParachainConfig) Validate() error {
if p.Endpoint == "" {
return errors.New("[endpoint] is not set")
}
if p.MaxWatchedExtrinsics == 0 {
return errors.New("[maxWatchedExtrinsics] is not set")
}
if p.HeaderRedundancy == 0 {
return errors.New("[HeaderRedundancy] is not set")
}
return nil
}
22 changes: 11 additions & 11 deletions relayer/relays/beacon/header/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,32 +523,32 @@ func (h *Header) findLatestCheckPoint(slot uint64) (state.FinalizedHeader, error
return beaconState, fmt.Errorf("GetLastFinalizedStateIndex error: %w", err)
}
startIndex := uint64(lastIndex)
endIndex := uint64(0)
endIndex := startIndex + 1

syncCommitteePeriod := h.protocol.Settings.SlotsInEpoch * h.protocol.Settings.EpochsPerSyncCommitteePeriod
slotPeriodIndex := slot / syncCommitteePeriod

for index := startIndex; index >= endIndex; index-- {
totalStates := syncCommitteePeriod * h.protocol.HeaderRedundancy // Total size of the circular buffer,
// https://github.com/paritytech/polkadot-sdk/blob/master/bridges/snowbridge/pallets/ethereum-client/src/lib.rs#L75
for index := startIndex; index != endIndex; index = (index - 1 + totalStates) % totalStates {
beaconRoot, err := h.writer.GetFinalizedBeaconRootByIndex(uint32(index))
if err != nil {
return beaconState, fmt.Errorf("GetFinalizedBeaconRootByIndex %d, error: %w", index, err)
}
beaconState, err = h.writer.GetFinalizedHeaderStateByBlockRoot(beaconRoot)
if err != nil {
return beaconState, fmt.Errorf("GetFinalizedHeaderStateByBlockRoot %s, error: %w", beaconRoot.Hex(), err)
// As soon as it can't find a block root, it means the circular wrap around array is empty.
log.WithFields(log.Fields{"index": index, "blockRoot": beaconRoot.Hex()}).WithError(err).Info("searching for checkpoint on-chain failed")
break
}
statePeriodIndex := beaconState.BeaconSlot / syncCommitteePeriod

if beaconState.BeaconSlot < slot {
log.WithFields(log.Fields{"index": index, "blockRoot": beaconRoot.Hex()}).WithError(err).Debug("unable to find a relevant on-chain header")
break
}
// Found the beaconState
if beaconState.BeaconSlot > slot && beaconState.BeaconSlot < slot+syncCommitteePeriod && slotPeriodIndex == statePeriodIndex {
break
if beaconState.BeaconSlot > slot && beaconState.BeaconSlot < slot+syncCommitteePeriod {
return beaconState, nil
}
}
if beaconState.BeaconSlot > slot && beaconState.BeaconSlot < slot+syncCommitteePeriod {
return beaconState, nil
}

return beaconState, fmt.Errorf("no checkpoint on chain for slot %d", slot)
}
Expand Down
112 changes: 107 additions & 5 deletions relayer/relays/beacon/header/header_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package header
import (
"context"
"github.com/ethereum/go-ethereum/common"
"github.com/snowfork/go-substrate-rpc-client/v4/types"
"github.com/snowfork/snowbridge/relayer/relays/beacon/config"
"github.com/snowfork/snowbridge/relayer/relays/beacon/header/syncer/api"
"github.com/snowfork/snowbridge/relayer/relays/beacon/mock"
Expand All @@ -15,14 +16,16 @@ import (
"testing"
)

const MaxRedundancy = 20

// Verifies that the closest checkpoint is populated successfully if it is not populated in the first place.
func TestSyncInterimFinalizedUpdate_WithDataFromAPI(t *testing.T) {
settings := config.SpecSettings{
SlotsInEpoch: 32,
EpochsPerSyncCommitteePeriod: 256,
DenebForkEpoch: 0,
}
p := protocol.New(settings)
p := protocol.New(settings, MaxRedundancy)
client := mock.API{}
beaconStore := mock.Store{}

Expand Down Expand Up @@ -80,7 +83,7 @@ func TestSyncInterimFinalizedUpdate_WithDataFromStore(t *testing.T) {
EpochsPerSyncCommitteePeriod: 256,
DenebForkEpoch: 0,
}
p := protocol.New(settings)
p := protocol.New(settings, MaxRedundancy)
client := mock.API{}
beaconStore := mock.Store{}

Expand Down Expand Up @@ -146,7 +149,7 @@ func TestSyncInterimFinalizedUpdate_WithDataFromStoreWithDifferentBlocks(t *test
EpochsPerSyncCommitteePeriod: 256,
DenebForkEpoch: 0,
}
p := protocol.New(settings)
p := protocol.New(settings, MaxRedundancy)
client := mock.API{}
beaconStore := mock.Store{}

Expand Down Expand Up @@ -212,7 +215,7 @@ func TestSyncInterimFinalizedUpdate_BeaconStateNotAvailableInAPIAndStore(t *test
EpochsPerSyncCommitteePeriod: 256,
DenebForkEpoch: 0,
}
p := protocol.New(settings)
p := protocol.New(settings, MaxRedundancy)
client := mock.API{}
beaconStore := mock.Store{}

Expand Down Expand Up @@ -256,7 +259,7 @@ func TestSyncInterimFinalizedUpdate_NoValidBlocksFound(t *testing.T) {
EpochsPerSyncCommitteePeriod: 256,
DenebForkEpoch: 0,
}
p := protocol.New(settings)
p := protocol.New(settings, MaxRedundancy)
client := mock.API{}
beaconStore := mock.Store{}

Expand Down Expand Up @@ -324,3 +327,102 @@ func TestShouldUpdate(t *testing.T) {
assert.Equal(t, tt.result, result, "expected %t but found %t", tt.result, result)
}
}

func TestFindLatestCheckPoint(t *testing.T) {
settings := config.SpecSettings{
SlotsInEpoch: 4,
EpochsPerSyncCommitteePeriod: 2,
DenebForkEpoch: 0,
}
maxRedundancy := uint64(2)
p := protocol.New(settings, maxRedundancy)
// Total circular array would be 4 * 2 * 2 = 16
client := mock.API{}
beaconStore := mock.Store{}

headerIndex5 := common.HexToHash("0xd118e1464716db841f14ac1c3245f2b7900ee6f896ac85362deae3ff90c14c78")
headerIndex4 := common.HexToHash("0xe9d993e257b0d7ac775b8a03827209db2c7314a780c24a7fad64fd9fcee529f7")
headerIndex3 := common.HexToHash("0x7f2c1240dd714f3d74050638c642f14bf49f541d42f0808b7ae0c188c7edbb08")
headerIndex2 := common.HexToHash("0x01eaa6cbb00311f19c84965f3a9e8ddf56dd5443dfa8ea35c3e6d0b6306554b3")
headerIndex1 := common.HexToHash("0xa106b85508139ad0417cc521f41943a74908bfedbc6f548b3d1acddf60548493")
headerIndex0 := common.HexToHash("0xefef79bf51c3e02c19f9cbe718c6e226ad516153622a500bf783fce2aa8ec7c6")
headerIndex15 := common.HexToHash("0x416f890494e218d3cb32ce1ef3bd08e3acccf6e112b66db544cfcc6295bbdc2a")
headerIndex14 := common.HexToHash("0x74c4e67ca468722a7c3af52c5f96f4bbdd60b4d237ae7693863dca308e3c354c")

h := New(
&mock.Writer{
LastFinalizedState: state.FinalizedHeader{
BeaconBlockRoot: common.Hash{},
BeaconSlot: 50,
InitialCheckpointRoot: common.Hash{},
InitialCheckpointSlot: 0,
},
LastFinalizedStateIndex: 5,
FinalizedBeaconRootByIndex: map[uint32]types.H256{
5: types.H256(headerIndex5),
4: types.H256(headerIndex4),
3: types.H256(headerIndex3),
2: types.H256(headerIndex2),
1: types.H256(headerIndex1),
0: types.H256(headerIndex0),
15: types.H256(headerIndex15),
14: types.H256(headerIndex14),
},
FinalizedHeaderStateByBlockRoot: map[types.H256]state.FinalizedHeader{
types.H256(headerIndex5): state.FinalizedHeader{
BeaconBlockRoot: headerIndex5,
BeaconSlot: 50,
},
types.H256(headerIndex4): state.FinalizedHeader{
BeaconBlockRoot: headerIndex4,
BeaconSlot: 46,
},
types.H256(headerIndex3): state.FinalizedHeader{
BeaconBlockRoot: headerIndex3,
BeaconSlot: 42,
},
types.H256(headerIndex2): state.FinalizedHeader{
BeaconBlockRoot: headerIndex2,
BeaconSlot: 38,
},
types.H256(headerIndex1): state.FinalizedHeader{
BeaconBlockRoot: headerIndex1,
BeaconSlot: 30,
},
types.H256(headerIndex0): state.FinalizedHeader{
BeaconBlockRoot: headerIndex0,
BeaconSlot: 32,
},
types.H256(headerIndex15): state.FinalizedHeader{
BeaconBlockRoot: headerIndex15,
BeaconSlot: 20,
},
types.H256(headerIndex14): state.FinalizedHeader{
BeaconBlockRoot: headerIndex14,
BeaconSlot: 18,
},
},
},
&client,
settings,
&beaconStore,
p,
316,
)

// Slot 20 would be usable to prove slot 19
header, err := h.findLatestCheckPoint(19)
assert.NoError(t, err)
assert.Equal(t, headerIndex15, header.BeaconBlockRoot)
assert.Equal(t, uint64(20), header.BeaconSlot)

// No header would be within range to prove slot 4
_, err = h.findLatestCheckPoint(4)
assert.Error(t, err)

// Slot 46 would be usable to prove slot 19
header, err = h.findLatestCheckPoint(40)
assert.NoError(t, err)
assert.Equal(t, headerIndex4, header.BeaconBlockRoot)
assert.Equal(t, uint64(46), header.BeaconSlot)
}
13 changes: 7 additions & 6 deletions relayer/relays/beacon/header/syncer/syncer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ import (
)

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

func newTestRunner() *Syncer {
return New(api.NewBeaconClient(TestUrl, TestUrl), &mock.Store{}, protocol.New(config.SpecSettings{
SlotsInEpoch: 32,
EpochsPerSyncCommitteePeriod: 256,
DenebForkEpoch: 0,
}))
}, MaxRedundancy))
}

// Verifies that the Lodestar provided finalized endpoint matches the manually constructed finalized endpoint
Expand Down Expand Up @@ -110,7 +111,7 @@ func TestGetFinalizedUpdateWithSyncCommitteeUpdateAtSlot(t *testing.T) {
SlotsInEpoch: 32,
EpochsPerSyncCommitteePeriod: 256,
DenebForkEpoch: 0,
}))
}, MaxRedundancy))

// Manually construct a finalized update
manualUpdate, err := syncer.GetFinalizedUpdateAtAttestedSlot(129, 0, true)
Expand Down Expand Up @@ -165,7 +166,7 @@ func TestFindAttestedAndFinalizedHeadersAtBoundary(t *testing.T) {
SlotsInEpoch: 32,
EpochsPerSyncCommitteePeriod: 256,
DenebForkEpoch: 0,
}))
}, MaxRedundancy))

attested, err := syncer.FindValidAttestedHeader(8000, 8160)
assert.NoError(t, err)
Expand Down Expand Up @@ -195,7 +196,7 @@ func TestFindAttestedAndFinalizedHeadersAtBoundary(t *testing.T) {
SlotsInEpoch: 32,
EpochsPerSyncCommitteePeriod: 256,
DenebForkEpoch: 0,
}))
}, MaxRedundancy))

attested, err = syncer.FindValidAttestedHeader(32576, 32704)
assert.NoError(t, err)
Expand Down Expand Up @@ -225,7 +226,7 @@ func TestFindAttestedAndFinalizedHeadersAtBoundary(t *testing.T) {
SlotsInEpoch: 32,
EpochsPerSyncCommitteePeriod: 256,
DenebForkEpoch: 0,
}))
}, MaxRedundancy))

attested, err = syncer.FindValidAttestedHeader(25076, 32736)
assert.NoError(t, err)
Expand All @@ -249,7 +250,7 @@ func TestFindAttestedAndFinalizedHeadersAtBoundary(t *testing.T) {
SlotsInEpoch: 32,
EpochsPerSyncCommitteePeriod: 256,
DenebForkEpoch: 0,
}))
}, MaxRedundancy))

attested, err = syncer.FindValidAttestedHeader(32540, 32768)
assert.Error(t, err)
Expand Down
Loading

0 comments on commit 02a325b

Please sign in to comment.