diff --git a/CHANGELOG.md b/CHANGELOG.md index c1fbb17f..11be050d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * [#149](https://github.com/babylonlabs-io/finality-provider/pull/149) Remove update of config after `fpd keys add` * [#148](https://github.com/babylonlabs-io/finality-provider/pull/148) Allow command `eotsd keys add` to use empty HD path to derive new key and use master private key. +* [#153](https://github.com/babylonlabs-io/finality-provider/pull/153) Add `unsafe-commit-pubrand` command * [#154](https://github.com/babylonlabs-io/finality-provider/pull/154) Use sign schnorr instead of getting private key from EOTS manager * [#167](https://github.com/babylonlabs-io/finality-provider/pull/167) Remove last processed height diff --git a/finality-provider/cmd/fpd/daemon/commit_pr.go b/finality-provider/cmd/fpd/daemon/commit_pr.go new file mode 100644 index 00000000..f39eb4f8 --- /dev/null +++ b/finality-provider/cmd/fpd/daemon/commit_pr.go @@ -0,0 +1,103 @@ +package daemon + +import ( + "fmt" + "math" + "path/filepath" + "strconv" + + bbntypes "github.com/babylonlabs-io/babylon/types" + fpcc "github.com/babylonlabs-io/finality-provider/clientcontroller" + eotsclient "github.com/babylonlabs-io/finality-provider/eotsmanager/client" + fpcfg "github.com/babylonlabs-io/finality-provider/finality-provider/config" + "github.com/babylonlabs-io/finality-provider/finality-provider/service" + "github.com/babylonlabs-io/finality-provider/finality-provider/store" + "github.com/babylonlabs-io/finality-provider/log" + "github.com/babylonlabs-io/finality-provider/metrics" + "github.com/babylonlabs-io/finality-provider/util" + "github.com/cosmos/cosmos-sdk/client" + "github.com/spf13/cobra" +) + +// CommandCommitPubRand returns the commit-pubrand command +func CommandCommitPubRand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "unsafe-commit-pubrand [fp-eots-pk-hex] [target-height]", + Aliases: []string{"unsafe-cpr"}, + Short: "[UNSAFE] Manually trigger public randomness commitment for a finality provider", + Long: `[UNSAFE] Manually trigger public randomness commitment for a finality provider. +WARNING: this can drain the finality provider's balance if the target height is too high.`, + Example: `fpd unsafe-commit-pubrand --home /home/user/.fpd [fp-eots-pk-hex] [target-height]`, + Args: cobra.ExactArgs(2), + RunE: runCommandCommitPubRand, + } + cmd.Flags().Uint64("start-height", math.MaxUint64, "The block height to start committing pubrand from (optional)") + return cmd +} + +func runCommandCommitPubRand(cmd *cobra.Command, args []string) error { + fpPk, err := bbntypes.NewBIP340PubKeyFromHex(args[0]) + if err != nil { + return err + } + targetHeight, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return err + } + startHeight, err := cmd.Flags().GetUint64("start-height") + if err != nil { + return err + } + + // Get homePath from context like in start.go + clientCtx := client.GetClientContextFromCmd(cmd) + homePath, err := filepath.Abs(clientCtx.HomeDir) + if err != nil { + return err + } + homePath = util.CleanAndExpandPath(homePath) + + cfg, err := fpcfg.LoadConfig(homePath) + if err != nil { + return fmt.Errorf("failed to load configuration: %w", err) + } + + logger, err := log.NewRootLoggerWithFile(fpcfg.LogFile(homePath), cfg.LogLevel) + if err != nil { + return fmt.Errorf("failed to initialize the logger: %w", err) + } + + db, err := cfg.DatabaseConfig.GetDBBackend() + if err != nil { + return fmt.Errorf("failed to create db backend: %w", err) + } + + fpStore, err := store.NewFinalityProviderStore(db) + if err != nil { + return fmt.Errorf("failed to initiate finality provider store: %w", err) + } + pubRandStore, err := store.NewPubRandProofStore(db) + if err != nil { + return fmt.Errorf("failed to initiate public randomness store: %w", err) + } + cc, err := fpcc.NewClientController(cfg.ChainType, cfg.BabylonConfig, &cfg.BTCNetParams, logger) + if err != nil { + return fmt.Errorf("failed to create rpc client for the Babylon chain: %w", err) + } + em, err := eotsclient.NewEOTSManagerGRpcClient(cfg.EOTSManagerAddress) + if err != nil { + return fmt.Errorf("failed to create EOTS manager client: %w", err) + } + + fp, err := service.NewFinalityProviderInstance( + fpPk, cfg, fpStore, pubRandStore, cc, em, metrics.NewFpMetrics(), "", + make(chan<- *service.CriticalError), logger) + if err != nil { + return fmt.Errorf("failed to create finality-provider %s instance: %w", fpPk.MarshalHex(), err) + } + + if startHeight == math.MaxUint64 { + return fp.TestCommitPubRand(targetHeight) + } + return fp.TestCommitPubRandWithStartHeight(startHeight, targetHeight) +} diff --git a/finality-provider/cmd/fpd/daemon/daemon_commands.go b/finality-provider/cmd/fpd/daemon/daemon_commands.go index 28e897d4..69e1149b 100644 --- a/finality-provider/cmd/fpd/daemon/daemon_commands.go +++ b/finality-provider/cmd/fpd/daemon/daemon_commands.go @@ -393,10 +393,11 @@ func runCommandRegisterFP(cmd *cobra.Command, args []string) error { // CommandAddFinalitySig returns the add-finality-sig command by connecting to the fpd daemon. func CommandAddFinalitySig() *cobra.Command { var cmd = &cobra.Command{ - Use: "add-finality-sig [fp-eots-pk-hex] [block-height]", - Aliases: []string{"afs"}, - Short: "Send a finality signature to the consumer chain. This command should only be used for presentation/testing purposes", - Example: fmt.Sprintf(`fpd add-finality-sig --daemon-address %s`, defaultFpdDaemonAddress), + Use: "unsafe-add-finality-sig [fp-eots-pk-hex] [block-height]", + Aliases: []string{"unsafe-afs"}, + Short: "[UNSAFE] Send a finality signature to the consumer chain.", + Long: "[UNSAFE] Send a finality signature to the consumer chain. This command should only be used for presentation/testing purposes", + Example: fmt.Sprintf(`fpd unsafe-add-finality-sig [fp-eots-pk-hex] [block-height] --daemon-address %s`, defaultFpdDaemonAddress), Args: cobra.ExactArgs(2), RunE: runCommandAddFinalitySig, } diff --git a/finality-provider/cmd/fpd/main.go b/finality-provider/cmd/fpd/main.go index 6c9ca981..fcebe405 100644 --- a/finality-provider/cmd/fpd/main.go +++ b/finality-provider/cmd/fpd/main.go @@ -35,6 +35,7 @@ func main() { daemon.CommandInfoFP(), daemon.CommandRegisterFP(), daemon.CommandAddFinalitySig(), daemon.CommandExportFP(), daemon.CommandTxs(), daemon.CommandUnjailFP(), daemon.CommandEditFinalityDescription(), daemon.CommandVersion(), + daemon.CommandCommitPubRand(), ) if err := cmd.Execute(); err != nil { diff --git a/finality-provider/service/fp_instance.go b/finality-provider/service/fp_instance.go index 5a2b3e02..a4ec4fa6 100644 --- a/finality-provider/service/fp_instance.go +++ b/finality-provider/service/fp_instance.go @@ -74,6 +74,22 @@ func NewFinalityProviderInstance( return nil, fmt.Errorf("the finality provider instance cannot be initiated with status %s", sfp.Status.String()) } + return newFinalityProviderInstanceFromStore(sfp, cfg, s, prStore, cc, em, metrics, passphrase, errChan, logger) +} + +// Helper function to create FinalityProviderInstance from store data +func newFinalityProviderInstanceFromStore( + sfp *store.StoredFinalityProvider, + cfg *fpcfg.Config, + s *store.FinalityProviderStore, + prStore *store.PubRandProofStore, + cc clientcontroller.ClientController, + em eotsmanager.EOTSManager, + metrics *metrics.FpMetrics, + passphrase string, + errChan chan<- *CriticalError, + logger *zap.Logger, +) (*FinalityProviderInstance, error) { return &FinalityProviderInstance{ btcPk: bbntypes.NewBIP340PubKeyFromBTCPK(sfp.BtcPk), fpState: newFpState(sfp, s), @@ -456,6 +472,9 @@ func (fp *FinalityProviderInstance) retryCommitPubRandUntilBlockFinalized(target // CommitPubRand generates a list of Schnorr rand pairs, // commits the public randomness for the managed finality providers, // and save the randomness pair to DB +// Note: +// - if there is no pubrand committed before, it will start from the tipHeight +// - if the tipHeight is too large, it will only commit fp.cfg.NumPubRand pairs func (fp *FinalityProviderInstance) CommitPubRand(tipHeight uint64) (*types.TxResponse, error) { lastCommittedHeight, err := fp.GetLastCommittedHeight() if err != nil { @@ -481,6 +500,11 @@ func (fp *FinalityProviderInstance) CommitPubRand(tipHeight uint64) (*types.TxRe return nil, nil } + return fp.commitPubRandPairs(startHeight) +} + +// it will commit fp.cfg.NumPubRand pairs of public randomness starting from startHeight +func (fp *FinalityProviderInstance) commitPubRandPairs(startHeight uint64) (*types.TxResponse, error) { activationBlkHeight, err := fp.cc.QueryFinalityActivationBlockHeight() if err != nil { return nil, err @@ -520,12 +544,86 @@ func (fp *FinalityProviderInstance) CommitPubRand(tipHeight uint64) (*types.TxRe // Update metrics fp.metrics.RecordFpRandomnessTime(fp.GetBtcPkHex()) - fp.metrics.RecordFpLastCommittedRandomnessHeight(fp.GetBtcPkHex(), lastCommittedHeight) + fp.metrics.RecordFpLastCommittedRandomnessHeight(fp.GetBtcPkHex(), startHeight+numPubRand-1) fp.metrics.AddToFpTotalCommittedRandomness(fp.GetBtcPkHex(), float64(len(pubRandList))) return res, nil } +// TestCommitPubRand is exposed for devops/testing purpose to allow manual committing public randomness in cases +// where FP is stuck due to lack of public randomness. +// +// Note: +// - this function is similar to `CommitPubRand` but should not be used in the main pubrand submission loop. +// - it will always start from the last committed height + 1 +// - if targetBlockHeight is too large, it will commit multiple fp.cfg.NumPubRand pairs in a loop until reaching the targetBlockHeight +func (fp *FinalityProviderInstance) TestCommitPubRand(targetBlockHeight uint64) error { + var startHeight, lastCommittedHeight uint64 + + lastCommittedHeight, err := fp.GetLastCommittedHeight() + if err != nil { + return err + } + + if lastCommittedHeight >= targetBlockHeight { + return fmt.Errorf( + "finality provider has already committed pubrand to target block height (pk: %s, target: %d, last committed: %d)", + fp.GetBtcPkHex(), + targetBlockHeight, + lastCommittedHeight, + ) + } + + if lastCommittedHeight == uint64(0) { + // Note: it can also be the case that the finality-provider has committed 1 pubrand before (but in practice, we + // will never set cfg.NumPubRand to 1. so we can safely assume it has never committed before) + startHeight = 0 + } else { + startHeight = lastCommittedHeight + 1 + } + + return fp.TestCommitPubRandWithStartHeight(startHeight, targetBlockHeight) +} + +// TestCommitPubRandWithStartHeight is exposed for devops/testing purpose to allow manual committing public randomness +// in cases where FP is stuck due to lack of public randomness. +func (fp *FinalityProviderInstance) TestCommitPubRandWithStartHeight(startHeight uint64, targetBlockHeight uint64) error { + if startHeight > targetBlockHeight { + return fmt.Errorf("start height should not be greater than target block height") + } + + var lastCommittedHeight uint64 + lastCommittedHeight, err := fp.GetLastCommittedHeight() + if err != nil { + return err + } + if lastCommittedHeight >= startHeight { + return fmt.Errorf( + "finality provider has already committed pubrand at the start height (pk: %s, startHeight: %d, lastCommittedHeight: %d)", + fp.GetBtcPkHex(), + startHeight, + lastCommittedHeight, + ) + } + + fp.logger.Info("Start committing pubrand from block height", zap.Uint64("start_height", startHeight)) + + // TODO: instead of sending multiple txs, a better way is to bundle all the commit messages into + // one like we do for batch finality signatures. see discussion https://bit.ly/3OmbjkN + for startHeight <= targetBlockHeight { + _, err = fp.commitPubRandPairs(startHeight) + if err != nil { + return err + } + lastCommittedHeight = startHeight + uint64(fp.cfg.NumPubRand) - 1 + startHeight = lastCommittedHeight + 1 + fp.logger.Info("Committed pubrand to block height", zap.Uint64("height", lastCommittedHeight)) + } + + // no error. success + return nil +} + // SubmitFinalitySignature builds and sends a finality signature over the given block to the consumer chain func (fp *FinalityProviderInstance) SubmitFinalitySignature(b *types.BlockInfo) (*types.TxResponse, error) { return fp.SubmitBatchFinalitySignatures([]*types.BlockInfo{b})