From e23e7d1347cd2ddd597e53a30a0dd0d349bcfdd8 Mon Sep 17 00:00:00 2001 From: Rafael Tenfen Date: Mon, 14 Oct 2024 12:32:40 -0300 Subject: [PATCH] chore: signing behind interface (#20) * chore: simplified equal pk check * feat: add crypto signer wrapper for btcstaking signing functions * chore: update fpName to keyName in ChainKeyringController * chore: address naming suggestions CriptoSigner -> Signer, KeyringCriptoSigner -> KeyringSigner, NewLocalKeyringCriptoSigner -> NewKeyringSigner * chore: add changelog #20 * feat: partial refactory in generating signatures * chore: removed unused funcs, simplified sort * fix: unit test sorting of covenant sigs to match expected * chore: rollback to use pub key with pointer * chore: rollback to schnorr sig with pointer * chore: rollback to use covenant sigs as pointer * chore: rollback to pointer * chore: refactory signer signature handling * chore: address gitferry pr comments * fix: pwd parameter * chore: modify to call the signed once per each delegation * chore: remove unecessary sorting of covenant signatures --- CHANGELOG.md | 5 + cmd/covd/key.go | 4 +- cmd/covd/start.go | 20 ++- covenant/covenant.go | 273 +++++++++++++---------------------- covenant/covenant_test.go | 14 +- covenant/expected_signer.go | 35 +++++ itest/test_manager.go | 9 +- keyring/keyring.go | 22 +++ keyring/keyringcontroller.go | 26 ++-- keyring/signer.go | 145 +++++++++++++++++++ 10 files changed, 359 insertions(+), 194 deletions(-) create mode 100644 covenant/expected_signer.go create mode 100644 keyring/signer.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 5af09fa..3083feb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,3 +36,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## Unreleased + +### Improvements + +* [#20](https://github.com/babylonlabs-io/covenant-emulator/pull/20) Add signing behind +interface. diff --git a/cmd/covd/key.go b/cmd/covd/key.go index bb6633f..84e92f2 100644 --- a/cmd/covd/key.go +++ b/cmd/covd/key.go @@ -9,7 +9,7 @@ import ( "github.com/urfave/cli" covcfg "github.com/babylonlabs-io/covenant-emulator/config" - "github.com/babylonlabs-io/covenant-emulator/covenant" + "github.com/babylonlabs-io/covenant-emulator/keyring" ) type covenantKey struct { @@ -71,7 +71,7 @@ func createKey(ctx *cli.Context) error { return fmt.Errorf("failed to load the config from %s: %w", covcfg.ConfigFile(homePath), err) } - keyPair, err := covenant.CreateCovenantKey( + keyPair, err := keyring.CreateCovenantKey( homePath, chainID, keyName, diff --git a/cmd/covd/start.go b/cmd/covd/start.go index 1256f61..a9a8a39 100644 --- a/cmd/covd/start.go +++ b/cmd/covd/start.go @@ -5,6 +5,7 @@ import ( "path/filepath" covcfg "github.com/babylonlabs-io/covenant-emulator/config" + "github.com/babylonlabs-io/covenant-emulator/keyring" "github.com/babylonlabs-io/covenant-emulator/log" "github.com/babylonlabs-io/covenant-emulator/util" @@ -57,7 +58,14 @@ func start(ctx *cli.Context) error { return fmt.Errorf("failed to create rpc client for the consumer chain: %w", err) } - ce, err := covenant.NewCovenantEmulator(cfg, bbnClient, ctx.String(passphraseFlag), logger) + pwd := ctx.String(passphraseFlag) + + signer, err := newSignerFromConfig(cfg, pwd) + if err != nil { + return fmt.Errorf("failed to create signer from config: %w", err) + } + + ce, err := covenant.NewCovenantEmulator(cfg, bbnClient, logger, signer) if err != nil { return fmt.Errorf("failed to start the covenant emulator: %w", err) } @@ -75,3 +83,13 @@ func start(ctx *cli.Context) error { return srv.RunUntilShutdown() } + +func newSignerFromConfig(cfg *covcfg.Config, passphrase string) (*keyring.KeyringSigner, error) { + return keyring.NewKeyringSigner( + cfg.BabylonConfig.ChainID, + cfg.BabylonConfig.Key, + cfg.BabylonConfig.KeyDirectory, + cfg.BabylonConfig.KeyringBackend, + passphrase, + ) +} diff --git a/covenant/covenant.go b/covenant/covenant.go index 8fdfa51..01c1332 100644 --- a/covenant/covenant.go +++ b/covenant/covenant.go @@ -1,10 +1,8 @@ package covenant import ( - "bytes" "encoding/hex" "fmt" - "strings" "sync" "time" @@ -22,7 +20,6 @@ import ( "github.com/babylonlabs-io/covenant-emulator/clientcontroller" covcfg "github.com/babylonlabs-io/covenant-emulator/config" - "github.com/babylonlabs-io/covenant-emulator/keyring" "github.com/babylonlabs-io/covenant-emulator/types" ) @@ -43,58 +40,31 @@ type CovenantEmulator struct { pk *btcec.PublicKey - cc clientcontroller.ClientController - kc *keyring.ChainKeyringController + signer Signer + cc clientcontroller.ClientController config *covcfg.Config logger *zap.Logger - - // input is used to pass passphrase to the keyring - input *strings.Reader - passphrase string } func NewCovenantEmulator( config *covcfg.Config, cc clientcontroller.ClientController, - passphrase string, logger *zap.Logger, + signer Signer, ) (*CovenantEmulator, error) { - input := strings.NewReader("") - kr, err := keyring.CreateKeyring( - config.BabylonConfig.KeyDirectory, - config.BabylonConfig.ChainID, - config.BabylonConfig.KeyringBackend, - input, - ) - if err != nil { - return nil, fmt.Errorf("failed to create keyring: %w", err) - } - - kc, err := keyring.NewChainKeyringControllerWithKeyring(kr, config.BabylonConfig.Key, input) - if err != nil { - return nil, err - } - - sk, err := kc.GetChainPrivKey(passphrase) - if err != nil { - return nil, fmt.Errorf("covenant key %s is not found: %w", config.BabylonConfig.Key, err) - } - - pk, err := btcec.ParsePubKey(sk.PubKey().Bytes()) + pk, err := signer.PubKey() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get signer pub key: %w", err) } return &CovenantEmulator{ - cc: cc, - kc: kc, - config: config, - logger: logger, - input: input, - passphrase: passphrase, - pk: pk, - quit: make(chan struct{}), + cc: cc, + signer: signer, + config: config, + logger: logger, + pk: pk, + quit: make(chan struct{}), }, nil } @@ -113,6 +83,7 @@ func (ce *CovenantEmulator) AddCovenantSignatures(btcDels []*types.Delegation) ( if len(btcDels) == 0 { return nil, fmt.Errorf("no delegations") } + covenantSigs := make([]*types.CovenantSigs, 0, len(btcDels)) for _, btcDel := range btcDels { // 0. nil checks @@ -206,7 +177,6 @@ func (ce *CovenantEmulator) AddCovenantSignatures(btcDels []*types.Delegation) ( // 7. Check unbonding fee unbondingFee := stakingTx.TxOut[btcDel.StakingOutputIdx].Value - unbondingTx.TxOut[0].Value - if unbondingFee != int64(params.UnbondingFee) { ce.logger.Error("invalid unbonding fee", zap.Int64("expected_unbonding_fee", int64(params.UnbondingFee)), @@ -215,60 +185,48 @@ func (ce *CovenantEmulator) AddCovenantSignatures(btcDels []*types.Delegation) ( continue } - // 8. sign covenant staking sigs - // record metrics - startSignTime := time.Now() - metricsTimeKeeper.SetPreviousSignStart(&startSignTime) - - covenantPrivKey, err := ce.getPrivKey() + // 8. Generate Signing Request + // Finality providers encryption keys + // pk script paths for Slash, unbond and unbonding slashing + fpsEncKeys, err := fpEncKeysFromDel(btcDel) if err != nil { - return nil, fmt.Errorf("failed to get Covenant private key: %w", err) + ce.logger.Error("failed to encript the finality provider keys of the btc delegation", zap.String("staker_pk", stakerPkHex), zap.Error(err)) + continue } - slashSigs, unbondingSig, err := signSlashAndUnbondSignatures( - btcDel, - stakingTx, - slashingTx, - unbondingTx, - covenantPrivKey, - params, - &ce.config.BTCNetParams, - ) + slashingPkScriptPath, stakingTxUnbondingPkScriptPath, unbondingTxSlashingPkScriptPath, err := pkScriptPaths(btcDel, params, &ce.config.BTCNetParams, unbondingTx) if err != nil { - ce.logger.Error("failed to sign signatures or unbonding signature", zap.Error(err)) + ce.logger.Error("failed to generate pk script path", zap.Error(err)) continue } - // 7. sign covenant slash unbonding signatures - slashUnbondingSigs, err := signSlashUnbondingSignatures( - btcDel, - unbondingTx, - slashUnbondingTx, - covenantPrivKey, - params, - &ce.config.BTCNetParams, - ) + // 9. sign covenant transactions + resp, err := ce.SignTransactions(SigningRequest{ + StakingTx: stakingTx, + SlashingTx: slashingTx, + UnbondingTx: unbondingTx, + SlashUnbondingTx: slashUnbondingTx, + StakingOutputIdx: btcDel.StakingOutputIdx, + SlashingPkScriptPath: slashingPkScriptPath, + StakingTxUnbondingPkScriptPath: stakingTxUnbondingPkScriptPath, + UnbondingTxSlashingPkScriptPath: unbondingTxSlashingPkScriptPath, + FpEncKeys: fpsEncKeys, + }) if err != nil { - ce.logger.Error("failed to slash unbonding signature", zap.Error(err)) + ce.logger.Error("failed to sign transactions", zap.Error(err)) continue } - // record metrics - finishSignTime := time.Now() - metricsTimeKeeper.SetPreviousSignFinish(&finishSignTime) - timedSignDelegationLag.Observe(time.Since(startSignTime).Seconds()) - - // 8. collect covenant sigs covenantSigs = append(covenantSigs, &types.CovenantSigs{ PublicKey: ce.pk, StakingTxHash: stakingTx.TxHash(), - SlashingSigs: slashSigs, - UnbondingSig: unbondingSig, - SlashingUnbondingSigs: slashUnbondingSigs, + SlashingSigs: resp.SlashSigs, + UnbondingSig: resp.UnbondingSig, + SlashingUnbondingSigs: resp.SlashUnbondingSigs, }) } - // 9. submit covenant sigs + // 10. submit covenant sigs res, err := ce.cc.SubmitCovenantSigs(covenantSigs) if err != nil { ce.recordMetricsFailedSignDelegations(len(covenantSigs)) @@ -283,14 +241,45 @@ func (ce *CovenantEmulator) AddCovenantSignatures(btcDels []*types.Delegation) ( return res, nil } -func signSlashUnbondingSignatures( +// SignTransactions calls the signer and record metrics about signing +func (ce *CovenantEmulator) SignTransactions(signingReq SigningRequest) (*SignaturesResponse, error) { + // record metrics + startSignTime := time.Now() + metricsTimeKeeper.SetPreviousSignStart(&startSignTime) + + resp, err := ce.signer.SignTransactions(signingReq) + if err != nil { + return nil, err + } + + // record metrics + finishSignTime := time.Now() + metricsTimeKeeper.SetPreviousSignFinish(&finishSignTime) + timedSignDelegationLag.Observe(time.Since(startSignTime).Seconds()) + + return resp, nil +} + +func fpEncKeysFromDel(btcDel *types.Delegation) ([]*asig.EncryptionKey, error) { + fpsEncKeys := make([]*asig.EncryptionKey, 0, len(btcDel.FpBtcPks)) + for _, fpPk := range btcDel.FpBtcPks { + encKey, err := asig.NewEncryptionKeyFromBTCPK(fpPk) + if err != nil { + fpPkHex := bbntypes.NewBIP340PubKeyFromBTCPK(fpPk).MarshalHex() + return nil, fmt.Errorf("failed to get encryption key from finality provider public key %s: %w", fpPkHex, err) + } + fpsEncKeys = append(fpsEncKeys, encKey) + } + + return fpsEncKeys, nil +} + +func pkScriptPathUnbondingSlash( del *types.Delegation, unbondingTx *wire.MsgTx, - slashUnbondingTx *wire.MsgTx, - covPrivKey *btcec.PrivateKey, params *types.StakingParams, btcNet *chaincfg.Params, -) ([][]byte, error) { +) (unbondingTxSlashingScriptPath []byte, err error) { unbondingInfo, err := btcstaking.BuildUnbondingInfo( del.BtcPk, del.FpBtcPks, @@ -304,43 +293,39 @@ func signSlashUnbondingSignatures( return nil, err } - unbondingTxSlashingPath, err := unbondingInfo.SlashingPathSpendInfo() + unbondingTxSlashingPathInfo, err := unbondingInfo.SlashingPathSpendInfo() if err != nil { return nil, err } + unbondingTxSlashingScriptPath = unbondingTxSlashingPathInfo.GetPkScriptPath() - slashUnbondingSigs := make([][]byte, 0, len(del.FpBtcPks)) - for _, fpPk := range del.FpBtcPks { - encKey, err := asig.NewEncryptionKeyFromBTCPK(fpPk) - if err != nil { - return nil, err - } - slashUnbondingSig, err := btcstaking.EncSignTxWithOneScriptSpendInputStrict( - slashUnbondingTx, - unbondingTx, - 0, // 0th output is always the unbonding script output - unbondingTxSlashingPath.GetPkScriptPath(), - covPrivKey, - encKey, - ) - if err != nil { - return nil, err - } - slashUnbondingSigs = append(slashUnbondingSigs, slashUnbondingSig.MustMarshal()) + return unbondingTxSlashingScriptPath, nil +} + +func pkScriptPaths( + del *types.Delegation, + params *types.StakingParams, + btcNet *chaincfg.Params, + unbondingTx *wire.MsgTx, +) (slash, unbond, unbondSlash []byte, err error) { + slash, unbond, err = pkScriptPathSlashAndUnbond(del, params, btcNet) + if err != nil { + return nil, nil, nil, err + } + + unbondSlash, err = pkScriptPathUnbondingSlash(del, unbondingTx, params, btcNet) + if err != nil { + return nil, nil, nil, err } - return slashUnbondingSigs, nil + return slash, unbond, unbondSlash, nil } -func signSlashAndUnbondSignatures( +func pkScriptPathSlashAndUnbond( del *types.Delegation, - stakingTx *wire.MsgTx, - slashingTx *wire.MsgTx, - unbondingTx *wire.MsgTx, - covPrivKey *btcec.PrivateKey, params *types.StakingParams, btcNet *chaincfg.Params, -) ([][]byte, *schnorr.Signature, error) { +) (slashingPkScriptPath, stakingTxUnbondingPkScriptPath []byte, err error) { // sign slash signatures with every finality providers stakingInfo, err := btcstaking.BuildStakingInfo( del.BtcPk, @@ -359,47 +344,16 @@ func signSlashAndUnbondSignatures( if err != nil { return nil, nil, fmt.Errorf("failed to get slashing path info: %w", err) } - - slashSigs := make([][]byte, 0, len(del.FpBtcPks)) - for _, fpPk := range del.FpBtcPks { - fpPkHex := bbntypes.NewBIP340PubKeyFromBTCPK(fpPk).MarshalHex() - encKey, err := asig.NewEncryptionKeyFromBTCPK(fpPk) - if err != nil { - return nil, nil, fmt.Errorf("failed to get encryption key from finality provider public key %s: %w", - fpPkHex, err) - } - slashSig, err := btcstaking.EncSignTxWithOneScriptSpendInputStrict( - slashingTx, - stakingTx, - del.StakingOutputIdx, - slashingPathInfo.GetPkScriptPath(), - covPrivKey, - encKey, - ) - if err != nil { - return nil, nil, fmt.Errorf("failed to sign adaptor signature with finaliyt provider public key %s: %w", - fpPkHex, err) - } - slashSigs = append(slashSigs, slashSig.MustMarshal()) - } + slashingPkScriptPath = slashingPathInfo.GetPkScriptPath() // sign unbonding sig stakingTxUnbondingPathInfo, err := stakingInfo.UnbondingPathSpendInfo() if err != nil { return nil, nil, fmt.Errorf("failed to get unbonding path spend info") } - unbondingSig, err := btcstaking.SignTxWithOneScriptSpendInputStrict( - unbondingTx, - stakingTx, - del.StakingOutputIdx, - stakingTxUnbondingPathInfo.GetPkScriptPath(), - covPrivKey, - ) - if err != nil { - return nil, nil, fmt.Errorf("failed to sign unbonding tx: %w", err) - } + stakingTxUnbondingPkScriptPath = stakingTxUnbondingPathInfo.GetPkScriptPath() - return slashSigs, unbondingSig, nil + return slashingPkScriptPath, stakingTxUnbondingPkScriptPath, nil } func decodeDelegationTransactions(del *types.Delegation, params *types.StakingParams, btcNet *chaincfg.Params) (*wire.MsgTx, *wire.MsgTx, error) { @@ -467,17 +421,6 @@ func decodeUndelegationTransactions(del *types.Delegation, params *types.Staking return unbondingMsgTx, unbondingSlashingMsgTx, err } -func (ce *CovenantEmulator) getPrivKey() (*btcec.PrivateKey, error) { - sdkPrivKey, err := ce.kc.GetChainPrivKey(ce.passphrase) - if err != nil { - return nil, err - } - - privKey, _ := btcec.PrivKeyFromBytes(sdkPrivKey.Key) - - return privKey, nil -} - // delegationsToBatches takes a list of delegations and splits them into batches func (ce *CovenantEmulator) delegationsToBatches(dels []*types.Delegation) [][]*types.Delegation { batchSize := ce.config.SigsBatchSize @@ -502,7 +445,7 @@ func (ce *CovenantEmulator) removeAlreadySigned(dels []*types.Delegation) []*typ delCopy := del alreadySigned := false for _, covSig := range delCopy.CovenantSigs { - if bytes.Equal(schnorr.SerializePubKey(covSig.Pk), schnorr.SerializePubKey(ce.pk)) { + if covSig.Pk.IsEqual(ce.pk) { alreadySigned = true break } @@ -584,26 +527,6 @@ func (ce *CovenantEmulator) metricsUpdateLoop() { } } -func CreateCovenantKey(keyringDir, chainID, keyName, backend, passphrase, hdPath string) (*types.ChainKeyInfo, error) { - sdkCtx, err := keyring.CreateClientCtx( - keyringDir, chainID, - ) - if err != nil { - return nil, err - } - - krController, err := keyring.NewChainKeyringController( - sdkCtx, - keyName, - backend, - ) - if err != nil { - return nil, err - } - - return krController.CreateChainKey(passphrase, hdPath) -} - func (ce *CovenantEmulator) getParamsByVersionWithRetry(version uint32) (*types.StakingParams, error) { var ( params *types.StakingParams diff --git a/covenant/covenant_test.go b/covenant/covenant_test.go index 5ed2891..3d206a3 100644 --- a/covenant/covenant_test.go +++ b/covenant/covenant_test.go @@ -2,10 +2,11 @@ package covenant_test import ( "encoding/hex" - "github.com/btcsuite/btcd/btcutil" "math/rand" "testing" + "github.com/btcsuite/btcd/btcutil" + "github.com/babylonlabs-io/babylon/btcstaking" asig "github.com/babylonlabs-io/babylon/crypto/schnorr-adaptor-signature" "github.com/babylonlabs-io/babylon/testutil/datagen" @@ -17,6 +18,7 @@ import ( covcfg "github.com/babylonlabs-io/covenant-emulator/config" "github.com/babylonlabs-io/covenant-emulator/covenant" + "github.com/babylonlabs-io/covenant-emulator/keyring" "github.com/babylonlabs-io/covenant-emulator/testutil" "github.com/babylonlabs-io/covenant-emulator/types" ) @@ -31,6 +33,7 @@ var net = &chaincfg.SimNetParams func FuzzAddCovenantSig(f *testing.F) { testutil.AddRandomSeedsToFuzzer(f, 10) f.Fuzz(func(t *testing.T, seed int64) { + t.Log("Seed", seed) r := rand.New(rand.NewSource(seed)) params := testutil.GenRandomParams(r, t) @@ -38,7 +41,9 @@ func FuzzAddCovenantSig(f *testing.F) { // create a Covenant key pair in the keyring covenantConfig := covcfg.DefaultConfig() - covKeyPair, err := covenant.CreateCovenantKey( + covenantConfig.BabylonConfig.KeyDirectory = t.TempDir() + + covKeyPair, err := keyring.CreateCovenantKey( covenantConfig.BabylonConfig.KeyDirectory, covenantConfig.BabylonConfig.ChainID, covenantConfig.BabylonConfig.Key, @@ -48,8 +53,11 @@ func FuzzAddCovenantSig(f *testing.F) { ) require.NoError(t, err) + signer, err := keyring.NewKeyringSigner(covenantConfig.BabylonConfig.ChainID, covenantConfig.BabylonConfig.Key, covenantConfig.BabylonConfig.KeyDirectory, covenantConfig.BabylonConfig.KeyringBackend, passphrase) + require.NoError(t, err) + // create and start covenant emulator - ce, err := covenant.NewCovenantEmulator(&covenantConfig, mockClientController, passphrase, zap.NewNop()) + ce, err := covenant.NewCovenantEmulator(&covenantConfig, mockClientController, zap.NewNop(), signer) require.NoError(t, err) numDels := datagen.RandomInt(r, 3) + 1 diff --git a/covenant/expected_signer.go b/covenant/expected_signer.go new file mode 100644 index 0000000..ff71cc2 --- /dev/null +++ b/covenant/expected_signer.go @@ -0,0 +1,35 @@ +package covenant + +import ( + asig "github.com/babylonlabs-io/babylon/crypto/schnorr-adaptor-signature" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/wire" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +// Signer wrapper interface to sign messages +type Signer interface { + // SignTransactions signs all the transactions from the request + // and returns all the signatures for Slash, Unbond and Unbonding Slash. + SignTransactions(req SigningRequest) (*SignaturesResponse, error) + // PubKey returns the current secp256k1 public key + PubKey() (*secp.PublicKey, error) +} + +type SigningRequest struct { + StakingTx *wire.MsgTx + SlashingTx *wire.MsgTx + UnbondingTx *wire.MsgTx + SlashUnbondingTx *wire.MsgTx + StakingOutputIdx uint32 + SlashingPkScriptPath []byte + StakingTxUnbondingPkScriptPath []byte + UnbondingTxSlashingPkScriptPath []byte + FpEncKeys []*asig.EncryptionKey +} + +type SignaturesResponse struct { + SlashSigs [][]byte + UnbondingSig *schnorr.Signature + SlashUnbondingSigs [][]byte +} diff --git a/itest/test_manager.go b/itest/test_manager.go index bc16e67..d2977f0 100644 --- a/itest/test_manager.go +++ b/itest/test_manager.go @@ -24,6 +24,7 @@ import ( covcc "github.com/babylonlabs-io/covenant-emulator/clientcontroller" covcfg "github.com/babylonlabs-io/covenant-emulator/config" "github.com/babylonlabs-io/covenant-emulator/covenant" + covdkeyring "github.com/babylonlabs-io/covenant-emulator/keyring" "github.com/babylonlabs-io/covenant-emulator/testutil" "github.com/babylonlabs-io/covenant-emulator/types" ) @@ -80,7 +81,7 @@ func StartManager(t *testing.T) *TestManager { covenantConfig := defaultCovenantConfig(testDir) err = covenantConfig.Validate() require.NoError(t, err) - covKeyPair, err := covenant.CreateCovenantKey(testDir, chainID, covenantKeyName, keyring.BackendTest, passphrase, hdPath) + covKeyPair, err := covdkeyring.CreateCovenantKey(testDir, chainID, covenantKeyName, keyring.BackendTest, passphrase, hdPath) require.NoError(t, err) // 2. prepare Babylon node @@ -92,7 +93,11 @@ func StartManager(t *testing.T) *TestManager { bbnCfg := defaultBBNConfigWithKey("test-spending-key", bh.GetNodeDataDir()) covbc, err := covcc.NewBabylonController(bbnCfg, &covenantConfig.BTCNetParams, logger) require.NoError(t, err) - ce, err := covenant.NewCovenantEmulator(covenantConfig, covbc, passphrase, logger) + + signer, err := covdkeyring.NewKeyringSigner(covenantConfig.BabylonConfig.ChainID, covenantConfig.BabylonConfig.Key, covenantConfig.BabylonConfig.KeyDirectory, covenantConfig.BabylonConfig.KeyringBackend, passphrase) + require.NoError(t, err) + + ce, err := covenant.NewCovenantEmulator(covenantConfig, covbc, logger, signer) require.NoError(t, err) err = ce.Start() require.NoError(t, err) diff --git a/keyring/keyring.go b/keyring/keyring.go index c1c914a..efefdd8 100644 --- a/keyring/keyring.go +++ b/keyring/keyring.go @@ -10,6 +10,7 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/babylonlabs-io/covenant-emulator/codec" + "github.com/babylonlabs-io/covenant-emulator/types" ) func CreateKeyring(keyringDir string, chainId string, backend string, input *strings.Reader) (keyring.Keyring, error) { @@ -52,3 +53,24 @@ func CreateClientCtx(keyringDir string, chainId string) (client.Context, error) WithCodec(codec.MakeCodec()). WithKeyringDir(keyringDir), nil } + +// CreateCovenantKey creates a new key inside the keyring +func CreateCovenantKey(keyringDir, chainID, keyName, backend, passphrase, hdPath string) (*types.ChainKeyInfo, error) { + sdkCtx, err := CreateClientCtx( + keyringDir, chainID, + ) + if err != nil { + return nil, err + } + + krController, err := NewChainKeyringController( + sdkCtx, + keyName, + backend, + ) + if err != nil { + return nil, err + } + + return krController.CreateChainKey(passphrase, hdPath) +} diff --git a/keyring/keyringcontroller.go b/keyring/keyringcontroller.go index e6ae73f..0852947 100644 --- a/keyring/keyringcontroller.go +++ b/keyring/keyringcontroller.go @@ -19,8 +19,8 @@ const ( ) type ChainKeyringController struct { - kr keyring.Keyring - fpName string + kr keyring.Keyring + keyName string // input is to send passphrase to kr input *strings.Reader } @@ -47,9 +47,9 @@ func NewChainKeyringController(ctx client.Context, name, keyringBackend string) } return &ChainKeyringController{ - fpName: name, - kr: kr, - input: inputReader, + keyName: name, + kr: kr, + input: inputReader, }, nil } @@ -59,9 +59,9 @@ func NewChainKeyringControllerWithKeyring(kr keyring.Keyring, name string, input } return &ChainKeyringController{ - kr: kr, - fpName: name, - input: input, + kr: kr, + keyName: name, + input: input, }, nil } @@ -89,7 +89,7 @@ func (kc *ChainKeyringController) CreateChainKey(passphrase, hdPath string) (*ty // we need to repeat the passphrase to mock the reentry kc.input.Reset(passphrase + "\n" + passphrase) - record, err := kc.kr.NewAccount(kc.fpName, mnemonic, passphrase, hdPath, algo) + record, err := kc.kr.NewAccount(kc.keyName, mnemonic, passphrase, hdPath, algo) if err != nil { return nil, err } @@ -100,7 +100,7 @@ func (kc *ChainKeyringController) CreateChainKey(passphrase, hdPath string) (*ty case *sdksecp256k1.PrivKey: sk, pk := btcec.PrivKeyFromBytes(v.Key) return &types.ChainKeyInfo{ - Name: kc.fpName, + Name: kc.keyName, PublicKey: pk, PrivateKey: sk, Mnemonic: mnemonic, @@ -112,7 +112,7 @@ func (kc *ChainKeyringController) CreateChainKey(passphrase, hdPath string) (*ty func (kc *ChainKeyringController) GetChainPrivKey(passphrase string) (*sdksecp256k1.PrivKey, error) { kc.input.Reset(passphrase) - k, err := kc.kr.Key(kc.fpName) + k, err := kc.kr.Key(kc.keyName) if err != nil { return nil, fmt.Errorf("failed to get private key: %w", err) } @@ -126,3 +126,7 @@ func (kc *ChainKeyringController) GetChainPrivKey(passphrase string) (*sdksecp25 return nil, fmt.Errorf("unsupported key type in keyring") } } + +func (kc *ChainKeyringController) KeyRecord() (*keyring.Record, error) { + return kc.GetKeyring().Key(kc.keyName) +} diff --git a/keyring/signer.go b/keyring/signer.go new file mode 100644 index 0000000..a53aa6d --- /dev/null +++ b/keyring/signer.go @@ -0,0 +1,145 @@ +package keyring + +import ( + "fmt" + "strings" + + "github.com/babylonlabs-io/babylon/btcstaking" + "github.com/babylonlabs-io/covenant-emulator/covenant" + + asig "github.com/babylonlabs-io/babylon/crypto/schnorr-adaptor-signature" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +var _ covenant.Signer = KeyringSigner{} + +type KeyringSigner struct { + kc *ChainKeyringController + passphrase string +} + +func NewKeyringSigner(chainId, keyName, keyringDir, keyringBackend, passphrase string) (*KeyringSigner, error) { + input := strings.NewReader("") + kr, err := CreateKeyring(keyringDir, chainId, keyringBackend, input) + if err != nil { + return nil, fmt.Errorf("failed to create keyring: %w", err) + } + + kc, err := NewChainKeyringControllerWithKeyring(kr, keyName, input) + if err != nil { + return nil, err + } + + return &KeyringSigner{ + kc: kc, + passphrase: passphrase, + }, nil +} + +func (kcs KeyringSigner) PubKey() (*secp.PublicKey, error) { + record, err := kcs.kc.KeyRecord() + if err != nil { + return nil, err + } + + pubKey, err := record.GetPubKey() + if err != nil { + return nil, err + } + + return btcec.ParsePubKey(pubKey.Bytes()) +} + +// getPrivKey returns the keyring private key +// TODO: update btcstaking functions to avoid receiving private key as parameter +// and only sign it using the kcs.kc.GetKeyring().Sign() +func (kcs KeyringSigner) getPrivKey() (*btcec.PrivateKey, error) { + sdkPrivKey, err := kcs.kc.GetChainPrivKey(kcs.passphrase) + if err != nil { + return nil, err + } + + privKey, _ := btcec.PrivKeyFromBytes(sdkPrivKey.Key) + return privKey, nil +} + +// SignTransactions receives BTC delegation transactions to sign and returns all the signatures needed if nothing fails. +func (kcs KeyringSigner) SignTransactions(req covenant.SigningRequest) (*covenant.SignaturesResponse, error) { + covenantPrivKey, err := kcs.getPrivKey() + if err != nil { + return nil, fmt.Errorf("failed to get Covenant private key: %w", err) + } + + slashSigs := make([][]byte, 0, len(req.FpEncKeys)) + slashUnbondingSigs := make([][]byte, 0, len(req.FpEncKeys)) + for _, fpEncKey := range req.FpEncKeys { + slashSig, slashUnbondingSig, err := slashUnbondSig(covenantPrivKey, req, fpEncKey) + if err != nil { + return nil, err + } + + slashSigs = append(slashSigs, slashSig.MustMarshal()) + slashUnbondingSigs = append(slashUnbondingSigs, slashUnbondingSig.MustMarshal()) + } + + unbondingSig, err := unbondSig(covenantPrivKey, req) + if err != nil { + return nil, err + } + + return &covenant.SignaturesResponse{ + SlashSigs: slashSigs, + UnbondingSig: unbondingSig, + SlashUnbondingSigs: slashUnbondingSigs, + }, nil +} + +func slashUnbondSig( + covenantPrivKey *secp.PrivateKey, + signingTxReq covenant.SigningRequest, + fpEncKey *asig.EncryptionKey, +) (slashSig, slashUnbondingSig *asig.AdaptorSignature, err error) { + // creates slash sigs + slashSig, err = btcstaking.EncSignTxWithOneScriptSpendInputStrict( + signingTxReq.SlashingTx, + signingTxReq.StakingTx, + signingTxReq.StakingOutputIdx, + signingTxReq.SlashingPkScriptPath, + covenantPrivKey, + fpEncKey, + ) + if err != nil { + return nil, nil, fmt.Errorf("failed to sign adaptor slash signature with finality provider public key %s: %w", fpEncKey.ToBytes(), err) + } + + // creates slash unbonding sig + slashUnbondingSig, err = btcstaking.EncSignTxWithOneScriptSpendInputStrict( + signingTxReq.SlashUnbondingTx, + signingTxReq.UnbondingTx, + 0, // 0th output is always the unbonding script output + signingTxReq.UnbondingTxSlashingPkScriptPath, + covenantPrivKey, + fpEncKey, + ) + if err != nil { + return nil, nil, fmt.Errorf("failed to sign adaptor slash unbonding signature with finality provider public key %s: %w", fpEncKey.ToBytes(), err) + } + + return slashSig, slashUnbondingSig, nil +} + +func unbondSig(covenantPrivKey *secp.PrivateKey, signingTxReq covenant.SigningRequest) (*schnorr.Signature, error) { + unbondingSig, err := btcstaking.SignTxWithOneScriptSpendInputStrict( + signingTxReq.UnbondingTx, + signingTxReq.StakingTx, + signingTxReq.StakingOutputIdx, + signingTxReq.StakingTxUnbondingPkScriptPath, + covenantPrivKey, + ) + if err != nil { + return nil, fmt.Errorf("failed to sign unbonding tx: %w", err) + } + return unbondingSig, nil +}