From c5767447d9f37da4a922e3622740ac4958414df9 Mon Sep 17 00:00:00 2001 From: Vincent Geddes <117534+vgeddes@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:26:24 +0200 Subject: [PATCH] Split BEEFY relayer into two separate relayers (#1216) Co-authored-by: ron --- contracts/foundry.toml | 19 +- contracts/scripts/deploy.sh | 15 ++ relayer/cmd/root.go | 1 + relayer/cmd/scan_beefy.go | 7 +- relayer/cmd/sync_beefy_commitment.go | 64 +++++ relayer/relays/beefy/ethereum-writer.go | 92 +++++--- relayer/relays/beefy/fixture-data-logger.go | 1 - relayer/relays/beefy/main.go | 95 ++++++-- relayer/relays/beefy/parameters.go | 29 +-- relayer/relays/beefy/polkadot-listener.go | 191 +++++++++------ relayer/relays/beefy/scanner.go | 249 +++++++++----------- relayer/relays/beefy/task.go | 11 +- 12 files changed, 483 insertions(+), 291 deletions(-) create mode 100755 contracts/scripts/deploy.sh create mode 100644 relayer/cmd/sync_beefy_commitment.go diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 24dea2d8e9..088aa9a909 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -1,14 +1,19 @@ [profile.default] -solc_version = "0.8.23" +solc_version = "0.8.25" optimizer = true -optimizer_runs = 20_000 -via_ir = true +optimizer_runs = 20000 +via_ir = false test = 'test' -fs_permissions = [{ access = "read-write", path = "test/data"}, { access = "read", path = "./"}] +script = 'scripts' +fs_permissions = [{ access = "read-write", path = "test/data" }, { access = "read", path = "./" }] ignored_error_codes = [ - # DeployScript.sol is never deployed - 5574 + # DeployLocal.sol is never deployed + 5574, ] -# no_match_test = "testRegenerateBitField" +[profile.production] +via_ir = true + +[profile.production.etherscan] +mainnet = { key = "${ETHERSCAN_API_KEY}" } diff --git a/contracts/scripts/deploy.sh b/contracts/scripts/deploy.sh new file mode 100755 index 0000000000..8f35b74644 --- /dev/null +++ b/contracts/scripts/deploy.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -eux + +forge script "scripts/DeployBeefyClient.sol:DeployBeefyClient" \ + --chain-id 1 \ + --rpc-url "${MAINNET_RPC_URL}" \ + --ledger \ + --mnemonic-derivation-paths "${MNEMONIC_DERIVATION_PATH}" \ + --broadcast \ + --verify \ + --optimize \ + --via-ir \ + --optimizer-runs 100000 \ + -vvvv diff --git a/relayer/cmd/root.go b/relayer/cmd/root.go index 3327c2485d..53a398022d 100644 --- a/relayer/cmd/root.go +++ b/relayer/cmd/root.go @@ -35,6 +35,7 @@ func init() { rootCmd.AddCommand(storeBeaconStateCmd()) rootCmd.AddCommand(importBeaconStateCmd()) rootCmd.AddCommand(listBeaconStateCmd()) + rootCmd.AddCommand(syncBeefyCommitmentCmd()) } func Execute() { diff --git a/relayer/cmd/scan_beefy.go b/relayer/cmd/scan_beefy.go index 6d7cf00070..5c54550739 100644 --- a/relayer/cmd/scan_beefy.go +++ b/relayer/cmd/scan_beefy.go @@ -55,10 +55,9 @@ func ScanBeefyFn(cmd *cobra.Command, _ []string) error { beefyBlock, _ := cmd.Flags().GetUint64("beefy-block") validatorSetID, _ := cmd.Flags().GetUint64("validator-set-id") logrus.WithFields(logrus.Fields{ - "polkadot-url": polkadotUrl, - "fast-forward-depth": fastForwardDepth, - "beefy-block": beefyBlock, - "validator-set-id": validatorSetID, + "polkadot-url": polkadotUrl, + "beefy-block": beefyBlock, + "validator-set-id": validatorSetID, }).Info("Connected to relaychain.") commitments, err := polkadotListener.Start(ctx, eg, beefyBlock, validatorSetID) diff --git a/relayer/cmd/sync_beefy_commitment.go b/relayer/cmd/sync_beefy_commitment.go new file mode 100644 index 0000000000..444840e33d --- /dev/null +++ b/relayer/cmd/sync_beefy_commitment.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "fmt" + "log" + + "github.com/sirupsen/logrus" + "github.com/snowfork/snowbridge/relayer/chain/ethereum" + "github.com/snowfork/snowbridge/relayer/relays/beefy" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func syncBeefyCommitmentCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "sync-latest-beefy-commitment", + Short: "Sync beefy commitment on demand", + Args: cobra.ExactArgs(0), + RunE: SyncBeefyCommitmentFn, + } + + cmd.Flags().String("config", "/tmp/snowbridge/beefy-relay.json", "Path to configuration file") + cmd.Flags().String("private-key", "", "Ethereum private key") + cmd.Flags().String("private-key-file", "", "The file from which to read the private key") + cmd.Flags().Uint64P("block-number", "b", 0, "Relay block number which contains a Parachain message") + cmd.MarkFlagRequired("block-number") + return cmd +} + +func SyncBeefyCommitmentFn(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + + log.SetOutput(logrus.WithFields(logrus.Fields{"logger": "stdlib"}).WriterLevel(logrus.InfoLevel)) + logrus.SetLevel(logrus.DebugLevel) + + configFile, err := cmd.Flags().GetString("config") + viper.SetConfigFile(configFile) + if err := viper.ReadInConfig(); err != nil { + return err + } + + var config beefy.Config + err = viper.Unmarshal(&config) + if err != nil { + return err + } + privateKey, _ := cmd.Flags().GetString("private-key") + privateKeyFile, _ := cmd.Flags().GetString("private-key-file") + if privateKey == "" && privateKeyFile == "" { + return fmt.Errorf("missing private key") + } + keypair, err := ethereum.ResolvePrivateKey(privateKey, privateKeyFile) + if err != nil { + return err + } + + relay, err := beefy.NewRelay(&config, keypair) + if err != nil { + return err + } + blockNumber, _ := cmd.Flags().GetUint64("block-number") + err = relay.OneShotSync(ctx, blockNumber) + return err +} diff --git a/relayer/relays/beefy/ethereum-writer.go b/relayer/relays/beefy/ethereum-writer.go index f9d88c3572..187c190f97 100644 --- a/relayer/relays/beefy/ethereum-writer.go +++ b/relayer/relays/beefy/ethereum-writer.go @@ -40,23 +40,6 @@ func NewEthereumWriter( } func (wr *EthereumWriter) Start(ctx context.Context, eg *errgroup.Group, requests <-chan Request) error { - address := common.HexToAddress(wr.config.Contracts.BeefyClient) - contract, err := contracts.NewBeefyClient(address, wr.conn.Client()) - if err != nil { - return fmt.Errorf("create beefy client: %w", err) - } - wr.contract = contract - - callOpts := bind.CallOpts{ - Context: ctx, - } - blockWaitPeriod, err := wr.contract.RandaoCommitDelay(&callOpts) - if err != nil { - return fmt.Errorf("create randao commit delay: %w", err) - } - wr.blockWaitPeriod = blockWaitPeriod.Uint64() - log.WithField("randaoCommitDelay", wr.blockWaitPeriod).Trace("Fetched randaoCommitDelay") - // launch task processor eg.Go(func() error { for { @@ -68,7 +51,24 @@ func (wr *EthereumWriter) Start(ctx context.Context, eg *errgroup.Group, request return nil } - err := wr.submit(ctx, task) + state, err := wr.queryBeefyClientState(ctx) + if err != nil { + return fmt.Errorf("query beefy client state: %w", err) + } + + if task.SignedCommitment.Commitment.BlockNumber < uint32(state.LatestBeefyBlock) { + log.WithFields(logrus.Fields{ + "beefyBlockNumber": task.SignedCommitment.Commitment.BlockNumber, + "latestBeefyBlock": state.LatestBeefyBlock, + }).Info("Commitment already synced") + continue + } + + // Mandatory commitments are always signed by the next validator set recorded in + // the beefy light client + task.ValidatorsRoot = state.NextValidatorSetRoot + + err = wr.submit(ctx, task) if err != nil { return fmt.Errorf("submit request: %w", err) } @@ -79,32 +79,43 @@ func (wr *EthereumWriter) Start(ctx context.Context, eg *errgroup.Group, request return nil } -func (wr *EthereumWriter) submit(ctx context.Context, task Request) error { +type BeefyClientState struct { + LatestBeefyBlock uint64 + CurrentValidatorSetID uint64 + CurrentValidatorSetRoot [32]byte + NextValidatorSetID uint64 + NextValidatorSetRoot [32]byte +} + +func (wr *EthereumWriter) queryBeefyClientState(ctx context.Context) (*BeefyClientState, error) { callOpts := bind.CallOpts{ Context: ctx, } latestBeefyBlock, err := wr.contract.LatestBeefyBlock(&callOpts) if err != nil { - return err - } - if uint32(latestBeefyBlock) >= task.SignedCommitment.Commitment.BlockNumber { - return nil + return nil, err } currentValidatorSet, err := wr.contract.CurrentValidatorSet(&callOpts) if err != nil { - return err + return nil, err } nextValidatorSet, err := wr.contract.NextValidatorSet(&callOpts) if err != nil { - return err - } - task.ValidatorsRoot = currentValidatorSet.Root - if task.IsHandover { - task.ValidatorsRoot = nextValidatorSet.Root + return nil, err } + return &BeefyClientState{ + LatestBeefyBlock: latestBeefyBlock, + CurrentValidatorSetID: currentValidatorSet.Id.Uint64(), + CurrentValidatorSetRoot: currentValidatorSet.Root, + NextValidatorSetID: nextValidatorSet.Id.Uint64(), + NextValidatorSetRoot: nextValidatorSet.Root, + }, nil +} + +func (wr *EthereumWriter) submit(ctx context.Context, task Request) error { // Initial submission tx, initialBitfield, err := wr.doSubmitInitial(ctx, &task) if err != nil { @@ -131,6 +142,7 @@ func (wr *EthereumWriter) submit(ctx context.Context, task Request) error { wr.conn.MakeTxOpts(ctx), *commitmentHash, ) + _, err = wr.conn.WatchTransaction(ctx, tx, 1) if err != nil { log.WithError(err).Error("Failed to CommitPrevRandao") @@ -153,7 +165,6 @@ func (wr *EthereumWriter) submit(ctx context.Context, task Request) error { log.WithFields(logrus.Fields{ "tx": tx.Hash().Hex(), "blockNumber": task.SignedCommitment.Commitment.BlockNumber, - "IsHandover": task.IsHandover, }).Debug("Transaction SubmitFinal succeeded") return nil @@ -267,3 +278,24 @@ func (wr *EthereumWriter) doSubmitFinal(ctx context.Context, commitmentHash [32] return tx, nil } + +func (wr *EthereumWriter) initialize(ctx context.Context) error { + address := common.HexToAddress(wr.config.Contracts.BeefyClient) + contract, err := contracts.NewBeefyClient(address, wr.conn.Client()) + if err != nil { + return fmt.Errorf("create beefy client: %w", err) + } + wr.contract = contract + + callOpts := bind.CallOpts{ + Context: ctx, + } + blockWaitPeriod, err := wr.contract.RandaoCommitDelay(&callOpts) + if err != nil { + return fmt.Errorf("create randao commit delay: %w", err) + } + wr.blockWaitPeriod = blockWaitPeriod.Uint64() + log.WithField("randaoCommitDelay", wr.blockWaitPeriod).Trace("Fetched randaoCommitDelay") + + return nil +} diff --git a/relayer/relays/beefy/fixture-data-logger.go b/relayer/relays/beefy/fixture-data-logger.go index 566868fe8c..c713814097 100644 --- a/relayer/relays/beefy/fixture-data-logger.go +++ b/relayer/relays/beefy/fixture-data-logger.go @@ -48,7 +48,6 @@ func (wr *EthereumWriter) makeSubmitFinalLogFields( "leafProofOrder": params.LeafProofOrder, }, "commitmentHash": commitmentHash, - "handover": task.IsHandover, } return fields, nil diff --git a/relayer/relays/beefy/main.go b/relayer/relays/beefy/main.go index af7a6b9fac..209e33e5d8 100644 --- a/relayer/relays/beefy/main.go +++ b/relayer/relays/beefy/main.go @@ -6,11 +6,8 @@ import ( "golang.org/x/sync/errgroup" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" "github.com/snowfork/snowbridge/relayer/chain/ethereum" "github.com/snowfork/snowbridge/relayer/chain/relaychain" - "github.com/snowfork/snowbridge/relayer/contracts" "github.com/snowfork/snowbridge/relayer/crypto/secp256k1" log "github.com/sirupsen/logrus" @@ -56,49 +53,109 @@ func (relay *Relay) Start(ctx context.Context, eg *errgroup.Group) error { if err != nil { return fmt.Errorf("create ethereum connection: %w", err) } + err = relay.ethereumWriter.initialize(ctx) + if err != nil { + return fmt.Errorf("initialize ethereum writer: %w", err) + } - initialBeefyBlock, initialValidatorSetID, err := relay.getInitialState(ctx) + initialState, err := relay.ethereumWriter.queryBeefyClientState(ctx) if err != nil { return fmt.Errorf("fetch BeefyClient current state: %w", err) } log.WithFields(log.Fields{ - "beefyBlock": initialBeefyBlock, - "validatorSetID": initialValidatorSetID, + "beefyBlock": initialState.LatestBeefyBlock, + "validatorSetID": initialState.CurrentValidatorSetID, }).Info("Retrieved current BeefyClient state") - requests, err := relay.polkadotListener.Start(ctx, eg, initialBeefyBlock, initialValidatorSetID) + requests, err := relay.polkadotListener.Start(ctx, eg, initialState.LatestBeefyBlock, initialState.CurrentValidatorSetID) if err != nil { return fmt.Errorf("initialize polkadot listener: %w", err) } err = relay.ethereumWriter.Start(ctx, eg, requests) if err != nil { - return fmt.Errorf("initialize ethereum writer: %w", err) + return fmt.Errorf("start ethereum writer: %w", err) } return nil } -func (relay *Relay) getInitialState(ctx context.Context) (uint64, uint64, error) { - address := common.HexToAddress(relay.config.Sink.Contracts.BeefyClient) - beefyClient, err := contracts.NewBeefyClient(address, relay.ethereumConn.Client()) +func (relay *Relay) OneShotSync(ctx context.Context, blockNumber uint64) error { + // Initialize relaychainConn + err := relay.relaychainConn.Connect(ctx) if err != nil { - return 0, 0, err + return fmt.Errorf("create relaychain connection: %w", err) } - callOpts := bind.CallOpts{ - Context: ctx, + // Initialize ethereumConn + err = relay.ethereumConn.Connect(ctx) + if err != nil { + return fmt.Errorf("create ethereum connection: %w", err) + } + err = relay.ethereumWriter.initialize(ctx) + if err != nil { + return fmt.Errorf("initialize EthereumWriter: %w", err) } - latestBeefyBlock, err := beefyClient.LatestBeefyBlock(&callOpts) + state, err := relay.ethereumWriter.queryBeefyClientState(ctx) if err != nil { - return 0, 0, err + return fmt.Errorf("query beefy client state: %w", err) + } + // Ignore relay block already synced + if blockNumber <= state.LatestBeefyBlock { + log.WithFields(log.Fields{ + "validatorSetID": state.CurrentValidatorSetID, + "beefyBlock": state.LatestBeefyBlock, + "relayBlock": blockNumber, + }).Info("Relay block already synced, just ignore") + return nil } - currentValidatorSet, err := beefyClient.CurrentValidatorSet(&callOpts) + // generate beefy update for that specific relay block + task, err := relay.polkadotListener.generateBeefyUpdate(blockNumber) if err != nil { - return 0, 0, err + return fmt.Errorf("fail to generate next beefy request: %w", err) } - return latestBeefyBlock, currentValidatorSet.Id.Uint64(), nil + // Ignore commitment earlier than LatestBeefyBlock which is outdated + if task.SignedCommitment.Commitment.BlockNumber <= uint32(state.LatestBeefyBlock) { + log.WithFields(log.Fields{ + "latestBeefyBlock": state.LatestBeefyBlock, + "currentValidatorSetID": state.CurrentValidatorSetID, + "nextValidatorSetID": state.NextValidatorSetID, + "blockNumberToSync": task.SignedCommitment.Commitment.BlockNumber, + }).Info("Commitment outdated, just ignore") + return nil + } + if task.SignedCommitment.Commitment.ValidatorSetID > state.NextValidatorSetID { + log.WithFields(log.Fields{ + "latestBeefyBlock": state.LatestBeefyBlock, + "currentValidatorSetID": state.CurrentValidatorSetID, + "nextValidatorSetID": state.NextValidatorSetID, + "validatorSetIDToSync": task.SignedCommitment.Commitment.ValidatorSetID, + }).Warn("Task unexpected, wait for mandatory updates to catch up first") + return nil + } + + // Submit the task + if task.SignedCommitment.Commitment.ValidatorSetID == state.CurrentValidatorSetID { + task.ValidatorsRoot = state.CurrentValidatorSetRoot + } else { + task.ValidatorsRoot = state.NextValidatorSetRoot + } + err = relay.ethereumWriter.submit(ctx, task) + if err != nil { + return fmt.Errorf("fail to submit beefy update: %w", err) + } + + updatedState, err := relay.ethereumWriter.queryBeefyClientState(ctx) + if err != nil { + return fmt.Errorf("query beefy client state: %w", err) + } + log.WithFields(log.Fields{ + "latestBeefyBlock": updatedState.LatestBeefyBlock, + "currentValidatorSetID": updatedState.CurrentValidatorSetID, + "nextValidatorSetID": updatedState.NextValidatorSetID, + }).Info("Sync beefy update success") + return nil } diff --git a/relayer/relays/beefy/parameters.go b/relayer/relays/beefy/parameters.go index 54a34c2c6a..03af798f69 100644 --- a/relayer/relays/beefy/parameters.go +++ b/relayer/relays/beefy/parameters.go @@ -164,22 +164,19 @@ func (r *Request) MakeSubmitFinalParams(validatorIndices []uint64, initialBitfie var merkleProofItems [][32]byte proofOrder := new(big.Int) - - if r.IsHandover { - inputLeaf = contracts.BeefyClientMMRLeaf{ - Version: uint8(r.Proof.Leaf.Version), - ParentNumber: uint32(r.Proof.Leaf.ParentNumberAndHash.ParentNumber), - ParentHash: r.Proof.Leaf.ParentNumberAndHash.Hash, - ParachainHeadsRoot: r.Proof.Leaf.ParachainHeads, - NextAuthoritySetID: uint64(r.Proof.Leaf.BeefyNextAuthoritySet.ID), - NextAuthoritySetLen: uint32(r.Proof.Leaf.BeefyNextAuthoritySet.Len), - NextAuthoritySetRoot: r.Proof.Leaf.BeefyNextAuthoritySet.Root, - } - for _, mmrProofItem := range r.Proof.MerkleProofItems { - merkleProofItems = append(merkleProofItems, mmrProofItem) - } - proofOrder = proofOrder.SetUint64(r.Proof.MerkleProofOrder) - } + inputLeaf = contracts.BeefyClientMMRLeaf{ + Version: uint8(r.Proof.Leaf.Version), + ParentNumber: uint32(r.Proof.Leaf.ParentNumberAndHash.ParentNumber), + ParentHash: r.Proof.Leaf.ParentNumberAndHash.Hash, + ParachainHeadsRoot: r.Proof.Leaf.ParachainHeads, + NextAuthoritySetID: uint64(r.Proof.Leaf.BeefyNextAuthoritySet.ID), + NextAuthoritySetLen: uint32(r.Proof.Leaf.BeefyNextAuthoritySet.Len), + NextAuthoritySetRoot: r.Proof.Leaf.BeefyNextAuthoritySet.Root, + } + for _, mmrProofItem := range r.Proof.MerkleProofItems { + merkleProofItems = append(merkleProofItems, mmrProofItem) + } + proofOrder = proofOrder.SetUint64(r.Proof.MerkleProofOrder) msg := FinalRequestParams{ Commitment: commitment, diff --git a/relayer/relays/beefy/polkadot-listener.go b/relayer/relays/beefy/polkadot-listener.go index 96cc45fc49..24894af1e8 100644 --- a/relayer/relays/beefy/polkadot-listener.go +++ b/relayer/relays/beefy/polkadot-listener.go @@ -3,20 +3,19 @@ package beefy import ( "context" "fmt" + "time" + log "github.com/sirupsen/logrus" "github.com/snowfork/go-substrate-rpc-client/v4/types" "golang.org/x/sync/errgroup" "github.com/snowfork/snowbridge/relayer/chain/relaychain" "github.com/snowfork/snowbridge/relayer/substrate" - - log "github.com/sirupsen/logrus" ) type PolkadotListener struct { - config *SourceConfig - conn *relaychain.Connection - beefyAuthoritiesKey types.StorageKey + config *SourceConfig + conn *relaychain.Connection } func NewPolkadotListener( @@ -35,13 +34,7 @@ func (li *PolkadotListener) Start( currentBeefyBlock uint64, currentValidatorSetID uint64, ) (<-chan Request, error) { - storageKey, err := types.CreateStorageKey(li.conn.Metadata(), "Beefy", "Authorities", nil, nil) - if err != nil { - return nil, fmt.Errorf("create storage key: %w", err) - } - li.beefyAuthoritiesKey = storageKey - - requests := make(chan Request) + requests := make(chan Request, 1) eg.Go(func() error { defer close(requests) @@ -61,11 +54,10 @@ func (li *PolkadotListener) scanCommitments( currentValidatorSet uint64, requests chan<- Request, ) error { - in, err := ScanSafeCommitments(ctx, li.conn.Metadata(), li.conn.API(), currentBeefyBlock+1) + in, err := ScanCommitments(ctx, li.conn.Metadata(), li.conn.API(), currentBeefyBlock+1) if err != nil { - return fmt.Errorf("scan commitments: %w", err) + return fmt.Errorf("scan provable commitments: %w", err) } - lastSyncedBeefyBlock := currentBeefyBlock for { select { @@ -81,77 +73,44 @@ func (li *PolkadotListener) scanCommitments( committedBeefyBlock := uint64(result.SignedCommitment.Commitment.BlockNumber) validatorSetID := result.SignedCommitment.Commitment.ValidatorSetID - nextValidatorSetID := uint64(result.MMRProof.Leaf.BeefyNextAuthoritySet.ID) - - if validatorSetID != currentValidatorSet && validatorSetID != currentValidatorSet+1 { - return fmt.Errorf("commitment has unexpected validatorSetID: blockNumber=%v validatorSetID=%v expectedValidatorSetID=%v", - committedBeefyBlock, - validatorSetID, - currentValidatorSet, - ) - } - - logEntry := log.WithFields(log.Fields{ - "commitment": log.Fields{ - "blockNumber": committedBeefyBlock, - "validatorSetID": validatorSetID, - "nextValidatorSetID": nextValidatorSetID, - }, - "validatorSetID": currentValidatorSet, - "IsHandover": validatorSetID == currentValidatorSet+1, - "lastSyncedBeefyBlock": lastSyncedBeefyBlock, - }) + nextValidatorSetID := uint64(result.Proof.Leaf.BeefyNextAuthoritySet.ID) validators, err := li.queryBeefyAuthorities(result.BlockHash) if err != nil { return fmt.Errorf("fetch beefy authorities at block %v: %w", result.BlockHash, err) } + task := Request{ Validators: validators, SignedCommitment: result.SignedCommitment, - Proof: result.MMRProof, + Proof: result.Proof, } - if validatorSetID == currentValidatorSet+1 && validatorSetID == nextValidatorSetID-1 { - task.IsHandover = true - select { - case <-ctx.Done(): - return ctx.Err() - case requests <- task: - logEntry.Info("New commitment with handover added to channel") - currentValidatorSet++ - lastSyncedBeefyBlock = committedBeefyBlock - } - } else if validatorSetID == currentValidatorSet { - if result.Depth > li.config.FastForwardDepth { - logEntry.Warn("Discarded commitment with depth not fast forward") - continue - } - if committedBeefyBlock < lastSyncedBeefyBlock+li.config.UpdatePeriod { - logEntry.Info("Discarded commitment with sampling") - continue - } + log.WithFields(log.Fields{ + "commitment": log.Fields{ + "blockNumber": committedBeefyBlock, + "validatorSetID": validatorSetID, + "nextValidatorSetID": nextValidatorSetID, + }, + "validatorSetID": currentValidatorSet, + }).Info("Sending BEEFY commitment to ethereum writer") - // drop task if it can't be processed immediately - select { - case <-ctx.Done(): - return ctx.Err() - case requests <- task: - lastSyncedBeefyBlock = committedBeefyBlock - logEntry.Info("New commitment added to channel") - default: - logEntry.Warn("Discarded commitment fail adding to channel") - } - } else { - logEntry.Warn("Discarded invalid commitment") + select { + case <-ctx.Done(): + return ctx.Err() + case requests <- task: } } } } func (li *PolkadotListener) queryBeefyAuthorities(blockHash types.Hash) ([]substrate.Authority, error) { + storageKey, err := types.CreateStorageKey(li.conn.Metadata(), "Beefy", "Authorities", nil, nil) + if err != nil { + return nil, fmt.Errorf("create storage key: %w", err) + } var authorities []substrate.Authority - ok, err := li.conn.API().RPC.State.GetStorage(li.beefyAuthoritiesKey, &authorities, blockHash) + ok, err := li.conn.API().RPC.State.GetStorage(storageKey, &authorities, blockHash) if err != nil { return nil, err } @@ -162,16 +121,96 @@ func (li *PolkadotListener) queryBeefyAuthorities(blockHash types.Hash) ([]subst return authorities, nil } -func (li *PolkadotListener) queryBeefyNextAuthoritySet(blockHash types.Hash) (types.BeefyNextAuthoritySet, error) { - var nextAuthoritySet types.BeefyNextAuthoritySet - storageKey, err := types.CreateStorageKey(li.conn.Metadata(), "MmrLeaf", "BeefyNextAuthorities", nil, nil) - ok, err := li.conn.API().RPC.State.GetStorage(storageKey, &nextAuthoritySet, blockHash) +func (li *PolkadotListener) generateBeefyUpdate(relayBlockNumber uint64) (Request, error) { + api := li.conn.API() + meta := li.conn.Metadata() + var request Request + beefyBlockHash, err := li.findNextBeefyBlock(relayBlockNumber) if err != nil { - return nextAuthoritySet, err + return request, fmt.Errorf("find match beefy block: %w", err) } - if !ok { - return nextAuthoritySet, fmt.Errorf("beefy nextAuthoritySet not found") + + commitment, proof, err := fetchCommitmentAndProof(meta, api, beefyBlockHash) + if err != nil { + return request, fmt.Errorf("fetch commitment and proof: %w", err) } - return nextAuthoritySet, nil + committedBeefyBlockNumber := uint64(commitment.Commitment.BlockNumber) + committedBeefyBlockHash, err := api.RPC.Chain.GetBlockHash(uint64(committedBeefyBlockNumber)) + + validators, err := li.queryBeefyAuthorities(committedBeefyBlockHash) + if err != nil { + return request, fmt.Errorf("fetch beefy authorities at block %v: %w", committedBeefyBlockHash, err) + } + request = Request{ + Validators: validators, + SignedCommitment: *commitment, + Proof: *proof, + } + + return request, nil +} + +func (li *PolkadotListener) findNextBeefyBlock(blockNumber uint64) (types.Hash, error) { + api := li.conn.API() + var nextBeefyBlockHash, finalizedBeefyBlockHash types.Hash + var err error + nextBeefyBlockNumber := blockNumber + for { + finalizedBeefyBlockHash, err = api.RPC.Beefy.GetFinalizedHead() + if err != nil { + return nextBeefyBlockHash, fmt.Errorf("fetch beefy finalized head: %w", err) + } + finalizedBeefyBlockHeader, err := api.RPC.Chain.GetHeader(finalizedBeefyBlockHash) + if err != nil { + return nextBeefyBlockHash, fmt.Errorf("fetch block header: %w", err) + } + latestBeefyBlockNumber := uint64(finalizedBeefyBlockHeader.Number) + if latestBeefyBlockNumber <= nextBeefyBlockNumber { + // The relay block not finalized yet, just wait and retry + time.Sleep(6 * time.Second) + continue + } else if latestBeefyBlockNumber <= nextBeefyBlockNumber+600 { + // The relay block has been finalized not long ago(1 hour), just return the finalized block + nextBeefyBlockHash = finalizedBeefyBlockHash + break + } else { + // The relay block has been finalized for a long time, in this case return the next block + // which contains a beefy justification + for { + if nextBeefyBlockNumber == latestBeefyBlockNumber { + nextBeefyBlockHash = finalizedBeefyBlockHash + break + } + nextBeefyBlockHash, err = api.RPC.Chain.GetBlockHash(nextBeefyBlockNumber) + if err != nil { + return nextBeefyBlockHash, fmt.Errorf("fetch block hash: %w", err) + } + block, err := api.RPC.Chain.GetBlock(nextBeefyBlockHash) + if err != nil { + return nextBeefyBlockHash, fmt.Errorf("fetch block: %w", err) + } + + var commitment *types.SignedCommitment + for j := range block.Justifications { + sc := types.OptionalSignedCommitment{} + if block.Justifications[j].EngineID() == "BEEF" { + err := types.DecodeFromBytes(block.Justifications[j].Payload(), &sc) + if err != nil { + return nextBeefyBlockHash, fmt.Errorf("decode BEEFY signed commitment: %w", err) + } + ok, value := sc.Unwrap() + if ok { + commitment = &value + } + } + } + if commitment != nil { + return nextBeefyBlockHash, nil + } + nextBeefyBlockNumber++ + } + } + } + return nextBeefyBlockHash, nil } diff --git a/relayer/relays/beefy/scanner.go b/relayer/relays/beefy/scanner.go index 16cce67a3d..d6f08352f5 100644 --- a/relayer/relays/beefy/scanner.go +++ b/relayer/relays/beefy/scanner.go @@ -5,7 +5,6 @@ import ( "fmt" "time" - log "github.com/sirupsen/logrus" gsrpc "github.com/snowfork/go-substrate-rpc-client/v4" "github.com/snowfork/go-substrate-rpc-client/v4/types" "github.com/snowfork/snowbridge/relayer/crypto/keccak" @@ -15,20 +14,19 @@ import ( type ScanBlocksResult struct { BlockNumber uint64 BlockHash types.Hash - Depth uint64 Error error } -func ScanBlocks(ctx context.Context, api *gsrpc.SubstrateAPI, startBlock uint64) (chan ScanBlocksResult, error) { +func ScanBlocks(ctx context.Context, meta *types.Metadata, api *gsrpc.SubstrateAPI, startBlock uint64) (chan ScanBlocksResult, error) { results := make(chan ScanBlocksResult) - go scanBlocks(ctx, api, startBlock, results) + go scanBlocks(ctx, meta, api, startBlock, results) return results, nil } -func scanBlocks(ctx context.Context, api *gsrpc.SubstrateAPI, startBlock uint64, out chan<- ScanBlocksResult) { +func scanBlocks(ctx context.Context, meta *types.Metadata, api *gsrpc.SubstrateAPI, startBlock uint64, out chan<- ScanBlocksResult) { defer close(out) - sendError := func(err error) { + emitError := func(err error) { select { case <-ctx.Done(): return @@ -36,20 +34,47 @@ func scanBlocks(ctx context.Context, api *gsrpc.SubstrateAPI, startBlock uint64, } } - current := startBlock - for { + fetchFinalizedBeefyHeader := func() (*types.Header, error) { finalizedHash, err := api.RPC.Beefy.GetFinalizedHead() if err != nil { - sendError(fmt.Errorf("fetch finalized head: %w", err)) - return + return nil, fmt.Errorf("fetch finalized head: %w", err) } finalizedHeader, err := api.RPC.Chain.GetHeader(finalizedHash) if err != nil { - sendError(fmt.Errorf("fetch header for finalised head %v: %w", finalizedHash.Hex(), err)) - return + return nil, fmt.Errorf("fetch header for finalised head %v: %w", finalizedHash.Hex(), err) } + return finalizedHeader, nil + } + + sessionCurrentIndexKey, err := types.CreateStorageKey(meta, "Session", "CurrentIndex", nil, nil) + if err != nil { + emitError(fmt.Errorf("create storage key: %w", err)) + return + } + + blockHash, err := api.RPC.Chain.GetBlockHash(max(startBlock-1, 0)) + if err != nil { + emitError(fmt.Errorf("fetch block hash: %w", err)) + return + } + + // Get session index of block before start block + var currentSessionIndex uint32 + _, err = api.RPC.State.GetStorage(sessionCurrentIndexKey, ¤tSessionIndex, blockHash) + if err != nil { + emitError(fmt.Errorf("fetch session index: %w", err)) + return + } + + finalizedHeader, err := fetchFinalizedBeefyHeader() + if err != nil { + emitError(err) + return + } + current := startBlock + for { finalizedBlockNumber := uint64(finalizedHeader.Number) if current > finalizedBlockNumber { select { @@ -57,19 +82,42 @@ func scanBlocks(ctx context.Context, api *gsrpc.SubstrateAPI, startBlock uint64, return case <-time.After(3 * time.Second): } + finalizedHeader, err = fetchFinalizedBeefyHeader() + if err != nil { + emitError(err) + return + } continue } + if current > uint64(finalizedHeader.Number) { + return + } + blockHash, err := api.RPC.Chain.GetBlockHash(current) if err != nil { - sendError(fmt.Errorf("fetch block hash: %w", err)) + emitError(fmt.Errorf("fetch block hash: %w", err)) return } + var sessionIndex uint32 + _, err = api.RPC.State.GetStorage(sessionCurrentIndexKey, &sessionIndex, blockHash) + if err != nil { + emitError(fmt.Errorf("fetch session index: %w", err)) + return + } + + if sessionIndex > currentSessionIndex { + currentSessionIndex = sessionIndex + } else { + current++ + continue + } + select { case <-ctx.Done(): return - case out <- ScanBlocksResult{BlockNumber: current, BlockHash: blockHash, Depth: finalizedBlockNumber - current}: + case out <- ScanBlocksResult{BlockNumber: current, BlockHash: blockHash}: } current++ @@ -78,22 +126,21 @@ func scanBlocks(ctx context.Context, api *gsrpc.SubstrateAPI, startBlock uint64, type ScanCommitmentsResult struct { SignedCommitment types.SignedCommitment - BlockNumber uint64 + Proof merkle.SimplifiedMMRProof BlockHash types.Hash - Depth uint64 Error error } -func ScanCommitments(ctx context.Context, api *gsrpc.SubstrateAPI, startBlock uint64) (<-chan ScanCommitmentsResult, error) { +func ScanCommitments(ctx context.Context, meta *types.Metadata, api *gsrpc.SubstrateAPI, startBlock uint64) (<-chan ScanCommitmentsResult, error) { out := make(chan ScanCommitmentsResult) - go scanCommitments(ctx, api, startBlock, out) + go scanCommitments(ctx, meta, api, startBlock, out) return out, nil } -func scanCommitments(ctx context.Context, api *gsrpc.SubstrateAPI, startBlock uint64, out chan<- ScanCommitmentsResult) { +func scanCommitments(ctx context.Context, meta *types.Metadata, api *gsrpc.SubstrateAPI, startBlock uint64, out chan<- ScanCommitmentsResult) { defer close(out) - sendError := func(err error) { + emitError := func(err error) { select { case <-ctx.Done(): return @@ -101,17 +148,16 @@ func scanCommitments(ctx context.Context, api *gsrpc.SubstrateAPI, startBlock ui } } - in, err := ScanBlocks(ctx, api, startBlock) + in, err := ScanBlocks(ctx, meta, api, startBlock) if err != nil { - sendError(err) + emitError(err) return } for { select { case <-ctx.Done(): - out <- ScanCommitmentsResult{Error: ctx.Err()} - close(out) + emitError(err) return case result, ok := <-in: if !ok { @@ -119,130 +165,21 @@ func scanCommitments(ctx context.Context, api *gsrpc.SubstrateAPI, startBlock ui } if result.Error != nil { - sendError(result.Error) - return - } - - block, err := api.RPC.Chain.GetBlock(result.BlockHash) - if err != nil { - sendError(fmt.Errorf("fetch block: %w", err)) - return - } - log.WithFields(log.Fields{ - "blockNumber": result.BlockNumber, - "depth": result.Depth, - }).Info("fetch block") - - var commitment *types.SignedCommitment - for j := range block.Justifications { - sc := types.OptionalSignedCommitment{} - // Filter justification by EngineID - // https://github.com/paritytech/substrate/blob/55c64bcc2af5a6e5fc3eb245e638379ebe18a58d/primitives/beefy/src/lib.rs#L114 - if block.Justifications[j].EngineID() == "BEEF" { - // Decode as SignedCommitment - // https://github.com/paritytech/substrate/blob/bcee526a9b73d2df9d5dea0f1a17677618d70b8e/primitives/beefy/src/commitment.rs#L89 - err := types.DecodeFromBytes(block.Justifications[j].Payload(), &sc) - if err != nil { - sendError(fmt.Errorf("decode BEEFY signed commitment: %w", err)) - return - } - ok, value := sc.Unwrap() - if ok { - commitment = &value - } - } - } - - if commitment == nil { - continue - } - - select { - case <-ctx.Done(): - return - case out <- ScanCommitmentsResult{BlockNumber: result.BlockNumber, BlockHash: result.BlockHash, SignedCommitment: *commitment, Depth: result.Depth}: - } - } - } -} - -type ScanSafeCommitmentsResult struct { - SignedCommitment types.SignedCommitment - MMRProof merkle.SimplifiedMMRProof - BlockHash types.Hash - Depth uint64 - Error error -} - -func ScanSafeCommitments(ctx context.Context, meta *types.Metadata, api *gsrpc.SubstrateAPI, startBlock uint64) (<-chan ScanSafeCommitmentsResult, error) { - out := make(chan ScanSafeCommitmentsResult) - go scanSafeCommitments(ctx, meta, api, startBlock, out) - return out, nil -} - -func scanSafeCommitments(ctx context.Context, meta *types.Metadata, api *gsrpc.SubstrateAPI, startBlock uint64, out chan<- ScanSafeCommitmentsResult) { - defer close(out) - - sendError := func(err error) { - select { - case <-ctx.Done(): - return - case out <- ScanSafeCommitmentsResult{Error: err}: - } - } - - in, err := ScanCommitments(ctx, api, startBlock) - if err != nil { - sendError(err) - return - } - - for { - select { - case <-ctx.Done(): - return - case result, ok := <-in: - if !ok { + emitError(result.Error) return } - if result.Error != nil { - sendError(result.Error) - return - } - log.WithFields(log.Fields{ - "blockNumber": result.BlockNumber, - "depth": result.Depth, - "commitment": result.SignedCommitment.Commitment, - }).Info("fetch commitment") - - blockNumber := result.SignedCommitment.Commitment.BlockNumber - blockHash, err := api.RPC.Chain.GetBlockHash(uint64(blockNumber)) + commitment, proof, err := fetchCommitmentAndProof(meta, api, result.BlockHash) if err != nil { - sendError(fmt.Errorf("fetch block hash: %w", err)) - return - } - proofIsValid, proof, err := makeProof(meta, api, blockNumber, blockHash) - if err != nil { - sendError(fmt.Errorf("proof generation for block %v at hash %v: %w", blockNumber, blockHash.Hex(), err)) + emitError(fmt.Errorf("fetch commitment and proof: %w", err)) return } - if !proofIsValid { - log.WithFields(log.Fields{ - "parentNumber": blockNumber, - "beefyBlockHash": blockHash, - "validatorSetID": result.SignedCommitment.Commitment.ValidatorSetID, - }).Info("Proof for leaf is invalid") - continue - } - select { case <-ctx.Done(): return - case out <- ScanSafeCommitmentsResult{result.SignedCommitment, proof, blockHash, result.Depth, nil}: + case out <- ScanCommitmentsResult{BlockHash: result.BlockHash, SignedCommitment: *commitment, Proof: *proof}: } - } } } @@ -302,3 +239,41 @@ func verifyProof(meta *types.Metadata, api *gsrpc.SubstrateAPI, proof merkle.Sim return actualRoot == expectedRoot, nil } + +func fetchCommitmentAndProof(meta *types.Metadata, api *gsrpc.SubstrateAPI, beefyBlockHash types.Hash) (*types.SignedCommitment, *merkle.SimplifiedMMRProof, error) { + beefyHeader, err := api.RPC.Chain.GetHeader(beefyBlockHash) + if err != nil { + return nil, nil, fmt.Errorf("fetch header: %w", err) + } + beefyBlock, err := api.RPC.Chain.GetBlock(beefyBlockHash) + if err != nil { + return nil, nil, fmt.Errorf("fetch block: %w", err) + } + + var commitment *types.SignedCommitment + for j := range beefyBlock.Justifications { + sc := types.OptionalSignedCommitment{} + if beefyBlock.Justifications[j].EngineID() == "BEEF" { + err := types.DecodeFromBytes(beefyBlock.Justifications[j].Payload(), &sc) + if err != nil { + return nil, nil, fmt.Errorf("decode BEEFY signed commitment: %w", err) + } + ok, value := sc.Unwrap() + if ok { + commitment = &value + } + } + } + if commitment == nil { + return nil, nil, fmt.Errorf("beefy block without a valid commitment") + } + + proofIsValid, proof, err := makeProof(meta, api, uint32(beefyHeader.Number), beefyBlockHash) + if err != nil { + return nil, nil, fmt.Errorf("proof generation for block %v at hash %v: %w", beefyHeader.Number, beefyBlockHash.Hex(), err) + } + if !proofIsValid { + return nil, nil, fmt.Errorf("Proof for leaf is invalid for block %v at hash %v: %w", beefyHeader.Number, beefyBlockHash.Hex(), err) + } + return commitment, &proof, nil +} diff --git a/relayer/relays/beefy/task.go b/relayer/relays/beefy/task.go index 7bd8733c55..e35862f40f 100644 --- a/relayer/relays/beefy/task.go +++ b/relayer/relays/beefy/task.go @@ -6,10 +6,19 @@ import ( "github.com/snowfork/snowbridge/relayer/substrate" ) +type BeefyAuthoritySet struct { + // ID + ID types.U64 + // Number of validators in the set. + Len types.U32 + // Merkle Root Hash build from BEEFY uncompressed AuthorityIds. + Root types.H256 +} + type Request struct { + // Validators that signed this commitment Validators []substrate.Authority ValidatorsRoot [32]byte SignedCommitment types.SignedCommitment Proof merkle.SimplifiedMMRProof - IsHandover bool }