diff --git a/relayer/cmd/root.go b/relayer/cmd/root.go index 11f8b794e..85b18eb65 100644 --- a/relayer/cmd/root.go +++ b/relayer/cmd/root.go @@ -36,6 +36,7 @@ func init() { rootCmd.AddCommand(storeBeaconStateCmd()) rootCmd.AddCommand(importBeaconStateCmd()) rootCmd.AddCommand(listBeaconStateCmd()) + rootCmd.AddCommand(syncBeefyCommitmentCmd()) } func Execute() { diff --git a/relayer/cmd/sync_beefy_commitment.go b/relayer/cmd/sync_beefy_commitment.go new file mode 100644 index 000000000..444840e33 --- /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 2efeb9b3b..187c190f9 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 { @@ -295,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/main.go b/relayer/relays/beefy/main.go index af7a6b9fa..1222f88a7 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(ctx, 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/polkadot-listener.go b/relayer/relays/beefy/polkadot-listener.go index 3ef4675ba..9c55caf84 100644 --- a/relayer/relays/beefy/polkadot-listener.go +++ b/relayer/relays/beefy/polkadot-listener.go @@ -3,6 +3,7 @@ package beefy import ( "context" "fmt" + "time" log "github.com/sirupsen/logrus" "github.com/snowfork/go-substrate-rpc-client/v4/types" @@ -13,9 +14,8 @@ import ( ) type PolkadotListener struct { - config *SourceConfig - conn *relaychain.Connection - beefyAuthoritiesKey types.StorageKey + config *SourceConfig + conn *relaychain.Connection } func NewPolkadotListener( @@ -34,12 +34,6 @@ 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, 1) eg.Go(func() error { @@ -111,8 +105,12 @@ func (li *PolkadotListener) scanCommitments( } 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 } @@ -122,3 +120,97 @@ func (li *PolkadotListener) queryBeefyAuthorities(blockHash types.Hash) ([]subst return authorities, nil } + +func (li *PolkadotListener) generateBeefyUpdate(ctx context.Context, relayBlockNumber uint64) (Request, error) { + api := li.conn.API() + meta := li.conn.Metadata() + var request Request + beefyBlockHash, err := li.findNextBeefyBlock(relayBlockNumber) + if err != nil { + return request, fmt.Errorf("find match beefy block: %w", err) + } + + commitment, proof, err := fetchCommitmentAndProof(ctx, meta, api, beefyBlockHash) + if err != nil { + return request, fmt.Errorf("fetch commitment and proof: %w", err) + } + + 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 fa316fd55..6c84cc06b 100644 --- a/relayer/relays/beefy/scanner.go +++ b/relayer/relays/beefy/scanner.go @@ -169,59 +169,16 @@ func scanCommitments(ctx context.Context, meta *types.Metadata, api *gsrpc.Subst return } - block, err := api.RPC.Chain.GetBlock(result.BlockHash) + commitment, proof, err := fetchCommitmentAndProof(ctx, meta, api, result.BlockHash) if err != nil { - emitError(fmt.Errorf("fetch block: %w", err)) - return - } - - 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 { - emitError(fmt.Errorf("decode signed beefy commitment: %w", err)) - return - } - ok, value := sc.Unwrap() - if ok { - commitment = &value - } - } - } - - if commitment == nil { - emitError(fmt.Errorf("expected mandatory beefy justification in block")) - return - } - - blockNumber := commitment.Commitment.BlockNumber - blockHash, err := api.RPC.Chain.GetBlockHash(uint64(blockNumber)) - if err != nil { - emitError(fmt.Errorf("fetch block hash: %w", err)) - return - - } - proofIsValid, proof, err := makeProof(meta, api, blockNumber, blockHash) - if err != nil { - emitError(fmt.Errorf("proof generation for block %v at hash %v: %w", blockNumber, blockHash.Hex(), err)) - return - } - - if !proofIsValid { - emitError(fmt.Errorf("Leaf for parent block %v at hash %v is unprovable", blockNumber, blockHash.Hex())) + emitError(fmt.Errorf("fetch commitment and proof: %w", err)) return } select { case <-ctx.Done(): return - case out <- ScanCommitmentsResult{BlockHash: blockHash, SignedCommitment: *commitment, Proof: proof}: + case out <- ScanCommitmentsResult{BlockHash: result.BlockHash, SignedCommitment: *commitment, Proof: *proof}: } } } @@ -282,3 +239,41 @@ func verifyProof(meta *types.Metadata, api *gsrpc.SubstrateAPI, proof merkle.Sim return actualRoot == expectedRoot, nil } + +func fetchCommitmentAndProof(ctx context.Context, 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 +}