diff --git a/CHANGELOG.md b/CHANGELOG.md index ab2615ac..f3875394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * [#227](https://github.com/babylonlabs-io/finality-provider/pull/227) Fix FP submission loop * [#226](https://github.com/babylonlabs-io/finality-provider/pull/226) Update local fp before register * [#233](https://github.com/babylonlabs-io/finality-provider/pull/233) Refactor CommitPubRand +* [#234](https://github.com/babylonlabs-io/finality-provider/pull/234) eotsd ls command ## v0.13.1 diff --git a/docs/finality-provider-operation.md b/docs/finality-provider-operation.md index ef4cae8e..ea61e02e 100644 --- a/docs/finality-provider-operation.md +++ b/docs/finality-provider-operation.md @@ -35,6 +35,8 @@ gain an overall understanding of the finality provider. 2. [Withdrawing Rewards](#52-withdrawing-rewards) 3. [Jailing and Unjailing](#53-jailing-and-unjailing) 4. [Slashing](#54-slashing) + 5. [Prometheus Metrics](#55-prometheus-metrics) + 6. [Withdrawing Rewards](#56-withdrawing-rewards) ## 1. A note about Phase-1 Finality Providers @@ -493,7 +495,7 @@ saves the finality provider information in the database. fpd create-finality-provider \ --chain-id bbn-test-5 \ --eots-pk \ - --commission 0.05 \ + --commission-rate 0.05 \ --key-name finality-provider \ --moniker "MyFinalityProvider" \ --website "https://myfinalityprovider.com" \ @@ -598,7 +600,8 @@ When jailed, the following happens to a finality provider: - Delegator rewards stop To unjail a finality provider, you must complete the following steps: -1. Fix the underlying issue that caused jailing (e.g., ensure your node is properly synced and voting) +1. Fix the underlying issue that caused jailing (e.g., ensure your node is + properly synced and voting) 2. Wait for the jailing period to pass (defined by finality module parameters) 3. Send the unjail transaction to the Babylon chain using the following command: @@ -626,7 +629,7 @@ removal from the active set. > ⚠️ **Critical**: Slashing is irreversible and results in > permanent removal from the network. -### 5.5 Prometheus +### 5.5. Prometheus Metrics The finality provider exposes Prometheus metrics for monitoring your finality provider. The metrics endpoint is configurable in `fpd.conf`: @@ -657,3 +660,51 @@ For a complete list of available metrics, see: - Finality Provider metrics: [fp_collectors.go](../metrics/fp_collectors.go) - EOTS metrics: [eots_collectors.go](../metrics/eots_collectors.go) +### 5.6. Withdrawing Rewards + +When you are ready to withdraw your rewards, you have the option first to set +the address to withdraw your rewards to. + +```shell +fpd set-withdraw-addr --from +--keyring-backend test --home --fees +``` + +Parameters: +- ``: The new address to withdraw rewards to. +- `--from`: The finality provider's registered Babylon address. +- `--keyring-backend`: The keyring backend to use. +- `--home`: The home directory for the finality provider. +- `--fees`: The fees to pay for the transaction, should be over `400ubbn`. + These fees are paid from the account specified in `--from`. + +This command should ask you to +`confirm transaction before signing and broadcasting [y/N]:` and output the +transaction hash. + +Once you have set the address, you can withdraw your rewards by running the +following command: + +```shell +fpd withdraw-reward --from +--keyring-backend test --home --fees +``` + +Parameters: +- ``: The type of reward to withdraw (one of `finality_provider`, + `btc_delegation`) +- `--from`: The finality provider's registered Babylon address. +- `--keyring-backend`: The keyring backend to use. +- `--home`: The home directory for the finality provider. +- `--fees`: The fees to pay for the transaction, should be over `400ubbn`. + These fees are paid from the account specified in `--from`. + +Again, this command should ask you to +`confirm transaction before signing and broadcasting [y/N]:` and output the +transaction hash. + +This will withdraw **ALL** accumulated rewards to the address you set in the +`set-withdraw-addr` command if you set one. If no withdrawal address was set, +the rewards will be withdrawn to your finality provider address. + +Congratulations! You have successfully set up and operated a finality provider. diff --git a/eotsmanager/cmd/eotsd/daemon/keys.go b/eotsmanager/cmd/eotsd/daemon/keys.go index ceead6f6..150147c4 100644 --- a/eotsmanager/cmd/eotsd/daemon/keys.go +++ b/eotsmanager/cmd/eotsd/daemon/keys.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "io" "github.com/babylonlabs-io/babylon/types" @@ -124,6 +125,79 @@ func saveKeyNameMapping(cmd *cobra.Command, keyName string) (*types.BIP340PubKey return eotsPk, nil } +// CommandPrintAllKeys prints all EOTS keys +func CommandPrintAllKeys() *cobra.Command { + var cmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "Print all EOTS key names and public keys mapping from database.", + Example: `eotsd list --home=/path/to/cfg`, + Args: cobra.NoArgs, + RunE: runCommandPrintAllKeys, + } + + cmd.Flags().String(flags.FlagHome, config.DefaultEOTSDir, "The path to the eotsd home directory") + + return cmd +} + +func runCommandPrintAllKeys(cmd *cobra.Command, _ []string) error { + eotsKeys, err := getAllEOTSKeys(cmd) + if err != nil { + return err + } + + for keyName, key := range eotsKeys { + pk, err := schnorr.ParsePubKey(key) + if err != nil { + return err + } + eotsPk := types.NewBIP340PubKeyFromBTCPK(pk) + cmd.Printf("Key Name: %s, EOTS PK: %s\n", keyName, eotsPk.MarshalHex()) + } + + return nil +} + +func getAllEOTSKeys(cmd *cobra.Command) (map[string][]byte, error) { + homePath, err := getHomePath(cmd) + if err != nil { + return nil, err + } + + // Load configuration + cfg, err := config.LoadConfig(homePath) + if err != nil { + return nil, fmt.Errorf("failed to load config: %w", err) + } + + // Setup logger + logger, err := log.NewRootLoggerWithFile(config.LogFile(homePath), cfg.LogLevel) + if err != nil { + return nil, fmt.Errorf("failed to load the logger: %w", err) + } + + // Get database backend + dbBackend, err := cfg.DatabaseConfig.GetDBBackend() + if err != nil { + return nil, fmt.Errorf("failed to create db backend: %w", err) + } + defer dbBackend.Close() + + // Create EOTS manager + eotsManager, err := eotsmanager.NewLocalEOTSManager(homePath, cfg.KeyringBackend, dbBackend, logger) + if err != nil { + return nil, fmt.Errorf("failed to create EOTS manager: %w", err) + } + + res, err := eotsManager.ListEOTSKeys() + if err != nil { + return nil, fmt.Errorf("failed to get keys from db: %w", err) + } + + return res, nil +} + func printFromKey(cmd *cobra.Command, keyName string, eotsPk *types.BIP340PubKey) error { clientCtx, err := client.GetClientQueryContext(cmd) if err != nil { diff --git a/eotsmanager/cmd/eotsd/daemon/root.go b/eotsmanager/cmd/eotsd/daemon/root.go index 590b83ee..de200793 100644 --- a/eotsmanager/cmd/eotsd/daemon/root.go +++ b/eotsmanager/cmd/eotsd/daemon/root.go @@ -25,6 +25,7 @@ func NewRootCmd() *cobra.Command { NewKeysCmd(), NewStartCmd(), version.CommandVersion("eotsd"), + CommandPrintAllKeys(), ) return rootCmd diff --git a/eotsmanager/localmanager.go b/eotsmanager/localmanager.go index 3c5a0feb..28ba6b36 100644 --- a/eotsmanager/localmanager.go +++ b/eotsmanager/localmanager.go @@ -369,3 +369,7 @@ func (lm *LocalEOTSManager) keyExists(name string) bool { _, err := lm.kr.Key(name) return err == nil } + +func (lm *LocalEOTSManager) ListEOTSKeys() (map[string][]byte, error) { + return lm.es.GetAllEOTSKeyNames() +} diff --git a/eotsmanager/store/eotsstore.go b/eotsmanager/store/eotsstore.go index c197db67..1ced1a27 100644 --- a/eotsmanager/store/eotsstore.go +++ b/eotsmanager/store/eotsstore.go @@ -106,6 +106,34 @@ func (s *EOTSStore) GetEOTSKeyName(pk []byte) (string, error) { return keyName, nil } +// GetAllEOTSKeyNames retrieves all keys and values. +// Returns keyName -> btcPK +func (s *EOTSStore) GetAllEOTSKeyNames() (map[string][]byte, error) { + result := make(map[string][]byte) + + err := s.db.View(func(tx kvdb.RTx) error { + eotsBucket := tx.ReadBucket(eotsBucketName) + if eotsBucket == nil { + return ErrCorruptedEOTSDb + } + + return eotsBucket.ForEach(func(k, v []byte) error { + if k == nil || v == nil { + return fmt.Errorf("encountered invalid key or value in bucket") + } + result[string(v)] = k + + return nil + }) + }, func() {}) + + if err != nil { + return nil, err + } + + return result, nil +} + func (s *EOTSStore) SaveSignRecord( height uint64, chainID []byte, diff --git a/eotsmanager/store/eotsstore_test.go b/eotsmanager/store/eotsstore_test.go index bf160a18..244533d5 100644 --- a/eotsmanager/store/eotsstore_test.go +++ b/eotsmanager/store/eotsstore_test.go @@ -125,3 +125,54 @@ func FuzzSignStore(f *testing.F) { require.False(t, found) }) } + +// FuzzListKeysEOTSStore tests getting all keys from store +func FuzzListKeysEOTSStore(f *testing.F) { + testutil.AddRandomSeedsToFuzzer(f, 10) + f.Fuzz(func(t *testing.T, seed int64) { + t.Parallel() + r := rand.New(rand.NewSource(seed)) + + homePath := t.TempDir() + cfg := config.DefaultDBConfigWithHomePath(homePath) + + dbBackend, err := cfg.GetDBBackend() + require.NoError(t, err) + + vs, err := store.NewEOTSStore(dbBackend) + require.NoError(t, err) + + defer func() { + dbBackend.Close() + }() + + expected := make(map[string][]byte) + for i := 0; i < r.Intn(10); i++ { + expectedKeyName := testutil.GenRandomHexStr(r, 10) + _, btcPk, err := datagen.GenRandomBTCKeyPair(r) + require.NoError(t, err) + expected[expectedKeyName] = schnorr.SerializePubKey(btcPk) + + err = vs.AddEOTSKeyName( + btcPk, + expectedKeyName, + ) + require.NoError(t, err) + } + + keys, err := vs.GetAllEOTSKeyNames() + require.NoError(t, err) + + for keyName, btcPk := range expected { + gotBtcPk, ok := keys[keyName] + require.True(t, ok) + + parsedGot, err := schnorr.ParsePubKey(gotBtcPk) + require.NoError(t, err) + parsedExpected, err := schnorr.ParsePubKey(btcPk) + require.NoError(t, err) + + require.Equal(t, parsedExpected, parsedGot) + } + }) +} diff --git a/itest/e2e_test.go b/itest/e2e_test.go index 3b1be818..99754def 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -4,6 +4,7 @@ package e2etest import ( + "bytes" "encoding/json" "errors" "fmt" @@ -13,17 +14,19 @@ import ( "testing" "time" - "github.com/babylonlabs-io/finality-provider/finality-provider/store" - - bbntypes "github.com/babylonlabs-io/babylon/types" - bstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" - sdkmath "cosmossdk.io/math" "github.com/babylonlabs-io/babylon/testutil/datagen" + bbntypes "github.com/babylonlabs-io/babylon/types" + bstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/jessevdk/go-flags" "github.com/stretchr/testify/require" + eotscmd "github.com/babylonlabs-io/finality-provider/eotsmanager/cmd/eotsd/daemon" + eotscfg "github.com/babylonlabs-io/finality-provider/eotsmanager/config" "github.com/babylonlabs-io/finality-provider/finality-provider/cmd/fpd/daemon" + "github.com/babylonlabs-io/finality-provider/finality-provider/store" "github.com/babylonlabs-io/finality-provider/types" ) @@ -356,3 +359,47 @@ func TestRemoveMerkleProofsCmd(t *testing.T) { return errors.Is(err, store.ErrPubRandProofNotFound) }, eventuallyWaitTimeOut, eventuallyPollTime) } + +func TestPrintEotsCmd(t *testing.T) { + tm := StartManager(t) + r := rand.New(rand.NewSource(time.Now().Unix())) + defer tm.Stop(t) + + expected := make(map[string]string) + for i := 0; i < r.Intn(10); i++ { + eotsKeyName := fmt.Sprintf("eots-key-%s", datagen.GenRandomHexStr(r, 4)) + ekey, err := tm.EOTSClient.CreateKey(eotsKeyName, passphrase, hdPath) + require.NoError(t, err) + pk, err := schnorr.ParsePubKey(ekey) + require.NoError(t, err) + expected[eotsKeyName] = bbntypes.NewBIP340PubKeyFromBTCPK(pk).MarshalHex() + } + + tm.EOTSServerHandler.Stop() + + cmd := eotscmd.CommandPrintAllKeys() + + defaultConfig := eotscfg.DefaultConfigWithHomePath(tm.EOTSHomeDir) + fileParser := flags.NewParser(defaultConfig, flags.Default) + err := flags.NewIniParser(fileParser).WriteFile(eotscfg.CfgFile(tm.EOTSHomeDir), flags.IniIncludeDefaults) + require.NoError(t, err) + + cmd.SetArgs([]string{ + "--home=" + tm.EOTSHomeDir, + }) + + var outputBuffer bytes.Buffer + cmd.SetOut(&outputBuffer) + cmd.SetErr(&outputBuffer) + + err = cmd.Execute() + require.NoError(t, err) + + output := outputBuffer.String() + t.Logf("Captured output: %s", output) + + for keyName, eotsPK := range expected { + require.Contains(t, output, keyName) + require.Contains(t, output, eotsPK) + } +} diff --git a/itest/test_manager.go b/itest/test_manager.go index 5a3b7a19..99d98317 100644 --- a/itest/test_manager.go +++ b/itest/test_manager.go @@ -59,6 +59,7 @@ var ( type TestManager struct { Wg sync.WaitGroup EOTSServerHandler *EOTSServerHandler + EOTSHomeDir string FpConfig *fpcfg.Config Fps []*service.FinalityProviderApp EOTSClient *client.EOTSManagerGRpcClient @@ -138,6 +139,7 @@ func StartManager(t *testing.T) *TestManager { tm := &TestManager{ EOTSServerHandler: eh, + EOTSHomeDir: eotsHomeDir, FpConfig: cfg, EOTSClient: eotsCli, BBNClient: bc,