diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e72f5d2..67926fc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ jobs: run-lint: true run-build: true run-gosec: true - gosec-args: "-exclude-generated -exclude-dir=itest -exclude-dir=testutil ./..." + gosec-args: "-exclude-generated -exclude-dir=itest -exclude-dir=testutil -exclude-dir=covenant-signer ./..." docker_pipeline: needs: ["lint_test"] @@ -33,4 +33,23 @@ jobs: # required for all workflows security-events: write # required to fetch internal or private CodeQL packs - packages: read \ No newline at end of file + packages: read + + go_sec_covenant_signer: + runs-on: ubuntu-24.04 + env: + GO111MODULE: on + steps: + - name: Fetch Repository + uses: actions/checkout@v4 + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version: '^1.23.x' + check-latest: true + cache: false + - name: Install Gosec + run: go install github.com/securego/gosec/v2/cmd/gosec@latest + - name: Run Gosec (covenant-signer) + working-directory: ./covenant-signer + run: gosec ./... diff --git a/CHANGELOG.md b/CHANGELOG.md index ccb2f43..70e43d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ version, fix some Dockerfile issue endpoint to the remote signer * [#38](https://github.com/babylonlabs-io/covenant-emulator/pull/38) Bump Babylon version to v0.17.1 +* [#37](https://github.com/babylonlabs-io/covenant-emulator/pull/37) Add remote signer +to the covenant emulator ## v0.8.0 diff --git a/cmd/covd/start.go b/cmd/covd/start.go index a9a8a39..446d8a8 100644 --- a/cmd/covd/start.go +++ b/cmd/covd/start.go @@ -7,6 +7,7 @@ import ( 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/remotesigner" "github.com/babylonlabs-io/covenant-emulator/util" "github.com/lightningnetwork/lnd/signal" @@ -60,9 +61,18 @@ func start(ctx *cli.Context) error { pwd := ctx.String(passphraseFlag) - signer, err := newSignerFromConfig(cfg, pwd) - if err != nil { - return fmt.Errorf("failed to create signer from config: %w", err) + var signer covenant.Signer + + if cfg.RemoteSignerEnabled { + signer, err = newRemoteSignerFromConfig(cfg) + if err != nil { + return fmt.Errorf("failed to create remote signer from config: %w", err) + } + } else { + signer, err = newSignerFromConfig(cfg, pwd) + if err != nil { + return fmt.Errorf("failed to create keyring signer from config: %w", err) + } } ce, err := covenant.NewCovenantEmulator(cfg, bbnClient, logger, signer) @@ -84,7 +94,7 @@ func start(ctx *cli.Context) error { return srv.RunUntilShutdown() } -func newSignerFromConfig(cfg *covcfg.Config, passphrase string) (*keyring.KeyringSigner, error) { +func newSignerFromConfig(cfg *covcfg.Config, passphrase string) (covenant.Signer, error) { return keyring.NewKeyringSigner( cfg.BabylonConfig.ChainID, cfg.BabylonConfig.Key, @@ -93,3 +103,7 @@ func newSignerFromConfig(cfg *covcfg.Config, passphrase string) (*keyring.Keyrin passphrase, ) } + +func newRemoteSignerFromConfig(cfg *covcfg.Config) (covenant.Signer, error) { + return remotesigner.NewRemoteSigner(cfg.RemoteSigner), nil +} diff --git a/config/config.go b/config/config.go index 04d4f94..dc57336 100644 --- a/config/config.go +++ b/config/config.go @@ -46,6 +46,10 @@ type Config struct { Metrics *MetricsConfig `group:"metrics" namespace:"metrics"` BabylonConfig *BBNConfig `group:"babylon" namespace:"babylon"` + + RemoteSignerEnabled bool `long:"remote-signer-enabled" description:"if true, covenant will use the remote signer to sign transactions"` + + RemoteSigner *RemoteSignerCfg `group:"remotesigner" namespace:"remotesigner"` } // LoadConfig initializes and parses the config using a config file and command @@ -128,15 +132,18 @@ func DefaultConfigWithHomePath(homePath string) Config { bbnCfg.Key = defaultCovenantKeyName bbnCfg.KeyDirectory = homePath metricsCfg := DefaultMetricsConfig() + remoteSignerCfg := DefaultRemoteSignerConfig() cfg := Config{ - LogLevel: defaultLogLevel, - QueryInterval: defaultQueryInterval, - DelegationLimit: defaultDelegationLimit, - SigsBatchSize: defaultSigsBatchSize, - BitcoinNetwork: defaultBitcoinNetwork, - BTCNetParams: defaultBTCNetParams, - Metrics: &metricsCfg, - BabylonConfig: &bbnCfg, + LogLevel: defaultLogLevel, + QueryInterval: defaultQueryInterval, + DelegationLimit: defaultDelegationLimit, + SigsBatchSize: defaultSigsBatchSize, + BitcoinNetwork: defaultBitcoinNetwork, + BTCNetParams: defaultBTCNetParams, + Metrics: &metricsCfg, + BabylonConfig: &bbnCfg, + RemoteSignerEnabled: false, + RemoteSigner: &remoteSignerCfg, } if err := cfg.Validate(); err != nil { diff --git a/config/remotesigner.go b/config/remotesigner.go new file mode 100644 index 0000000..d93129e --- /dev/null +++ b/config/remotesigner.go @@ -0,0 +1,20 @@ +package config + +import "time" + +const ( + defaultUrl = "http://127.0.0.1:9791" + defaultTimeout = 2 * time.Second +) + +type RemoteSignerCfg struct { + URL string `long:"url" description:"URL of the remote signer"` + Timeout time.Duration `long:"timeout" description:"client when making requests to the remote signer"` +} + +func DefaultRemoteSignerConfig() RemoteSignerCfg { + return RemoteSignerCfg{ + URL: defaultUrl, + Timeout: defaultTimeout, + } +} diff --git a/go.mod b/go.mod index 931a041..d662acc 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( cosmossdk.io/math v1.4.0 github.com/avast/retry-go/v4 v4.5.1 github.com/babylonlabs-io/babylon v0.17.1 + // TODO: Release covenant-signer + github.com/babylonlabs-io/covenant-emulator/covenant-signer v0.0.0-20241122072853-f24b47aaa46b github.com/btcsuite/btcd v0.24.2 github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/btcsuite/btcd/btcutil v1.1.6 @@ -105,6 +107,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/getsentry/sentry-go v0.27.0 // indirect + github.com/go-chi/chi/v5 v5.0.12 // indirect github.com/go-kit/kit v0.13.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect diff --git a/go.sum b/go.sum index a6491ea..9ab56b7 100644 --- a/go.sum +++ b/go.sum @@ -1421,6 +1421,8 @@ github.com/aws/aws-sdk-go v1.44.312/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/babylonlabs-io/babylon v0.17.1 h1:lyWGdR7B49qDw5pllLyTW/HAM5uQWXXPZefjFzy/Xy0= github.com/babylonlabs-io/babylon v0.17.1/go.mod h1:sT+KG2U+M0tDMNZZ2L5CwlXX0OpagGEs56BiWXqaZFw= +github.com/babylonlabs-io/covenant-emulator/covenant-signer v0.0.0-20241122072853-f24b47aaa46b h1:/v0YgkWITwFArI6/ovU1QyfbeGaE8GiojmY+z/rTdq8= +github.com/babylonlabs-io/covenant-emulator/covenant-signer v0.0.0-20241122072853-f24b47aaa46b/go.mod h1:9lAyEcdpfS21bMLMEa8WjTyLVfwHJABRh5TmoxC9LKU= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -1674,6 +1676,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= diff --git a/itest/e2e_test.go b/itest/e2e_test.go index e425971..f575b7c 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -18,7 +18,41 @@ var ( // TestCovenantEmulatorLifeCycle tests the whole life cycle of a covenant emulator // in two flows depending on whether the delegation is following pre-approval flow func TestCovenantEmulatorLifeCycle(t *testing.T) { - tm, btcPks := StartManagerWithFinalityProvider(t, 1) + tm, btcPks := StartManagerWithFinalityProvider(t, 1, false) + defer tm.Stop(t) + + // send a BTC delegation that is not following pre-approval flow + _ = tm.InsertBTCDelegation(t, btcPks, stakingTime, stakingAmount, false) + + // check the BTC delegation is pending + _ = tm.WaitForNPendingDels(t, 1) + + // check the BTC delegation is active + _ = tm.WaitForNActiveDels(t, 1) + + // send a BTC delegation that is following pre-approval flow + _ = tm.InsertBTCDelegation(t, btcPks, stakingTime, stakingAmount, true) + + // check the BTC delegation is pending + _ = tm.WaitForNPendingDels(t, 1) + + time.Sleep(10 * time.Second) + + // check the BTC delegation is verified + dels := tm.WaitForNVerifiedDels(t, 1) + + // test duplicate, should expect no error + // remove covenant sigs + dels[0].CovenantSigs = nil + dels[0].BtcUndelegation.CovenantSlashingSigs = nil + dels[0].BtcUndelegation.CovenantUnbondingSigs = nil + res, err := tm.CovenantEmulator.AddCovenantSignatures(dels) + require.NoError(t, err) + require.Empty(t, res) +} + +func TestCovenantEmulatorLifeCycleWithRemoteSigner(t *testing.T) { + tm, btcPks := StartManagerWithFinalityProvider(t, 1, true) defer tm.Stop(t) // send a BTC delegation that is not following pre-approval flow diff --git a/itest/test_manager.go b/itest/test_manager.go index 29ccd0c..83798d3 100644 --- a/itest/test_manager.go +++ b/itest/test_manager.go @@ -1,6 +1,7 @@ package e2etest import ( + "context" "math/rand" "os" "sync" @@ -12,6 +13,18 @@ import ( btcctypes "github.com/babylonlabs-io/babylon/x/btccheckpoint/types" btclctypes "github.com/babylonlabs-io/babylon/x/btclightclient/types" bstypes "github.com/babylonlabs-io/babylon/x/btcstaking/types" + covcc "github.com/babylonlabs-io/covenant-emulator/clientcontroller" + covcfg "github.com/babylonlabs-io/covenant-emulator/config" + "github.com/babylonlabs-io/covenant-emulator/covenant" + signerCfg "github.com/babylonlabs-io/covenant-emulator/covenant-signer/config" + "github.com/babylonlabs-io/covenant-emulator/covenant-signer/keystore/cosmos" + signerMetrics "github.com/babylonlabs-io/covenant-emulator/covenant-signer/observability/metrics" + signerApp "github.com/babylonlabs-io/covenant-emulator/covenant-signer/signerapp" + signerService "github.com/babylonlabs-io/covenant-emulator/covenant-signer/signerservice" + covdkeyring "github.com/babylonlabs-io/covenant-emulator/keyring" + "github.com/babylonlabs-io/covenant-emulator/remotesigner" + "github.com/babylonlabs-io/covenant-emulator/testutil" + "github.com/babylonlabs-io/covenant-emulator/types" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/wire" @@ -20,13 +33,6 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/stretchr/testify/require" "go.uber.org/zap" - - 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" ) var ( @@ -71,21 +77,81 @@ type testFinalityProviderData struct { PoP *bstypes.ProofOfPossessionBTC } -func StartManager(t *testing.T) *TestManager { +func StartManager(t *testing.T, useRemoteSigner bool) *TestManager { testDir, err := baseDir("cee2etest") require.NoError(t, err) logger := zap.NewNop() - - // 1. prepare covenant key, which will be used as input of Babylon node covenantConfig := defaultCovenantConfig(testDir) err = covenantConfig.Validate() require.NoError(t, err) - covKeyPair, err := covdkeyring.CreateCovenantKey(testDir, chainID, covenantKeyName, keyring.BackendTest, passphrase, hdPath) - require.NoError(t, err) + + // 1. prepare covenant key, which will be used as input of Babylon node + var signer covenant.Signer + var covPubKey *btcec.PublicKey + if useRemoteSigner { + covenantConfig.RemoteSignerEnabled = true + signerConfig := signerCfg.DefaultConfig() + signerConfig.KeyStore.CosmosKeyStore.ChainID = covenantConfig.BabylonConfig.ChainID + signerConfig.KeyStore.CosmosKeyStore.Passphrase = passphrase + signerConfig.KeyStore.CosmosKeyStore.KeyName = covenantConfig.BabylonConfig.Key + signerConfig.KeyStore.CosmosKeyStore.KeyringBackend = covenantConfig.BabylonConfig.KeyringBackend + signerConfig.KeyStore.CosmosKeyStore.KeyDirectory = covenantConfig.BabylonConfig.KeyDirectory + keyRetriever, err := cosmos.NewCosmosKeyringRetriever(signerConfig.KeyStore.CosmosKeyStore) + require.NoError(t, err) + keyInfo, err := keyRetriever.Kr.CreateChainKey( + passphrase, + hdPath, + ) + require.NoError(t, err) + require.NotNil(t, keyInfo) + + app := signerApp.NewSignerApp( + keyRetriever, + ) + + met := signerMetrics.NewCovenantSignerMetrics() + parsedConfig, err := signerConfig.Parse() + require.NoError(t, err) + + server, err := signerService.New( + context.Background(), + parsedConfig, + app, + met, + ) + + require.NoError(t, err) + + signer = remotesigner.NewRemoteSigner(covenantConfig.RemoteSigner) + covPubKey = keyInfo.PublicKey + + go func() { + _ = server.Start() + }() + + // Give some time to launch server + time.Sleep(3 * time.Second) + + t.Cleanup(func() { + _ = server.Stop(context.TODO()) + }) + } else { + covKeyPair, err := covdkeyring.CreateCovenantKey(testDir, chainID, covenantKeyName, keyring.BackendTest, passphrase, hdPath) + require.NoError(t, err) + signer, err = covdkeyring.NewKeyringSigner( + covenantConfig.BabylonConfig.ChainID, + covenantConfig.BabylonConfig.Key, + covenantConfig.BabylonConfig.KeyDirectory, + covenantConfig.BabylonConfig.KeyringBackend, + passphrase, + ) + require.NoError(t, err) + covPubKey = covKeyPair.PublicKey + } // 2. prepare Babylon node - bh := NewBabylonNodeHandler(t, bbntypes.NewBIP340PubKeyFromBTCPK(covKeyPair.PublicKey)) + bh := NewBabylonNodeHandler(t, bbntypes.NewBIP340PubKeyFromBTCPK(covPubKey)) err = bh.Start() require.NoError(t, err) @@ -94,7 +160,6 @@ func StartManager(t *testing.T) *TestManager { covbc, err := covcc.NewBabylonController(bbnCfg, &covenantConfig.BTCNetParams, logger) require.NoError(t, err) - 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) @@ -129,8 +194,8 @@ func (tm *TestManager) WaitForServicesStart(t *testing.T) { t.Logf("Babylon node is started") } -func StartManagerWithFinalityProvider(t *testing.T, n int) (*TestManager, []*btcec.PublicKey) { - tm := StartManager(t) +func StartManagerWithFinalityProvider(t *testing.T, n int, useRemoteSigner bool) (*TestManager, []*btcec.PublicKey) { + tm := StartManager(t, useRemoteSigner) var btcPks []*btcec.PublicKey for i := 0; i < n; i++ { diff --git a/remotesigner/signer.go b/remotesigner/signer.go new file mode 100644 index 0000000..bdba4ce --- /dev/null +++ b/remotesigner/signer.go @@ -0,0 +1,64 @@ +package remotesigner + +import ( + "context" + + "github.com/babylonlabs-io/covenant-emulator/config" + "github.com/babylonlabs-io/covenant-emulator/covenant" + "github.com/babylonlabs-io/covenant-emulator/covenant-signer/signerapp" + "github.com/babylonlabs-io/covenant-emulator/covenant-signer/signerservice" + "github.com/btcsuite/btcd/btcec/v2" +) + +var _ covenant.Signer = RemoteSigner{} + +func covenantRequestToSignerRequest(req covenant.SigningRequest) *signerapp.ParsedSigningRequest { + return &signerapp.ParsedSigningRequest{ + StakingTx: req.StakingTx, + SlashingTx: req.SlashingTx, + UnbondingTx: req.UnbondingTx, + SlashUnbondingTx: req.SlashUnbondingTx, + StakingOutputIdx: req.StakingOutputIdx, + SlashingScript: req.SlashingPkScriptPath, + UnbondingScript: req.StakingTxUnbondingPkScriptPath, + UnbondingSlashingScript: req.UnbondingTxSlashingPkScriptPath, + FpEncKeys: req.FpEncKeys, + } +} + +func signerResponseToCovenantResponse(resp *signerapp.ParsedSigningResponse) *covenant.SignaturesResponse { + return &covenant.SignaturesResponse{ + SlashSigs: resp.SlashAdaptorSigs, + UnbondingSig: resp.UnbondingSig, + SlashUnbondingSigs: resp.SlashUnbondingAdaptorSigs, + } +} + +type RemoteSigner struct { + cfg *config.RemoteSignerCfg +} + +func NewRemoteSigner(cfg *config.RemoteSignerCfg) RemoteSigner { + return RemoteSigner{ + cfg: cfg, + } +} + +func (rs RemoteSigner) PubKey() (*btcec.PublicKey, error) { + return signerservice.GetPublicKey(context.Background(), rs.cfg.URL, rs.cfg.Timeout) +} + +func (rs RemoteSigner) SignTransactions(req covenant.SigningRequest) (*covenant.SignaturesResponse, error) { + resp, err := signerservice.RequestCovenantSignaure( + context.Background(), + rs.cfg.URL, + rs.cfg.Timeout, + covenantRequestToSignerRequest(req), + ) + + if err != nil { + return nil, err + } + + return signerResponseToCovenantResponse(resp), nil +}