diff --git a/flake.lock b/flake.lock index 0d3db2b06..ab1c96a30 100644 --- a/flake.lock +++ b/flake.lock @@ -39,11 +39,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1717405880, - "narHash": "sha256-qcXXOnRSl0sGKm7JknntBU4su8/342YKZvjklHsIl+Q=", + "lastModified": 1714727549, + "narHash": "sha256-CWXRTxxcgMfQubJugpeg3yVWIfm70MYTtgaKWKgD60U=", "owner": "shazow", "repo": "foundry.nix", - "rev": "708c0df1e36b5185a727a3c517a5100e46392792", + "rev": "47cf189ec395eda4b3e0623179d1075c8027ca97", "type": "github" }, "original": { diff --git a/relayer/chain/parachain/writer.go b/relayer/chain/parachain/writer.go index 6291943c3..387734517 100644 --- a/relayer/chain/parachain/writer.go +++ b/relayer/chain/parachain/writer.go @@ -16,7 +16,7 @@ import ( ) type ChainWriter interface { - BatchCall(ctx context.Context, extrinsic string, calls []interface{}) error + BatchCall(ctx context.Context, extrinsic []string, calls []interface{}) error WriteToParachainAndRateLimit(ctx context.Context, extrinsicName string, payload ...interface{}) error WriteToParachainAndWatch(ctx context.Context, extrinsicName string, payload ...interface{}) error GetLastFinalizedHeaderState() (state.FinalizedHeader, error) @@ -34,19 +34,16 @@ type ParachainWriter struct { pool *ExtrinsicPool genesisHash types.Hash maxWatchedExtrinsics int64 - maxBatchCallSize int64 mu sync.Mutex } func NewParachainWriter( conn *Connection, maxWatchedExtrinsics int64, - maxBatchCallSize int64, ) *ParachainWriter { return &ParachainWriter{ conn: conn, maxWatchedExtrinsics: maxWatchedExtrinsics, - maxBatchCallSize: maxBatchCallSize, } } @@ -69,8 +66,8 @@ func (wr *ParachainWriter) Start(ctx context.Context, eg *errgroup.Group) error return nil } -func (wr *ParachainWriter) BatchCall(ctx context.Context, extrinsic string, calls []interface{}) error { - batchSize := int(wr.maxBatchCallSize) +func (wr *ParachainWriter) BatchCall(ctx context.Context, extrinsic []string, calls []interface{}) error { + batchSize := int(wr.maxWatchedExtrinsics) var j int for i := 0; i < len(calls); i += batchSize { j += batchSize @@ -80,13 +77,13 @@ func (wr *ParachainWriter) BatchCall(ctx context.Context, extrinsic string, call slicedCalls := append([]interface{}{}, calls[i:j]...) encodedCalls := make([]types.Call, len(slicedCalls)) for k := range slicedCalls { - call, err := wr.prepCall(extrinsic, slicedCalls[k]) + call, err := wr.prepCall(extrinsic[k], slicedCalls[k]) if err != nil { return err } encodedCalls[k] = *call } - err := wr.WriteToParachainAndRateLimit(ctx, "Utility.batch_all", encodedCalls) + err := wr.WriteToParachainAndWatch(ctx, "Utility.batch_all", encodedCalls) if err != nil { return fmt.Errorf("batch call failed: %w", err) } diff --git a/relayer/cmd/import_execution_header.go b/relayer/cmd/import_execution_header.go index 121fac298..c003131e9 100644 --- a/relayer/cmd/import_execution_header.go +++ b/relayer/cmd/import_execution_header.go @@ -101,7 +101,7 @@ func importExecutionHeaderFn(cmd *cobra.Command, _ []string) error { return fmt.Errorf("connect to parachain: %w", err) } - writer := parachain.NewParachainWriter(paraconn, 8, 8) + writer := parachain.NewParachainWriter(paraconn, 8) err = writer.Start(ctx, eg) if err != nil { return fmt.Errorf("start parachain conn: %w", err) diff --git a/relayer/cmd/run/beacon/command.go b/relayer/cmd/run/beacon/command.go index 3f990b0c2..b4abb8ff5 100644 --- a/relayer/cmd/run/beacon/command.go +++ b/relayer/cmd/run/beacon/command.go @@ -2,6 +2,7 @@ package beacon import ( "context" + "fmt" "log" "os" "os/signal" @@ -54,11 +55,16 @@ func run(_ *cobra.Command, _ []string) error { } var config config.Config - err := viper.Unmarshal(&config) + err := viper.UnmarshalExact(&config) if err != nil { return err } + err = config.Validate() + if err != nil { + return fmt.Errorf("config file validation failed: %w", err) + } + keypair, err := parachain.ResolvePrivateKey(privateKey, privateKeyFile, privateKeyID) if err != nil { return err diff --git a/relayer/cmd/run/beefy/command.go b/relayer/cmd/run/beefy/command.go index 302fe22ed..59f4a461b 100644 --- a/relayer/cmd/run/beefy/command.go +++ b/relayer/cmd/run/beefy/command.go @@ -2,6 +2,7 @@ package beefy import ( "context" + "fmt" "log" "os" "os/signal" @@ -50,11 +51,16 @@ func run(_ *cobra.Command, _ []string) error { } var config beefy.Config - err := viper.Unmarshal(&config) + err := viper.UnmarshalExact(&config) if err != nil { return err } + err = config.Validate() + if err != nil { + return fmt.Errorf("config file validation failed: %w", err) + } + keypair, err := ethereum.ResolvePrivateKey(privateKey, privateKeyFile, privateKeyID) if err != nil { return err diff --git a/relayer/cmd/run/execution/command.go b/relayer/cmd/run/execution/command.go index a4d6b133f..b1a12486d 100644 --- a/relayer/cmd/run/execution/command.go +++ b/relayer/cmd/run/execution/command.go @@ -3,6 +3,7 @@ package execution import ( "context" "encoding/hex" + "fmt" "log" "os" "os/signal" @@ -41,7 +42,6 @@ func Command() *cobra.Command { cmd.Flags().StringVar(&privateKeyFile, "substrate.private-key-file", "", "The file from which to read the private key URI") cmd.Flags().StringVar(&privateKeyID, "substrate.private-key-id", "", "The secret id to lookup the private key in AWS Secrets Manager") - return cmd } @@ -57,11 +57,16 @@ func run(_ *cobra.Command, _ []string) error { } var config execution.Config - err := viper.Unmarshal(&config, viper.DecodeHook(HexHookFunc())) + err := viper.UnmarshalExact(&config, viper.DecodeHook(HexHookFunc())) if err != nil { return err } + err = config.Validate() + if err != nil { + return fmt.Errorf("config file validation failed: %w", err) + } + keypair, err := parachain.ResolvePrivateKey(privateKey, privateKeyFile, privateKeyID) if err != nil { return err diff --git a/relayer/cmd/run/parachain/command.go b/relayer/cmd/run/parachain/command.go index 759d07ac3..7cda7872d 100644 --- a/relayer/cmd/run/parachain/command.go +++ b/relayer/cmd/run/parachain/command.go @@ -3,6 +3,7 @@ package parachain import ( "context" "encoding/hex" + "fmt" "log" "os" "os/signal" @@ -41,7 +42,6 @@ func Command() *cobra.Command { cmd.Flags().StringVar(&privateKeyFile, "ethereum.private-key-file", "", "The file from which to read the private key") cmd.Flags().StringVar(&privateKeyID, "ethereum.private-key-id", "", "The secret id to lookup the private key in AWS Secrets Manager") - return cmd } @@ -55,11 +55,16 @@ func run(_ *cobra.Command, _ []string) error { } var config parachain.Config - err := viper.Unmarshal(&config, viper.DecodeHook(HexHookFunc())) + err := viper.UnmarshalExact(&config, viper.DecodeHook(HexHookFunc())) if err != nil { return err } + err = config.Validate() + if err != nil { + return fmt.Errorf("config file validation failed: %w", err) + } + keypair, err := ethereum.ResolvePrivateKey(privateKey, privateKeyFile, privateKeyID) if err != nil { return err diff --git a/relayer/config/config.go b/relayer/config/config.go index a2ad0161b..71a19a1ca 100644 --- a/relayer/config/config.go +++ b/relayer/config/config.go @@ -1,5 +1,7 @@ package config +import "errors" + type PolkadotConfig struct { Endpoint string `mapstructure:"endpoint"` } @@ -7,7 +9,6 @@ type PolkadotConfig struct { type ParachainConfig struct { Endpoint string `mapstructure:"endpoint"` MaxWatchedExtrinsics int64 `mapstructure:"maxWatchedExtrinsics"` - MaxBatchCallSize int64 `mapstructure:"maxBatchCallSize"` } type EthereumConfig struct { @@ -16,3 +17,27 @@ type EthereumConfig struct { GasTipCap uint64 `mapstructure:"gas-tip-cap"` GasLimit uint64 `mapstructure:"gas-limit"` } + +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") + } + return nil +} + +func (e EthereumConfig) Validate() error { + if e.Endpoint == "" { + return errors.New("[endpoint] config is not set") + } + return nil +} + +func (p PolkadotConfig) Validate() error { + if p.Endpoint == "" { + return errors.New("[endpoint] config is not set") + } + return nil +} diff --git a/relayer/relays/beacon/config/config.go b/relayer/relays/beacon/config/config.go index 95351f40a..1e3b46559 100644 --- a/relayer/relays/beacon/config/config.go +++ b/relayer/relays/beacon/config/config.go @@ -1,6 +1,8 @@ package config import ( + "errors" + "fmt" "github.com/snowfork/snowbridge/relayer/config" ) @@ -33,5 +35,49 @@ type BeaconConfig struct { } type SinkConfig struct { - Parachain config.ParachainConfig `mapstructure:"parachain"` + Parachain config.ParachainConfig `mapstructure:"parachain"` + UpdateSlotInterval uint64 `mapstructure:"updateSlotInterval"` +} + +func (c Config) Validate() error { + err := c.Source.Beacon.Validate() + if err != nil { + return fmt.Errorf("source beacon config: %w", err) + } + err = c.Sink.Parachain.Validate() + if err != nil { + return fmt.Errorf("sink parachain config: %w", err) + } + if c.Sink.UpdateSlotInterval == 0 { + return errors.New("parachain [updateSlotInterval] config is not set") + } + return nil +} + +func (b BeaconConfig) Validate() error { + // spec settings + if b.Spec.EpochsPerSyncCommitteePeriod == 0 { + return errors.New("source beacon setting [epochsPerSyncCommitteePeriod] is not set") + } + if b.Spec.SlotsInEpoch == 0 { + return errors.New("source beacon setting [slotsInEpoch] is not set") + } + if b.Spec.SyncCommitteeSize == 0 { + return errors.New("source beacon setting [syncCommitteeSize] is not set") + } + // data store + if b.DataStore.Location == "" { + return errors.New("source beacon datastore [location] is not set") + } + if b.DataStore.MaxEntries == 0 { + return errors.New("source beacon datastore [maxEntries] is not set") + } + // api endpoints + if b.Endpoint == "" { + return errors.New("source beacon setting [endpoint] is not set") + } + if b.StateEndpoint == "" { + return errors.New("source beacon setting [stateEndpoint] is not set") + } + return nil } diff --git a/relayer/relays/beacon/header/header.go b/relayer/relays/beacon/header/header.go index 47891d920..031578122 100644 --- a/relayer/relays/beacon/header/header.go +++ b/relayer/relays/beacon/header/header.go @@ -30,18 +30,20 @@ var ErrExecutionHeaderNotImported = errors.New("execution header not imported") var ErrBeaconHeaderNotFinalized = errors.New("beacon header not finalized") type Header struct { - cache *cache.BeaconCache - writer parachain.ChainWriter - syncer *syncer.Syncer - protocol *protocol.Protocol + cache *cache.BeaconCache + writer parachain.ChainWriter + syncer *syncer.Syncer + protocol *protocol.Protocol + updateSlotInterval uint64 } -func New(writer parachain.ChainWriter, client api.BeaconAPI, setting config.SpecSettings, store store.BeaconStore, protocol *protocol.Protocol) Header { +func New(writer parachain.ChainWriter, client api.BeaconAPI, setting config.SpecSettings, store store.BeaconStore, protocol *protocol.Protocol, updateSlotInterval uint64) Header { return Header{ - cache: cache.New(setting.SlotsInEpoch, setting.EpochsPerSyncCommitteePeriod), - writer: writer, - syncer: syncer.New(client, store, protocol), - protocol: protocol, + cache: cache.New(setting.SlotsInEpoch, setting.EpochsPerSyncCommitteePeriod), + writer: writer, + syncer: syncer.New(client, store, protocol), + protocol: protocol, + updateSlotInterval: updateSlotInterval, } } @@ -180,17 +182,16 @@ func (h *Header) SyncCommitteePeriodUpdate(ctx context.Context, period uint64) e func (h *Header) SyncFinalizedHeader(ctx context.Context) error { // When the chain has been processed up until now, keep getting finalized block updates and send that to the parachain - update, err := h.syncer.GetFinalizedUpdate() + finalizedHeader, err := h.syncer.GetFinalizedHeader() if err != nil { - return fmt.Errorf("fetch finalized header update from Ethereum beacon client: %w", err) + return fmt.Errorf("fetch finalized header from Ethereum beacon client: %w", err) } log.WithFields(log.Fields{ - "slot": update.Payload.FinalizedHeader.Slot, - "blockRoot": update.FinalizedHeaderBlockRoot, - }).Info("syncing finalized header from Ethereum beacon client") + "slot": finalizedHeader.Slot, + }).Info("checking finalized header") - currentSyncPeriod := h.protocol.ComputeSyncPeriodAtSlot(uint64(update.Payload.AttestedHeader.Slot)) + currentSyncPeriod := h.protocol.ComputeSyncPeriodAtSlot(uint64(finalizedHeader.Slot)) lastSyncedPeriod := h.protocol.ComputeSyncPeriodAtSlot(h.cache.Finalized.LastSyncedSlot) if lastSyncedPeriod < currentSyncPeriod { @@ -200,7 +201,19 @@ func (h *Header) SyncFinalizedHeader(ctx context.Context) error { } } - return h.updateFinalizedHeaderOnchain(ctx, update) + if h.shouldUpdate(uint64(finalizedHeader.Slot), h.cache.Finalized.LastSyncedSlot) { + update, err := h.syncer.GetFinalizedUpdate() + if err != nil { + return fmt.Errorf("fetch finalized update from Ethereum beacon client: %w", err) + } + + err = h.updateFinalizedHeaderOnchain(ctx, update) + if err != nil { + return fmt.Errorf("sync finalized header on-chain: %w", err) + } + } + + return nil } // Write the provided finalized header update (possibly containing a sync committee) on-chain and check if it was @@ -339,8 +352,6 @@ func (h *Header) populateFinalizedCheckpoint(slot uint64) error { return fmt.Errorf("fetch block roots for slot %d: %w", slot, err) } - log.Info("populating checkpoint") - h.cache.AddCheckPoint(blockRoot, blockRootsProof.Tree, slot) return nil @@ -429,25 +440,72 @@ func (h *Header) getHeaderUpdateBySlot(slot uint64) (scale.HeaderUpdatePayload, return h.syncer.GetHeaderUpdate(blockRoot, &checkpoint) } -func (h *Header) FetchExecutionProof(blockRoot common.Hash) (scale.HeaderUpdatePayload, error) { - var headerUpdate scale.HeaderUpdatePayload +func (h *Header) FetchExecutionProof(blockRoot common.Hash, instantVerification bool) (scale.ProofPayload, error) { header, err := h.syncer.Client.GetHeaderByBlockRoot(blockRoot) if err != nil { - return headerUpdate, fmt.Errorf("get beacon header by blockRoot: %w", err) + return scale.ProofPayload{}, fmt.Errorf("get beacon header by blockRoot: %w", err) } lastFinalizedHeaderState, err := h.writer.GetLastFinalizedHeaderState() if err != nil { - return headerUpdate, fmt.Errorf("fetch last finalized header state: %w", err) + return scale.ProofPayload{}, fmt.Errorf("fetch last finalized header state: %w", err) } - if header.Slot > lastFinalizedHeaderState.BeaconSlot { - return headerUpdate, ErrBeaconHeaderNotFinalized - } - headerUpdate, err = h.getHeaderUpdateBySlot(header.Slot) + // The latest finalized header on-chain is older than the header containing the message, so we need to sync the + // finalized header with the message. + finalizedHeader, err := h.syncer.GetFinalizedHeader() if err != nil { - return headerUpdate, fmt.Errorf("get header update by slot with ancestry proof: %w", err) + return scale.ProofPayload{}, err + } + + // If the header is not finalized yet, we can't do anything further. + if header.Slot > uint64(finalizedHeader.Slot) { + return scale.ProofPayload{}, fmt.Errorf("chain not finalized yet: %w", ErrBeaconHeaderNotFinalized) + } + + if header.Slot > lastFinalizedHeaderState.BeaconSlot && !instantVerification { + return scale.ProofPayload{}, fmt.Errorf("on-chain header not recent enough and instantVerification is off: %w", ErrBeaconHeaderNotFinalized) + } + + // There is a finalized header on-chain that will be able to verify the header containing the message. + if header.Slot <= lastFinalizedHeaderState.BeaconSlot { + headerUpdate, err := h.getHeaderUpdateBySlot(header.Slot) + if err != nil { + return scale.ProofPayload{}, fmt.Errorf("get header update by slot with ancestry proof: %w", err) + } + + return scale.ProofPayload{ + HeaderPayload: headerUpdate, + FinalizedPayload: nil, + }, nil + } + + var finalizedUpdate scale.Update + // If we import the last finalized header, the gap between the finalized headers would be too large, so import + // a slightly older header. + if lastFinalizedHeaderState.BeaconSlot+h.protocol.SlotsPerHistoricalRoot < uint64(finalizedHeader.Slot) { + finalizedUpdate, err = h.syncer.GetFinalizedUpdateAtAttestedSlot(header.Slot, lastFinalizedHeaderState.BeaconSlot+h.protocol.SlotsPerHistoricalRoot, false) + if err != nil { + return scale.ProofPayload{}, fmt.Errorf("get finalized update at attested slot: %w", err) + } + } else { + finalizedUpdate, err = h.syncer.GetFinalizedUpdate() + if err != nil { + return scale.ProofPayload{}, fmt.Errorf("get finalized update: %w", err) + } + } + + checkpoint := cache.Proof{ + FinalizedBlockRoot: finalizedUpdate.FinalizedHeaderBlockRoot, + BlockRootsTree: finalizedUpdate.BlockRootsTree, + Slot: uint64(finalizedUpdate.Payload.FinalizedHeader.Slot), } - return headerUpdate, nil + headerUpdate, err := h.syncer.GetHeaderUpdate(blockRoot, &checkpoint) + + return scale.ProofPayload{ + HeaderPayload: headerUpdate, + FinalizedPayload: &finalizedUpdate, + }, nil + } func (h *Header) isInitialSyncPeriod() bool { @@ -492,3 +550,7 @@ func (h *Header) findLatestCheckPoint(slot uint64) (state.FinalizedHeader, error return beaconState, fmt.Errorf("no checkpoint on chain for slot %d", slot) } + +func (h *Header) shouldUpdate(currentFinalizedSlot, latestSyncedSlot uint64) bool { + return currentFinalizedSlot >= latestSyncedSlot+h.updateSlotInterval +} diff --git a/relayer/relays/beacon/header/header_test.go b/relayer/relays/beacon/header/header_test.go index 210fa16f6..5ff7cee5e 100644 --- a/relayer/relays/beacon/header/header_test.go +++ b/relayer/relays/beacon/header/header_test.go @@ -10,6 +10,7 @@ import ( "github.com/snowfork/snowbridge/relayer/relays/beacon/state" "github.com/snowfork/snowbridge/relayer/relays/beacon/store" "github.com/snowfork/snowbridge/relayer/relays/testutil" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" ) @@ -65,6 +66,7 @@ func TestSyncInterimFinalizedUpdate_WithDataFromAPI(t *testing.T) { settings, &beaconStore, p, + 316, ) // Find a checkpoint for a slot that is just out of the on-chain synced finalized header block roots range @@ -128,6 +130,7 @@ func TestSyncInterimFinalizedUpdate_WithDataFromStore(t *testing.T) { settings, &beaconStore, p, + 316, ) // Find a checkpoint for a slot that is just out of the on-chain synced finalized header block roots range @@ -193,6 +196,7 @@ func TestSyncInterimFinalizedUpdate_WithDataFromStoreWithDifferentBlocks(t *test settings, &beaconStore, p, + 316, ) // Find a checkpoint for a slot that is just out of the on-chain synced finalized header block roots range @@ -238,6 +242,7 @@ func TestSyncInterimFinalizedUpdate_BeaconStateNotAvailableInAPIAndStore(t *test settings, &beaconStore, p, + 316, ) // Find a checkpoint for a slot that is just out of the on-chain synced finalized header block roots range @@ -276,9 +281,46 @@ func TestSyncInterimFinalizedUpdate_NoValidBlocksFound(t *testing.T) { settings, &beaconStore, p, + 316, ) // Find a checkpoint for a slot that is just out of the on-chain synced finalized header block roots range _, err = h.syncInterimFinalizedUpdate(context.Background(), 4570722, 4578922) require.Errorf(t, err, "cannot find blocks at boundaries") } + +func TestShouldUpdate(t *testing.T) { + values := []struct { + name string + apiSlot uint64 + onChainSlot uint64 + result bool + }{ + { + name: "should sync, equal to interval", + apiSlot: 500, + onChainSlot: 200, + result: true, + }, + { + name: "should sync, large gap", + apiSlot: 800, + onChainSlot: 200, + result: true, + }, + { + name: "should not sync", + apiSlot: 500, + onChainSlot: 201, + result: false, + }, + } + + h := Header{} + h.updateSlotInterval = 300 + + for _, tt := range values { + result := h.shouldUpdate(tt.apiSlot, tt.onChainSlot) + assert.Equal(t, tt.result, result, "expected %t but found %t", tt.result, result) + } +} diff --git a/relayer/relays/beacon/header/syncer/scale/beacon_scale.go b/relayer/relays/beacon/header/syncer/scale/beacon_scale.go index a78a7f017..18cd8f258 100644 --- a/relayer/relays/beacon/header/syncer/scale/beacon_scale.go +++ b/relayer/relays/beacon/header/syncer/scale/beacon_scale.go @@ -32,6 +32,11 @@ type Update struct { BlockRootsTree *ssz.Node } +type ProofPayload struct { + HeaderPayload HeaderUpdatePayload + FinalizedPayload *Update +} + type UpdatePayload struct { AttestedHeader BeaconHeader SyncAggregate SyncAggregate @@ -42,7 +47,6 @@ type UpdatePayload struct { BlockRootsRoot types.H256 BlockRootsBranch []types.H256 } - type OptionNextSyncCommitteeUpdatePayload struct { HasValue bool Value NextSyncCommitteeUpdatePayload diff --git a/relayer/relays/beacon/header/syncer/syncer.go b/relayer/relays/beacon/header/syncer/syncer.go index 2254b0ec4..7f23cb66e 100644 --- a/relayer/relays/beacon/header/syncer/syncer.go +++ b/relayer/relays/beacon/header/syncer/syncer.go @@ -376,6 +376,20 @@ func (s *Syncer) GetBlockRootsFromState(beaconState state.BeaconState) (scale.Bl }, nil } +func (s *Syncer) GetFinalizedHeader() (scale.BeaconHeader, error) { + finalizedUpdate, err := s.Client.GetLatestFinalizedUpdate() + if err != nil { + return scale.BeaconHeader{}, fmt.Errorf("fetch finalized update: %w", err) + } + + finalizedHeader, err := finalizedUpdate.Data.FinalizedHeader.Beacon.ToScale() + if err != nil { + return scale.BeaconHeader{}, fmt.Errorf("convert finalized header to scale: %w", err) + } + + return finalizedHeader, nil +} + func (s *Syncer) GetFinalizedUpdate() (scale.Update, error) { finalizedUpdate, err := s.Client.GetLatestFinalizedUpdate() if err != nil { @@ -412,6 +426,19 @@ func (s *Syncer) GetFinalizedUpdate() (scale.Update, error) { return scale.Update{}, fmt.Errorf("parse signature slot as int: %w", err) } + signatureBlock, err := s.Client.GetBeaconBlockBySlot(signatureSlot) + if err != nil { + return scale.Update{}, fmt.Errorf("get signature block: %w", err) + } + + superMajority, err := s.protocol.SyncCommitteeSuperMajority(signatureBlock.Data.Message.Body.SyncAggregate.SyncCommitteeBits) + if err != nil { + return scale.Update{}, fmt.Errorf("compute sync committee supermajority: %d err: %w", signatureSlot, err) + } + if !superMajority { + return scale.Update{}, fmt.Errorf("sync committee at slot not supermajority: %d", signatureSlot) + } + updatePayload := scale.UpdatePayload{ AttestedHeader: attestedHeader, SyncAggregate: syncAggregate, @@ -719,6 +746,7 @@ func (s *Syncer) GetFinalizedUpdateAtAttestedSlot(minSlot, maxSlot uint64, fetch // Try getting beacon data from the API first data, err := s.getBeaconDataFromClient(attestedSlot) if err != nil { + log.WithError(err).Warn("unable to fetch beacon data from API, trying beacon store") // If it fails, using the beacon store and look for a relevant finalized update for { if minSlot > maxSlot { @@ -941,6 +969,7 @@ func (s *Syncer) getBestMatchBeaconDataFromStore(minSlot, maxSlot uint64) (final func (s *Syncer) getBeaconState(slot uint64) ([]byte, error) { data, err := s.Client.GetBeaconState(strconv.FormatUint(slot, 10)) if err != nil { + log.WithError(err).Warn("unable to fetch beacon state from API, trying beacon store") data, err = s.store.GetBeaconStateData(slot) if err != nil { return nil, fmt.Errorf("fetch beacon state from store: %w", err) diff --git a/relayer/relays/beacon/main.go b/relayer/relays/beacon/main.go index 3c80885b7..2fc42223f 100644 --- a/relayer/relays/beacon/main.go +++ b/relayer/relays/beacon/main.go @@ -44,7 +44,6 @@ func (r *Relay) Start(ctx context.Context, eg *errgroup.Group) error { writer := parachain.NewParachainWriter( paraconn, r.config.Sink.Parachain.MaxWatchedExtrinsics, - r.config.Sink.Parachain.MaxBatchCallSize, ) p := protocol.New(specSettings) @@ -67,6 +66,7 @@ func (r *Relay) Start(ctx context.Context, eg *errgroup.Group) error { specSettings, &s, p, + r.config.Sink.UpdateSlotInterval, ) return headers.Sync(ctx, eg) diff --git a/relayer/relays/beacon/mock/mock_writer.go b/relayer/relays/beacon/mock/mock_writer.go index 15b48ab6d..b0700e7cc 100644 --- a/relayer/relays/beacon/mock/mock_writer.go +++ b/relayer/relays/beacon/mock/mock_writer.go @@ -27,7 +27,7 @@ func (m *Writer) GetFinalizedBeaconRootByIndex(index uint32) (types.H256, error) return types.H256{}, nil } -func (m *Writer) BatchCall(ctx context.Context, extrinsic string, calls []interface{}) error { +func (m *Writer) BatchCall(ctx context.Context, extrinsic []string, calls []interface{}) error { return nil } diff --git a/relayer/relays/beacon/protocol/protocol_test.go b/relayer/relays/beacon/protocol/protocol_test.go index 2ad82608c..a8a2c5b8d 100644 --- a/relayer/relays/beacon/protocol/protocol_test.go +++ b/relayer/relays/beacon/protocol/protocol_test.go @@ -1,6 +1,7 @@ package protocol import ( + "errors" "testing" "github.com/stretchr/testify/assert" @@ -75,3 +76,46 @@ 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 TestSyncCommitteeBits(t *testing.T) { + values := []struct { + name string + bits string + expected bool + err error + }{ + { + name: "empty1", + bits: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + expected: false, + err: nil, + }, + { + name: "not supermajority", + bits: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000000000000000", + expected: false, + err: nil, + }, + { + name: "supermajority", + bits: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000", + expected: true, + err: nil, + }, + { + name: "invalid hex", + bits: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000", + expected: false, + err: errors.New("encoding/hex: odd length hex string"), + }, + } + + p := Protocol{} + p.Settings.SyncCommitteeSize = 512 + + for _, tt := range values { + result, err := p.SyncCommitteeSuperMajority(tt.bits) + assert.Equal(t, tt.err, err, "expected %t but found %t", tt.err, err) + assert.Equal(t, tt.expected, result, "expected %t but found %t", tt.expected, result) + } +} diff --git a/relayer/relays/beefy/config.go b/relayer/relays/beefy/config.go index b6a472bbb..cf8735a84 100644 --- a/relayer/relays/beefy/config.go +++ b/relayer/relays/beefy/config.go @@ -1,6 +1,7 @@ package beefy import ( + "fmt" "github.com/snowfork/snowbridge/relayer/config" ) @@ -26,3 +27,21 @@ type SinkConfig struct { type ContractsConfig struct { BeefyClient string `mapstructure:"BeefyClient"` } + +func (c Config) Validate() error { + err := c.Source.Polkadot.Validate() + if err != nil { + return fmt.Errorf("source polkadot config: %w", err) + } + err = c.Sink.Ethereum.Validate() + if err != nil { + return fmt.Errorf("sink ethereum config: %w", err) + } + if c.Sink.DescendantsUntilFinal == 0 { + return fmt.Errorf("sink ethereum setting [descendants-until-final] is not set") + } + if c.Sink.Contracts.BeefyClient == "" { + return fmt.Errorf("sink contracts setting [BeefyClient] is not set") + } + return nil +} diff --git a/relayer/relays/execution/config.go b/relayer/relays/execution/config.go index 1f97b635f..6a474e8ba 100644 --- a/relayer/relays/execution/config.go +++ b/relayer/relays/execution/config.go @@ -1,13 +1,15 @@ package execution import ( + "fmt" "github.com/snowfork/snowbridge/relayer/config" beaconconf "github.com/snowfork/snowbridge/relayer/relays/beacon/config" ) type Config struct { - Source SourceConfig `mapstructure:"source"` - Sink SinkConfig `mapstructure:"sink"` + Source SourceConfig `mapstructure:"source"` + Sink SinkConfig `mapstructure:"sink"` + InstantVerification bool `mapstructure:"instantVerification"` } type SourceConfig struct { @@ -26,3 +28,21 @@ type SinkConfig struct { } type ChannelID [32]byte + +func (c Config) Validate() error { + err := c.Source.Beacon.Validate() + if err != nil { + return fmt.Errorf("beacon config validation: %w", err) + } + err = c.Sink.Parachain.Validate() + if err != nil { + return fmt.Errorf("parachain config validation: %w", err) + } + if c.Source.ChannelID == [32]byte{} { + return fmt.Errorf("source setting [channel-id] is not set") + } + if c.Source.Contracts.Gateway == "" { + return fmt.Errorf("source setting [gateway] is not set") + } + return nil +} diff --git a/relayer/relays/execution/main.go b/relayer/relays/execution/main.go index 210cc2843..c16c03270 100644 --- a/relayer/relays/execution/main.go +++ b/relayer/relays/execution/main.go @@ -2,6 +2,7 @@ package execution import ( "context" + "errors" "fmt" "math/big" "sort" @@ -18,6 +19,7 @@ import ( "github.com/snowfork/snowbridge/relayer/crypto/sr25519" "github.com/snowfork/snowbridge/relayer/relays/beacon/header" "github.com/snowfork/snowbridge/relayer/relays/beacon/header/syncer/api" + "github.com/snowfork/snowbridge/relayer/relays/beacon/header/syncer/scale" "github.com/snowfork/snowbridge/relayer/relays/beacon/protocol" "github.com/snowfork/snowbridge/relayer/relays/beacon/store" "golang.org/x/sync/errgroup" @@ -30,6 +32,7 @@ type Relay struct { ethconn *ethereum.Connection gatewayContract *contracts.Gateway beaconHeader *header.Header + writer *parachain.ParachainWriter } func NewRelay( @@ -58,13 +61,12 @@ func (r *Relay) Start(ctx context.Context, eg *errgroup.Group) error { } r.ethconn = ethconn - writer := parachain.NewParachainWriter( + r.writer = parachain.NewParachainWriter( paraconn, r.config.Sink.Parachain.MaxWatchedExtrinsics, - r.config.Sink.Parachain.MaxBatchCallSize, ) - err = writer.Start(ctx, eg) + err = r.writer.Start(ctx, eg) if err != nil { return err } @@ -87,15 +89,15 @@ func (r *Relay) Start(ctx context.Context, eg *errgroup.Group) error { store := store.New(r.config.Source.Beacon.DataStore.Location, r.config.Source.Beacon.DataStore.MaxEntries, *p) store.Connect() - defer store.Close() beaconAPI := api.NewBeaconClient(r.config.Source.Beacon.Endpoint, r.config.Source.Beacon.StateEndpoint) beaconHeader := header.New( - writer, + r.writer, beaconAPI, r.config.Source.Beacon.Spec, &store, p, + 0, // setting is not used in the execution relay ) r.beaconHeader = &beaconHeader @@ -119,9 +121,10 @@ func (r *Relay) Start(ctx context.Context, eg *errgroup.Group) error { } log.WithFields(log.Fields{ - "channelId": types.H256(r.config.Source.ChannelID).Hex(), - "paraNonce": paraNonce, - "ethNonce": ethNonce, + "channelId": types.H256(r.config.Source.ChannelID).Hex(), + "paraNonce": paraNonce, + "ethNonce": ethNonce, + "instantVerification": r.config.InstantVerification, }).Info("Polled Nonces") if paraNonce == ethNonce { @@ -167,29 +170,22 @@ func (r *Relay) Start(ctx context.Context, eg *errgroup.Group) error { } // ParentBeaconRoot in https://eips.ethereum.org/EIPS/eip-4788 from Deneb onward - executionProof, err := beaconHeader.FetchExecutionProof(*blockHeader.ParentBeaconRoot) - if err == header.ErrBeaconHeaderNotFinalized { + proof, err := beaconHeader.FetchExecutionProof(*blockHeader.ParentBeaconRoot, r.config.InstantVerification) + if errors.Is(err, header.ErrBeaconHeaderNotFinalized) { logger.Warn("beacon header not finalized, just skipped") continue } if err != nil { return fmt.Errorf("fetch execution header proof: %w", err) } - inboundMsg.Proof.ExecutionProof = executionProof - logger.WithFields(logrus.Fields{ - "EventLog": inboundMsg.EventLog, - "Proof": inboundMsg.Proof, - }).Debug("Generated message from Ethereum log") - - err = writer.WriteToParachainAndWatch(ctx, "EthereumInboundQueue.submit", inboundMsg) + err = r.writeToParachain(ctx, proof, inboundMsg) if err != nil { - logger.Error("inbound message fail to sent") return fmt.Errorf("write to parachain: %w", err) } + paraNonce, _ = r.fetchLatestParachainNonce() if paraNonce != ev.Nonce { - logger.Error("inbound message sent but fail to execute") return fmt.Errorf("inbound message fail to execute") } logger.Info("inbound message executed successfully") @@ -198,6 +194,41 @@ func (r *Relay) Start(ctx context.Context, eg *errgroup.Group) error { } } +func (r *Relay) writeToParachain(ctx context.Context, proof scale.ProofPayload, inboundMsg *parachain.Message) error { + inboundMsg.Proof.ExecutionProof = proof.HeaderPayload + + log.WithFields(logrus.Fields{ + "EventLog": inboundMsg.EventLog, + "Proof": inboundMsg.Proof, + }).Debug("Generated message from Ethereum log") + + // There is already a valid finalized header on-chain that can prove the message + if proof.FinalizedPayload == nil { + err := r.writer.WriteToParachainAndWatch(ctx, "EthereumInboundQueue.submit", inboundMsg) + if err != nil { + return fmt.Errorf("submit message to inbound queue: %w", err) + } + + return nil + } + + log.WithFields(logrus.Fields{ + "finalized_slot": proof.FinalizedPayload.Payload.FinalizedHeader.Slot, + "finalized_root": proof.FinalizedPayload.FinalizedHeaderBlockRoot, + "message_slot": proof.HeaderPayload.Header.Slot, + }).Debug("Batching finalized header update with message") + + extrinsics := []string{"EthereumBeaconClient.submit", "EthereumInboundQueue.submit"} + payloads := []interface{}{proof.FinalizedPayload.Payload, inboundMsg} + // Batch the finalized header update with the inbound message + err := r.writer.BatchCall(ctx, extrinsics, payloads) + if err != nil { + return fmt.Errorf("batch call containing finalized header update and inbound queue message: %w", err) + } + + return nil +} + func (r *Relay) fetchLatestParachainNonce() (uint64, error) { paraID := r.config.Source.ChannelID encodedParaID, err := types.EncodeToBytes(r.config.Source.ChannelID) @@ -250,8 +281,6 @@ func (r *Relay) findEvents( blockNumber := latestFinalizedBlockNumber for { - log.Info("loop") - var begin uint64 if blockNumber < BlocksPerQuery { begin = 0 @@ -345,5 +374,11 @@ func (r *Relay) makeInboundMessage( return nil, err } + log.WithFields(logrus.Fields{ + "blockHash": event.Raw.BlockHash.Hex(), + "blockNumber": event.Raw.BlockNumber, + "txHash": event.Raw.TxHash.Hex(), + }).Info("found message") + return msg, nil } diff --git a/relayer/relays/parachain/config.go b/relayer/relays/parachain/config.go index e4eafb388..fcd8a32e4 100644 --- a/relayer/relays/parachain/config.go +++ b/relayer/relays/parachain/config.go @@ -1,6 +1,7 @@ package parachain import ( + "fmt" "github.com/snowfork/snowbridge/relayer/config" ) @@ -32,3 +33,38 @@ type SinkContractsConfig struct { } type ChannelID [32]byte + +func (c Config) Validate() error { + // Source + err := c.Source.Polkadot.Validate() + if err != nil { + return fmt.Errorf("source polkadot config: %w", err) + } + err = c.Source.Parachain.Validate() + if err != nil { + return fmt.Errorf("source parachain config: %w", err) + } + err = c.Source.Ethereum.Validate() + if err != nil { + return fmt.Errorf("source ethereum config: %w", err) + } + if c.Source.Contracts.BeefyClient == "" { + return fmt.Errorf("source contracts setting [BeefyClient] is not set") + } + if c.Source.Contracts.Gateway == "" { + return fmt.Errorf("source contracts setting [Gateway] is not set") + } + if c.Source.ChannelID == [32]byte{} { + return fmt.Errorf("source setting [channel-id] is not set") + } + + // Sink + err = c.Sink.Ethereum.Validate() + if err != nil { + return fmt.Errorf("sink ethereum config: %w", err) + } + if c.Sink.Contracts.Gateway == "" { + return fmt.Errorf("sink contracts setting [Gateway] is not set") + } + return nil +} diff --git a/smoketest/Cargo.lock b/smoketest/Cargo.lock index 3d39e0475..b7bc5bd1c 100644 --- a/smoketest/Cargo.lock +++ b/smoketest/Cargo.lock @@ -4439,9 +4439,8 @@ dependencies = [ [[package]] name = "sp-arithmetic" -version = "23.0.0" +version = "24.0.0" dependencies = [ - "docify", "integer-sqrt", "num-traits", "parity-scale-codec", @@ -4768,15 +4767,16 @@ dependencies = [ [[package]] name = "sp-weights" -version = "27.0.0" +version = "28.0.0" dependencies = [ "bounded-collections", "parity-scale-codec", "scale-info", "serde", "smallvec", - "sp-arithmetic 23.0.0", + "sp-arithmetic 24.0.0", "sp-debug-derive 14.0.0", + "sp-std 14.0.0", ] [[package]] @@ -4840,7 +4840,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "staging-xcm" -version = "7.0.0" +version = "8.0.1" dependencies = [ "array-bytes", "bounded-collections", @@ -4851,7 +4851,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", - "sp-weights 27.0.0", + "sp-weights 28.0.0", "xcm-procedural", ] @@ -6247,7 +6247,7 @@ dependencies = [ [[package]] name = "xcm-procedural" -version = "7.0.0" +version = "8.0.0" dependencies = [ "Inflector", "proc-macro2", diff --git a/web/packages/test/config/beacon-relay.json b/web/packages/test/config/beacon-relay.json index bbc033b5c..6528c4986 100644 --- a/web/packages/test/config/beacon-relay.json +++ b/web/packages/test/config/beacon-relay.json @@ -18,8 +18,8 @@ "sink": { "parachain": { "endpoint": "ws://127.0.0.1:11144", - "maxWatchedExtrinsics": 8, - "maxBatchCallSize": 8 - } + "maxWatchedExtrinsics": 8 + }, + "updateSlotInterval": 316 } } diff --git a/web/packages/test/config/execution-relay.json b/web/packages/test/config/execution-relay.json index c1e8b72f0..2d3d66c90 100644 --- a/web/packages/test/config/execution-relay.json +++ b/web/packages/test/config/execution-relay.json @@ -11,17 +11,22 @@ "endpoint": "http://127.0.0.1:9596", "stateEndpoint": "http://127.0.0.1:9596", "spec": { + "syncCommitteeSize": 512, "slotsInEpoch": 32, "epochsPerSyncCommitteePeriod": 256, "denebForkedEpoch": 0 + }, + "datastore": { + "location": "/tmp/snowbridge/beaconstore", + "maxEntries": 100 } } }, "sink": { "parachain": { "endpoint": "ws://127.0.0.1:11144", - "maxWatchedExtrinsics": 8, - "maxBatchCallSize": 8 + "maxWatchedExtrinsics": 8 } - } + }, + "instantVerification": false } diff --git a/web/packages/test/config/parachain-relay.json b/web/packages/test/config/parachain-relay.json index 8348c60db..8af630607 100644 --- a/web/packages/test/config/parachain-relay.json +++ b/web/packages/test/config/parachain-relay.json @@ -7,13 +7,13 @@ "endpoint": "ws://127.0.0.1:9944" }, "parachain": { - "endpoint": "ws://127.0.0.1:11144" + "endpoint": "ws://127.0.0.1:11144", + "maxWatchedExtrinsics": 8 }, "contracts": { "BeefyClient": null, "Gateway": null }, - "beefy-activation-block": 0, "channel-id": null }, "sink": { diff --git a/web/packages/test/scripts/start-relayer.sh b/web/packages/test/scripts/start-relayer.sh index 029397b69..cb1a2b392 100755 --- a/web/packages/test/scripts/start-relayer.sh +++ b/web/packages/test/scripts/start-relayer.sh @@ -11,7 +11,6 @@ config_relayer() { --arg eth_gas_limit $eth_gas_limit \ ' .sink.contracts.BeefyClient = $k1 - | .source.ethereum.endpoint = $eth_endpoint_ws | .sink.ethereum.endpoint = $eth_endpoint_ws | .sink.ethereum."gas-limit" = $eth_gas_limit ' \