diff --git a/CHANGELOG.md b/CHANGELOG.md index e4f2b014..9430003a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,9 +37,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## Unreleased +### Improvements + * [#251](https://github.com/babylonlabs-io/finality-provider/pull/251) Add nlreturn lint * [#252](https://github.com/babylonlabs-io/finality-provider/pull/252) Remove interceptors and use context * [#266](https://github.com/babylonlabs-io/finality-provider/pull/266) Change default config +* [#262](https://github.com/babylonlabs-io/finality-provider/pull/262) Add new command to export pop ## v0.14.3 diff --git a/docs/pop_format_spec.md b/docs/pop_format_spec.md new file mode 100644 index 00000000..0a390be7 --- /dev/null +++ b/docs/pop_format_spec.md @@ -0,0 +1,80 @@ +# Proof of Possession (PoP) Specification + +## Overview + +The Proof of Possession (PoP) structured specification outlined in this +document allows for the verification of the mutual ownership of a Babylon +key pair and an EOTS key pair. In the following, we outline the five essential +attributes exposed by the `PoPExport` structure and provide examples and +validation procedures. + +## Attributes + +The `PoPExport` structure is defined bellow: + +```go +// PoPExport the data needed to prove ownership of the eots and babylon key pairs. +type PoPExport struct { + // Btc public key is the EOTS PK *bbntypes.BIP340PubKey marshal hex + EotsPublicKey string `json:"eotsPublicKey"` + // Babylon public key is the *secp256k1.PubKey marshal hex + BabyPublicKey string `json:"babyPublicKey"` + + // Babylon key pair signs EOTS public key as hex + BabySignEotsPk string `json:"babySignEotsPk"` + // Schnorr signature of EOTS private key over the SHA256(Baby address) + EotsSignBaby string `json:"eotsSignBaby"` + + // Babylon address ex.: bbn1f04czxeqprn0s9fe7kdzqyde2e6nqj63dllwsm + BabyAddress string `json:"babyAddress"` +} +``` + +Detailed specification of each field: + +- `EotsPublicKey`: The EOTS public key of the finality provider in hexadecimal format. +- `BabyPublicKey` – The Babylon secp256k1 public key in base64 format. +- `BabyAddress` – The Babylon account address (`bbn` prefix). The address is +derived from the `BabyPublicKey` and used as the primary identifier on the +Babylon network. +- `EotsSignBaby` – A Schnorr signature in base64 format, created by signing the +`sha256(BabyAddress)` with the EOTS private key. +- `BabySignEotsPk` – A signature of the `EotsPublicKey`, created by the Babylon +private key. This signature follows the Cosmos +[ADR-036](https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-036-arbitrary-signature.md) +specification and is encoded in base64. + +## Example + +Below is an example JSON representation of the PoPExport structure: + +```json +{ + "eotsPublicKey": "3d0bebcbe800236ce8603c5bb1ab6c2af0932e947db4956a338f119797c37f1e", + "babyPublicKey": "A0V6yw74EdvoAWVauFqkH/GVM9YIpZitZf6bVEzG69tT", + "babySignEotsPk": "AOoIG2cwC2IMiJL3OL0zLEIUY201X1qKumDr/1qDJ4oQvAp78W1nb5EnVasRPQ/XrKXqudUDnZFprLd0jaRJtQ==", + "eotsSignBaby": "pR6vxgU0gXq+VqO+y7dHpZgHTz3zr5hdqXXh0WcWNkqUnRjHrizhYAHDMV8gh4vks4PqzKAIgZ779Wqwf5UrXQ==", + "babyAddress": "bbn1f04czxeqprn0s9fe7kdzqyde2e6nqj63dllwsm" +} +``` + +## Validation + +The function responsible for validating the `PoPExport` is `VerifyPopExport`, +which can be found [here](https://github.com/babylonlabs-io/finality-provider/blob/cc07bcd4dc434f7095668724aad6865bffe425e0/eotsmanager/cmd/eotsd/daemon/pop.go#L211). + +`VerifyPopExport` ensures the authenticity and integrity of the `PoPExport` +by cross-verifying the provided signatures and public keys. This process +consists of two core validation steps: + +- `ValidEotsSignBaby` – This function checks the validity of the Schnorr +signature `(EotsSignBaby)` by verifying that the EOTS private key has correctly +signed the SHA256 hash of the BABY address. +- `ValidBabySignEots` – This function confirms that the BABY private key has +signed the EOTS public key `(EotsPublicKey)`, ensuring mutual validation +between the key pairs. + +If both signatures pass verification, the export is deemed valid, confirming +that the finality provider holds both key pairs. This function plays a critical +role in maintaining trust and security in the finality provider's key +management process. diff --git a/eotsmanager/cmd/eotsd/daemon/flags.go b/eotsmanager/cmd/eotsd/daemon/flags.go index b5e7a2ba..0a21ac87 100644 --- a/eotsmanager/cmd/eotsd/daemon/flags.go +++ b/eotsmanager/cmd/eotsd/daemon/flags.go @@ -1,6 +1,9 @@ package daemon const ( + keyNameFlag = "key-name" + eotsPkFlag = "eots-pk" + passphraseFlag = "passphrase" forceFlag = "force" rpcListenerFlag = "rpc-listener" rpcClientFlag = "rpc-client" diff --git a/eotsmanager/cmd/eotsd/daemon/pop.go b/eotsmanager/cmd/eotsd/daemon/pop.go new file mode 100644 index 00000000..5891d5f8 --- /dev/null +++ b/eotsmanager/cmd/eotsd/daemon/pop.go @@ -0,0 +1,353 @@ +package daemon + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/cometbft/cometbft/crypto/tmhash" + sdkflags "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/spf13/cobra" + + bbnparams "github.com/babylonlabs-io/babylon/app/params" + bbntypes "github.com/babylonlabs-io/babylon/types" + "github.com/babylonlabs-io/finality-provider/codec" + "github.com/babylonlabs-io/finality-provider/eotsmanager" + "github.com/babylonlabs-io/finality-provider/eotsmanager/config" + "github.com/babylonlabs-io/finality-provider/log" +) + +const ( + flagHomeBaby = "baby-home" + flagKeyNameBaby = "baby-key-name" + flagKeyringBackendBaby = "baby-keyring-backend" +) + +func init() { + bbnparams.SetAddressPrefixes() +} + +// PoPExport the data needed to prove ownership of the eots and baby key pairs. +type PoPExport struct { + // Btc public key is the EOTS PK *bbntypes.BIP340PubKey marshal hex + EotsPublicKey string `json:"eotsPublicKey"` + // Baby public key is the *secp256k1.PubKey marshal hex + BabyPublicKey string `json:"babyPublicKey"` + + // Babylon key pair signs EOTS public key as hex + BabySignEotsPk string `json:"babySignEotsPk"` + // Schnorr signature of EOTS private key over the SHA256(Baby address) + EotsSignBaby string `json:"eotsSignBaby"` + + // Babylon address ex.: bbn1f04czxeqprn0s9fe7kdzqyde2e6nqj63dllwsm + BabyAddress string `json:"babyAddress"` +} + +func NewExportPopCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "export-pop", + Short: "Exports the Proof of Possession by (1) signing over the BABY address with the EOTS private key and (2) signing over the EOTS public key with the BABY private key.", + Long: `Parse the address from the BABY keyring, load the address, hash it with + sha256 and sign based on the EOTS key associated with the key-name or eots-pk flag. + If the both flags are supplied, eots-pk takes priority. Use the generated signature + to build a Proof of Possession. For the creation of the BABY signature over the eots pk, + it loads the BABY key pair and signs the eots-pk hex and exports it.`, + RunE: exportPop, + } + + f := cmd.Flags() + + f.String(sdkflags.FlagHome, config.DefaultEOTSDir, "EOTS home directory") + f.String(keyNameFlag, "", "EOTS key name") + f.String(eotsPkFlag, "", "EOTS public key of the finality-provider") + f.String(passphraseFlag, "", "EOTS passphrase used to decrypt the keyring") + f.String(sdkflags.FlagKeyringBackend, keyring.BackendTest, "EOTS backend of the keyring") + + f.String(flagHomeBaby, "", "BABY home directory") + f.String(flagKeyNameBaby, "", "BABY key name") + f.String(flagKeyringBackendBaby, keyring.BackendTest, "BABY backend of the keyring") + + return cmd +} + +func exportPop(cmd *cobra.Command, _ []string) error { + f := cmd.Flags() + + eotsKeyName, err := f.GetString(keyNameFlag) + if err != nil { + return err + } + + eotsFpPubKeyStr, err := f.GetString(eotsPkFlag) + if err != nil { + return err + } + + eotsPassphrase, err := f.GetString(passphraseFlag) + if err != nil { + return err + } + + eotsKeyringBackend, err := f.GetString(sdkflags.FlagKeyringBackend) + if err != nil { + return err + } + + eotsHomePath, err := getHomePath(cmd) + if err != nil { + return fmt.Errorf("failed to load home flag: %w", err) + } + + babyHomePath, err := getCleanPath(cmd, flagHomeBaby) + if err != nil { + return fmt.Errorf("failed to load baby home flag: %w", err) + } + + babyKeyName, err := f.GetString(flagKeyNameBaby) + if err != nil { + return err + } + + babyKeyringBackend, err := f.GetString(flagKeyringBackendBaby) + if err != nil { + return err + } + + cdc := codec.MakeCodec() + babyKeyring, err := keyring.New("baby", babyKeyringBackend, babyHomePath, cmd.InOrStdin(), cdc) + if err != nil { + return fmt.Errorf("failed to create keyring: %w", err) + } + + babyKeyRecord, err := babyKeyring.Key(babyKeyName) + if err != nil { + return err + } + + bbnAddr, err := babyKeyRecord.GetAddress() + if err != nil { + return err + } + + if len(eotsFpPubKeyStr) == 0 && len(eotsKeyName) == 0 { + return fmt.Errorf("at least one of the flags: %s, %s needs to be informed", keyNameFlag, eotsPkFlag) + } + + cfg, err := config.LoadConfig(eotsHomePath) + if err != nil { + return fmt.Errorf("failed to load config at %s: %w", eotsHomePath, err) + } + + logger, err := log.NewRootLoggerWithFile(config.LogFile(eotsHomePath), cfg.LogLevel) + if err != nil { + return fmt.Errorf("failed to load the logger") + } + + dbBackend, err := cfg.DatabaseConfig.GetDBBackend() + if err != nil { + return fmt.Errorf("failed to create db backend: %w", err) + } + defer dbBackend.Close() + + eotsManager, err := eotsmanager.NewLocalEOTSManager(eotsHomePath, eotsKeyringBackend, dbBackend, logger) + if err != nil { + return fmt.Errorf("failed to create EOTS manager: %w", err) + } + + hashOfMsgToSign := tmhash.Sum([]byte(bbnAddr.String())) + schnorrSigOverBabyAddr, btcPubKey, err := eotsSignMsg(eotsManager, eotsKeyName, eotsFpPubKeyStr, eotsPassphrase, hashOfMsgToSign) + if err != nil { + return fmt.Errorf("failed to sign address %s: %w", bbnAddr.String(), err) + } + + babyPubKey, err := babyPk(babyKeyRecord) + if err != nil { + return err + } + + eotsPkHex := btcPubKey.MarshalHex() + + babySignBtcDoc := NewCosmosSignDoc( + bbnAddr.String(), + eotsPkHex, + ) + + babySignBtcMarshaled, err := json.Marshal(babySignBtcDoc) + if err != nil { + return fmt.Errorf("failed to marshal sign doc: %w", err) + } + + babySignBtcBz := sdk.MustSortJSON(babySignBtcMarshaled) + babySignBtc, _, err := babyKeyring.Sign(babyKeyName, babySignBtcBz, signing.SignMode_SIGN_MODE_DIRECT) + if err != nil { + return err + } + + out := PoPExport{ + EotsPublicKey: eotsPkHex, + BabyPublicKey: base64.StdEncoding.EncodeToString(babyPubKey.Bytes()), + + BabyAddress: bbnAddr.String(), + + EotsSignBaby: base64.StdEncoding.EncodeToString(schnorrSigOverBabyAddr.Serialize()), + BabySignEotsPk: base64.StdEncoding.EncodeToString(babySignBtc), + } + + jsonString, err := json.MarshalIndent(out, "", " ") + if err != nil { + return err + } + + cmd.Println(string(jsonString)) + + return nil +} + +func VerifyPopExport(pop PoPExport) (bool, error) { + valid, err := ValidEotsSignBaby(pop.EotsPublicKey, pop.BabyAddress, pop.EotsSignBaby) + if err != nil || !valid { + return false, err + } + + return ValidBabySignEots( + pop.BabyPublicKey, + pop.BabyAddress, + pop.EotsPublicKey, + pop.BabySignEotsPk, + ) +} + +func ValidEotsSignBaby(eotsPk, babyAddr, eotsSigOverBabyAddr string) (bool, error) { + eotsPubKey, err := bbntypes.NewBIP340PubKeyFromHex(eotsPk) + if err != nil { + return false, err + } + + schnorrSigBase64, err := base64.StdEncoding.DecodeString(eotsSigOverBabyAddr) + if err != nil { + return false, err + } + + schnorrSig, err := schnorr.ParseSignature(schnorrSigBase64) + if err != nil { + return false, err + } + sha256Addr := tmhash.Sum([]byte(babyAddr)) + + return schnorrSig.Verify(sha256Addr, eotsPubKey.MustToBTCPK()), nil +} + +func ValidBabySignEots(babyPk, babyAddr, eotsPk, babySigOverEotsPk string) (bool, error) { + babyPubKeyBz, err := base64.StdEncoding.DecodeString(babyPk) + if err != nil { + return false, err + } + + babyPubKey := &secp256k1.PubKey{ + Key: babyPubKeyBz, + } + + babySignBtcDoc := NewCosmosSignDoc(babyAddr, eotsPk) + babySignBtcMarshaled, err := json.Marshal(babySignBtcDoc) + if err != nil { + return false, err + } + + babySignEotsBz := sdk.MustSortJSON(babySignBtcMarshaled) + + secp256SigBase64, err := base64.StdEncoding.DecodeString(babySigOverEotsPk) + if err != nil { + return false, err + } + + return babyPubKey.VerifySignature(babySignEotsBz, secp256SigBase64), nil +} + +func babyPk(babyRecord *keyring.Record) (*secp256k1.PubKey, error) { + pubKey, err := babyRecord.GetPubKey() + if err != nil { + return nil, err + } + + switch v := pubKey.(type) { + case *secp256k1.PubKey: + return v, nil + default: + return nil, fmt.Errorf("unsupported key type in keyring") + } +} + +func eotsSignMsg( + eotsManager *eotsmanager.LocalEOTSManager, + keyName, fpPkStr, passphrase string, + hashOfMsgToSign []byte, +) (*schnorr.Signature, *bbntypes.BIP340PubKey, error) { + if len(fpPkStr) > 0 { + fpPk, err := bbntypes.NewBIP340PubKeyFromHex(fpPkStr) + if err != nil { + return nil, nil, fmt.Errorf("invalid finality-provider public key %s: %w", fpPkStr, err) + } + signature, err := eotsManager.SignSchnorrSig(*fpPk, hashOfMsgToSign, passphrase) + if err != nil { + return nil, nil, fmt.Errorf("unable to sign msg with pk %s: %w", fpPkStr, err) + } + + return signature, fpPk, nil + } + + return eotsManager.SignSchnorrSigFromKeyname(keyName, passphrase, hashOfMsgToSign) +} + +type Msg struct { + Type string `json:"type"` + Value MsgValue `json:"value"` +} + +type SignDoc struct { + ChainID string `json:"chain_id"` + AccountNumber string `json:"account_number"` + Sequence string `json:"sequence"` + Fee Fee `json:"fee"` + Msgs []Msg `json:"msgs"` + Memo string `json:"memo"` +} + +type Fee struct { + Gas string `json:"gas"` + Amount []string `json:"amount"` +} + +type MsgValue struct { + Signer string `json:"signer"` + Data string `json:"data"` +} + +func NewCosmosSignDoc( + signer string, + data string, +) *SignDoc { + return &SignDoc{ + ChainID: "", + AccountNumber: "0", + Sequence: "0", + Fee: Fee{ + Gas: "0", + Amount: []string{}, + }, + Msgs: []Msg{ + { + Type: "sign/MsgSignData", + Value: MsgValue{ + Signer: signer, + Data: data, + }, + }, + }, + Memo: "", + } +} diff --git a/eotsmanager/cmd/eotsd/daemon/pop_test.go b/eotsmanager/cmd/eotsd/daemon/pop_test.go new file mode 100644 index 00000000..8a9627dc --- /dev/null +++ b/eotsmanager/cmd/eotsd/daemon/pop_test.go @@ -0,0 +1,48 @@ +package daemon_test + +import ( + "testing" + + "github.com/babylonlabs-io/finality-provider/eotsmanager/cmd/eotsd/daemon" + "github.com/stretchr/testify/require" +) + +var hardcodedPopToVerify daemon.PoPExport = daemon.PoPExport{ + EotsPublicKey: "3d0bebcbe800236ce8603c5bb1ab6c2af0932e947db4956a338f119797c37f1e", + BabyPublicKey: "A0V6yw74EdvoAWVauFqkH/GVM9YIpZitZf6bVEzG69tT", + + BabySignEotsPk: "AOoIG2cwC2IMiJL3OL0zLEIUY201X1qKumDr/1qDJ4oQvAp78W1nb5EnVasRPQ/XrKXqudUDnZFprLd0jaRJtQ==", + EotsSignBaby: "pR6vxgU0gXq+VqO+y7dHpZgHTz3zr5hdqXXh0WcWNkqUnRjHrizhYAHDMV8gh4vks4PqzKAIgZ779Wqwf5UrXQ==", + + BabyAddress: "bbn1f04czxeqprn0s9fe7kdzqyde2e6nqj63dllwsm", +} + +func TestPoPValidEotsSignBaby(t *testing.T) { + t.Parallel() + valid, err := daemon.ValidEotsSignBaby( + hardcodedPopToVerify.EotsPublicKey, + hardcodedPopToVerify.BabyAddress, + hardcodedPopToVerify.EotsSignBaby, + ) + require.NoError(t, err) + require.True(t, valid) +} + +func TestPoPValidBabySignEotsPk(t *testing.T) { + t.Parallel() + valid, err := daemon.ValidBabySignEots( + hardcodedPopToVerify.BabyPublicKey, + hardcodedPopToVerify.BabyAddress, + hardcodedPopToVerify.EotsPublicKey, + hardcodedPopToVerify.BabySignEotsPk, + ) + require.NoError(t, err) + require.True(t, valid) +} + +func TestPoPVerify(t *testing.T) { + t.Parallel() + valid, err := daemon.VerifyPopExport(hardcodedPopToVerify) + require.NoError(t, err) + require.True(t, valid) +} diff --git a/eotsmanager/cmd/eotsd/daemon/root.go b/eotsmanager/cmd/eotsd/daemon/root.go index de200793..22f7d7bc 100644 --- a/eotsmanager/cmd/eotsd/daemon/root.go +++ b/eotsmanager/cmd/eotsd/daemon/root.go @@ -26,6 +26,7 @@ func NewRootCmd() *cobra.Command { NewStartCmd(), version.CommandVersion("eotsd"), CommandPrintAllKeys(), + NewExportPopCmd(), ) return rootCmd diff --git a/eotsmanager/cmd/eotsd/daemon/utils.go b/eotsmanager/cmd/eotsd/daemon/utils.go index 9b3e6300..46920818 100644 --- a/eotsmanager/cmd/eotsd/daemon/utils.go +++ b/eotsmanager/cmd/eotsd/daemon/utils.go @@ -14,19 +14,21 @@ import ( ) func getHomePath(cmd *cobra.Command) (string, error) { - rawHomePath, err := cmd.Flags().GetString(sdkflags.FlagHome) + return getCleanPath(cmd, sdkflags.FlagHome) +} + +func getCleanPath(cmd *cobra.Command, flag string) (string, error) { + rawPath, err := cmd.Flags().GetString(flag) if err != nil { return "", err } - homePath, err := filepath.Abs(rawHomePath) + cleanPath, err := filepath.Abs(rawPath) if err != nil { return "", err } - // Create home directory - homePath = util.CleanAndExpandPath(homePath) - return homePath, nil + return util.CleanAndExpandPath(cleanPath), nil } // PersistClientCtx persist some vars from the cmd or config to the client context.