diff --git a/core/chains/cosmos/chain.go b/core/chains/cosmos/chain.go index 91f5484e877..c73793c1c3a 100644 --- a/core/chains/cosmos/chain.go +++ b/core/chains/cosmos/chain.go @@ -20,13 +20,13 @@ import ( "github.com/smartcontractkit/chainlink-cosmos/pkg/cosmos/db" "github.com/smartcontractkit/chainlink-relay/pkg/logger" + "github.com/smartcontractkit/chainlink-relay/pkg/loop" relaytypes "github.com/smartcontractkit/chainlink-relay/pkg/types" "github.com/smartcontractkit/chainlink/v2/core/chains" "github.com/smartcontractkit/chainlink/v2/core/chains/cosmos/cosmostxm" "github.com/smartcontractkit/chainlink/v2/core/chains/cosmos/types" "github.com/smartcontractkit/chainlink/v2/core/chains/internal" - "github.com/smartcontractkit/chainlink/v2/core/services/keystore" "github.com/smartcontractkit/chainlink/v2/core/services/pg" "github.com/smartcontractkit/chainlink/v2/core/utils" ) @@ -54,7 +54,7 @@ type ChainOpts struct { QueryConfig pg.QConfig Logger logger.Logger DB *sqlx.DB - KeyStore keystore.Cosmos + KeyStore loop.Keystore EventBroadcaster pg.EventBroadcaster Configs types.Configs } @@ -112,7 +112,7 @@ type chain struct { lggr logger.Logger } -func newChain(id string, cfg *CosmosConfig, db *sqlx.DB, ks keystore.Cosmos, logCfg pg.QConfig, eb pg.EventBroadcaster, cfgs types.Configs, lggr logger.Logger) (*chain, error) { +func newChain(id string, cfg *CosmosConfig, db *sqlx.DB, ks loop.Keystore, logCfg pg.QConfig, eb pg.EventBroadcaster, cfgs types.Configs, lggr logger.Logger) (*chain, error) { lggr = logger.With(lggr, "cosmosChainID", id) var ch = chain{ id: id, diff --git a/core/chains/cosmos/cosmostxm/helpers_test.go b/core/chains/cosmos/cosmostxm/helpers_test.go index 217160c3e80..ad93189082e 100644 --- a/core/chains/cosmos/cosmostxm/helpers_test.go +++ b/core/chains/cosmos/cosmostxm/helpers_test.go @@ -5,6 +5,7 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + "golang.org/x/exp/maps" cosmosclient "github.com/smartcontractkit/chainlink-cosmos/pkg/cosmos/client" ) @@ -28,3 +29,15 @@ func (txm *Txm) MarshalMsg(msg sdk.Msg) (string, []byte, error) { func (txm *Txm) SendMsgBatch(ctx context.Context) { txm.sendMsgBatch(ctx) } + +func (ka *KeystoreAdapter) Accounts(ctx context.Context) ([]string, error) { + ka.mutex.Lock() + defer ka.mutex.Unlock() + err := ka.updateMappingLocked() + if err != nil { + return nil, err + } + addresses := maps.Keys(ka.addressToPubKey) + + return addresses, nil +} diff --git a/core/chains/cosmos/cosmostxm/key_wrapper.go b/core/chains/cosmos/cosmostxm/key_wrapper.go index 6bc36f5aea7..1d2d686c8c0 100644 --- a/core/chains/cosmos/cosmostxm/key_wrapper.go +++ b/core/chains/cosmos/cosmostxm/key_wrapper.go @@ -1,56 +1,62 @@ package cosmostxm import ( - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "bytes" + "context" - "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/cosmoskey" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" ) -var _ cryptotypes.PrivKey = KeyWrapper{} - -// KeyWrapper wrapper around a cosmos transmitter key -// for use in the cosmos txbuilder and client, see chainlink-cosmos. +// KeyWrapper uses a KeystoreAdapter to implement the cosmos-sdk PrivKey interface for a specific key. type KeyWrapper struct { - key cosmoskey.Key + adapter *KeystoreAdapter + account string } -// NewKeyWrapper create a key wrapper -func NewKeyWrapper(key cosmoskey.Key) KeyWrapper { - return KeyWrapper{key: key} +var _ cryptotypes.PrivKey = &KeyWrapper{} + +func NewKeyWrapper(adapter *KeystoreAdapter, account string) *KeyWrapper { + return &KeyWrapper{ + adapter: adapter, + account: account, + } } -// Reset nop -func (k KeyWrapper) Reset() {} +func (a *KeyWrapper) Bytes() []byte { + // don't expose the private key. + return nil +} -// ProtoMessage nop -func (k KeyWrapper) ProtoMessage() {} +func (a *KeyWrapper) Sign(msg []byte) ([]byte, error) { + return a.adapter.Sign(context.Background(), a.account, msg) +} -// String nop -func (k KeyWrapper) String() string { - return "" +func (a *KeyWrapper) PubKey() cryptotypes.PubKey { + pubKey, err := a.adapter.PubKey(a.account) + if err != nil { + // return an empty pubkey if it's not found. + return &secp256k1.PubKey{Key: []byte{}} + } + return pubKey } -// Bytes does not expose private key -func (k KeyWrapper) Bytes() []byte { - return []byte{} +func (a *KeyWrapper) Equals(other cryptotypes.LedgerPrivKey) bool { + return bytes.Equal(a.PubKey().Bytes(), other.PubKey().Bytes()) } -// Sign sign a message with key -func (k KeyWrapper) Sign(msg []byte) ([]byte, error) { - return k.key.ToPrivKey().Sign(msg) +func (a *KeyWrapper) Type() string { + return "secp256k1" } -// PubKey get the pubkey -func (k KeyWrapper) PubKey() cryptotypes.PubKey { - return k.key.PublicKey() +func (a *KeyWrapper) Reset() { + // no-op } -// Equals compare against another key -func (k KeyWrapper) Equals(a cryptotypes.LedgerPrivKey) bool { - return k.PubKey().Address().String() == a.PubKey().Address().String() +func (a *KeyWrapper) String() string { + return "" } -// Type nop -func (k KeyWrapper) Type() string { - return "" +func (a *KeyWrapper) ProtoMessage() { + // no-op } diff --git a/core/chains/cosmos/cosmostxm/keystore_adapter.go b/core/chains/cosmos/cosmostxm/keystore_adapter.go new file mode 100644 index 00000000000..c8556015c6e --- /dev/null +++ b/core/chains/cosmos/cosmostxm/keystore_adapter.go @@ -0,0 +1,129 @@ +package cosmostxm + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "sync" + + "github.com/cometbft/cometbft/crypto" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/types/bech32" + "github.com/pkg/errors" + "golang.org/x/crypto/ripemd160" //nolint: staticcheck + + "github.com/smartcontractkit/chainlink-relay/pkg/loop" +) + +type accountInfo struct { + Account string + PubKey *secp256k1.PubKey +} + +// An adapter for a Cosmos loop.Keystore to translate public keys into bech32-prefixed account addresses. +type KeystoreAdapter struct { + keystore loop.Keystore + accountPrefix string + mutex sync.RWMutex + addressToPubKey map[string]*accountInfo +} + +func NewKeystoreAdapter(keystore loop.Keystore, accountPrefix string) *KeystoreAdapter { + return &KeystoreAdapter{ + keystore: keystore, + accountPrefix: accountPrefix, + addressToPubKey: make(map[string]*accountInfo), + } +} + +func (ka *KeystoreAdapter) updateMappingLocked() error { + accounts, err := ka.keystore.Accounts(context.Background()) + if err != nil { + return err + } + + // similar to cosmos-sdk, cache and re-use calculated bech32 addresses to prevent duplicated work. + // ref: https://github.com/cosmos/cosmos-sdk/blob/3b509c187e1643757f5ef8a0b5ae3decca0c7719/types/address.go#L705 + + type cacheEntry struct { + bech32Addr string + accountInfo *accountInfo + } + accountCache := make(map[string]cacheEntry, len(ka.addressToPubKey)) + for bech32Addr, accountInfo := range ka.addressToPubKey { + accountCache[accountInfo.Account] = cacheEntry{bech32Addr: bech32Addr, accountInfo: accountInfo} + } + + addressToPubKey := make(map[string]*accountInfo, len(accounts)) + for _, account := range accounts { + if prevEntry, ok := accountCache[account]; ok { + addressToPubKey[prevEntry.bech32Addr] = prevEntry.accountInfo + continue + } + pubKeyBytes, err := hex.DecodeString(account) + if err != nil { + return err + } + + if len(pubKeyBytes) != secp256k1.PubKeySize { + return errors.New("length of pubkey is incorrect") + } + + sha := sha256.Sum256(pubKeyBytes) + hasherRIPEMD160 := ripemd160.New() + _, _ = hasherRIPEMD160.Write(sha[:]) + address := crypto.Address(hasherRIPEMD160.Sum(nil)) + + bech32Addr, err := bech32.ConvertAndEncode(ka.accountPrefix, address) + if err != nil { + return err + } + + addressToPubKey[bech32Addr] = &accountInfo{ + Account: account, + PubKey: &secp256k1.PubKey{Key: pubKeyBytes}, + } + } + + ka.addressToPubKey = addressToPubKey + return nil +} + +func (ka *KeystoreAdapter) lookup(id string) (*accountInfo, error) { + ka.mutex.RLock() + ai, ok := ka.addressToPubKey[id] + ka.mutex.RUnlock() + if !ok { + // try updating the mapping once, incase there was an update on the keystore. + ka.mutex.Lock() + err := ka.updateMappingLocked() + if err != nil { + ka.mutex.Unlock() + return nil, err + } + ai, ok = ka.addressToPubKey[id] + ka.mutex.Unlock() + if !ok { + return nil, errors.New("No such id") + } + } + return ai, nil +} + +func (ka *KeystoreAdapter) Sign(ctx context.Context, id string, hash []byte) ([]byte, error) { + accountInfo, err := ka.lookup(id) + if err != nil { + return nil, err + } + return ka.keystore.Sign(ctx, accountInfo.Account, hash) +} + +// Returns the cosmos PubKey associated with the prefixed address. +func (ka *KeystoreAdapter) PubKey(address string) (cryptotypes.PubKey, error) { + accountInfo, err := ka.lookup(address) + if err != nil { + return nil, err + } + return accountInfo.PubKey, nil +} diff --git a/core/chains/cosmos/cosmostxm/txm.go b/core/chains/cosmos/cosmostxm/txm.go index 7b0e2ad2887..84118b9381b 100644 --- a/core/chains/cosmos/cosmostxm/txm.go +++ b/core/chains/cosmos/cosmostxm/txm.go @@ -25,10 +25,9 @@ import ( "github.com/smartcontractkit/chainlink-cosmos/pkg/cosmos/db" "github.com/smartcontractkit/chainlink-relay/pkg/logger" + "github.com/smartcontractkit/chainlink-relay/pkg/loop" "github.com/smartcontractkit/chainlink/v2/core/services" - "github.com/smartcontractkit/chainlink/v2/core/services/keystore" - "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/cosmoskey" "github.com/smartcontractkit/chainlink/v2/core/services/pg" "github.com/smartcontractkit/chainlink/v2/core/utils" ) @@ -40,32 +39,33 @@ var ( // Txm manages transactions for the cosmos blockchain. type Txm struct { - starter utils.StartStopOnce - eb pg.EventBroadcaster - sub pg.Subscription - orm *ORM - lggr logger.Logger - tc func() (cosmosclient.ReaderWriter, error) - ks keystore.Cosmos - stop, done chan struct{} - cfg coscfg.Config - gpe cosmosclient.ComposedGasPriceEstimator + starter utils.StartStopOnce + eb pg.EventBroadcaster + sub pg.Subscription + orm *ORM + lggr logger.Logger + tc func() (cosmosclient.ReaderWriter, error) + keystoreAdapter *KeystoreAdapter + stop, done chan struct{} + cfg coscfg.Config + gpe cosmosclient.ComposedGasPriceEstimator } // NewTxm creates a txm. Uses simulation so should only be used to send txes to trusted contracts i.e. OCR. -func NewTxm(db *sqlx.DB, tc func() (cosmosclient.ReaderWriter, error), gpe cosmosclient.ComposedGasPriceEstimator, chainID string, cfg coscfg.Config, ks keystore.Cosmos, lggr logger.Logger, logCfg pg.QConfig, eb pg.EventBroadcaster) *Txm { +func NewTxm(db *sqlx.DB, tc func() (cosmosclient.ReaderWriter, error), gpe cosmosclient.ComposedGasPriceEstimator, chainID string, cfg coscfg.Config, ks loop.Keystore, lggr logger.Logger, logCfg pg.QConfig, eb pg.EventBroadcaster) *Txm { lggr = logger.Named(lggr, "Txm") + keystoreAdapter := NewKeystoreAdapter(ks, cfg.Bech32Prefix()) return &Txm{ - starter: utils.StartStopOnce{}, - eb: eb, - orm: NewORM(chainID, db, lggr, logCfg), - ks: ks, - tc: tc, - lggr: lggr, - stop: make(chan struct{}), - done: make(chan struct{}), - cfg: cfg, - gpe: gpe, + starter: utils.StartStopOnce{}, + eb: eb, + orm: NewORM(chainID, db, lggr, logCfg), + lggr: lggr, + tc: tc, + keystoreAdapter: keystoreAdapter, + stop: make(chan struct{}), + done: make(chan struct{}), + cfg: cfg, + gpe: gpe, } } @@ -259,14 +259,11 @@ func (txm *Txm) sendMsgBatch(ctx context.Context) { } for s, msgs := range msgsByFrom { sender, _ := sdk.AccAddressFromBech32(s) // Already checked validity above - key, err := txm.ks.Get(sender.String()) + err := txm.sendMsgBatchFromAddress(ctx, gasPrice, sender, msgs) if err != nil { - txm.lggr.Errorw("unable to find key for from address", "err", err, "from", sender.String()) - // We check the transmitter key exists when the job is added. So it would have to be deleted - // after it was added for this to happen. Retry on next poll should the key be re-added. + txm.lggr.Errorw("Could not send message batch", "err", err, "from", sender.String()) continue } - txm.sendMsgBatchFromAddress(ctx, gasPrice, sender, key, msgs) if ctx.Err() != nil { return } @@ -274,18 +271,18 @@ func (txm *Txm) sendMsgBatch(ctx context.Context) { } -func (txm *Txm) sendMsgBatchFromAddress(ctx context.Context, gasPrice sdk.DecCoin, sender sdk.AccAddress, key cosmoskey.Key, msgs adapters.Msgs) { +func (txm *Txm) sendMsgBatchFromAddress(ctx context.Context, gasPrice sdk.DecCoin, sender sdk.AccAddress, msgs adapters.Msgs) error { tc, err := txm.tc() if err != nil { logger.Criticalw(txm.lggr, "unable to get client", "err", err) - return + return err } an, sn, err := tc.Account(sender) if err != nil { txm.lggr.Warnw("unable to read account", "err", err, "from", sender.String()) // If we can't read the account, assume transient api issues and leave msgs unstarted // to retry on next poll. - return + return err } txm.lggr.Debugw("simulating batch", "from", sender, "msgs", msgs, "seqnum", sn) @@ -296,27 +293,27 @@ func (txm *Txm) sendMsgBatchFromAddress(ctx context.Context, gasPrice sdk.DecCoi // Note one rare scenario in which this can happen: the cosmos node misbehaves // in that it confirms a txhash is present but still gives an old seq num. // This is benign as the next retry will succeeds. - return + return err } txm.lggr.Debugw("simulation results", "from", sender, "succeeded", simResults.Succeeded, "failed", simResults.Failed) err = txm.orm.UpdateMsgs(simResults.Failed.GetSimMsgsIDs(), db.Errored, nil) if err != nil { txm.lggr.Errorw("unable to mark failed sim txes as errored", "err", err, "from", sender.String()) // If we can't mark them as failed retry on next poll. Presumably same ones will fail. - return + return err } // Continue if there are no successful txes if len(simResults.Succeeded) == 0 { txm.lggr.Warnw("all sim msgs errored, not sending tx", "from", sender.String()) - return + return errors.New("all sim msgs errored") } // Get the gas limit for the successful batch s, err := tc.SimulateUnsigned(simResults.Succeeded.GetMsgs(), sn) if err != nil { // In the OCR context this should only happen upon stale report txm.lggr.Warnw("unexpected failure after successful simulation", "err", err) - return + return err } gasLimit := s.GasInfo.GasUsed @@ -324,14 +321,14 @@ func (txm *Txm) sendMsgBatchFromAddress(ctx context.Context, gasPrice sdk.DecCoi if err != nil { txm.lggr.Warnw("unable to get latest block", "err", err, "from", sender.String()) // Assume transient api issue and retry. - return + return err } timeoutHeight := uint64(lb.Block.Header.Height) + uint64(txm.cfg.BlocksUntilTxTimeout()) signedTx, err := tc.CreateAndSign(simResults.Succeeded.GetMsgs(), an, sn, gasLimit, txm.cfg.GasLimitMultiplier(), - gasPrice, NewKeyWrapper(key), timeoutHeight) + gasPrice, NewKeyWrapper(txm.keystoreAdapter, sender.String()), timeoutHeight) if err != nil { txm.lggr.Errorw("unable to sign tx", "err", err, "from", sender.String()) - return + return err } // We need to ensure that we either broadcast successfully and mark the tx as @@ -367,14 +364,16 @@ func (txm *Txm) sendMsgBatchFromAddress(ctx context.Context, gasPrice sdk.DecCoi if err != nil { txm.lggr.Errorw("error broadcasting tx", "err", err, "from", sender.String()) // Was unable to broadcast, retry on next poll - return + return err } maxPolls, pollPeriod := txm.confirmPollConfig() if err := txm.confirmTx(ctx, tc, resp.TxResponse.TxHash, simResults.Succeeded.GetSimMsgsIDs(), maxPolls, pollPeriod); err != nil { txm.lggr.Errorw("error confirming tx", "err", err, "hash", resp.TxResponse.TxHash) - return + return err } + + return nil } func (txm *Txm) confirmPollConfig() (maxPolls int, pollPeriod time.Duration) { diff --git a/core/chains/cosmos/cosmostxm/txm_internal_test.go b/core/chains/cosmos/cosmostxm/txm_internal_test.go index 6a9944a1a53..17eeb74421a 100644 --- a/core/chains/cosmos/cosmostxm/txm_internal_test.go +++ b/core/chains/cosmos/cosmostxm/txm_internal_test.go @@ -54,22 +54,27 @@ func TestTxm(t *testing.T) { lggr := testutils.LoggerAssertMaxLevel(t, zapcore.ErrorLevel) ks := keystore.New(db, utils.FastScryptParams, lggr, pgtest.NewQConfig(true)) require.NoError(t, ks.Unlock("blah")) - k1, err := ks.Cosmos().Create() - require.NoError(t, err) - sender1, err := cosmostypes.AccAddressFromBech32(k1.PublicKeyStr()) - require.NoError(t, err) - k2, err := ks.Cosmos().Create() - require.NoError(t, err) - sender2, err := cosmostypes.AccAddressFromBech32(k2.PublicKeyStr()) + + for i := 0; i < 4; i++ { + _, err := ks.Cosmos().Create() + require.NoError(t, err) + } + + loopKs := &keystore.CosmosLoopKeystore{Cosmos: ks.Cosmos()} + adapter := cosmostxm.NewKeystoreAdapter(loopKs, "wasm") + accounts, err := adapter.Accounts(testutils.Context(t)) require.NoError(t, err) - k3, err := ks.Cosmos().Create() + require.Equal(t, len(accounts), 4) + + sender1, err := cosmostypes.AccAddressFromBech32(accounts[0]) require.NoError(t, err) - contract, err := cosmostypes.AccAddressFromBech32(k3.PublicKeyStr()) + sender2, err := cosmostypes.AccAddressFromBech32(accounts[1]) require.NoError(t, err) - k4, err := ks.Cosmos().Create() + contract, err := cosmostypes.AccAddressFromBech32(accounts[2]) require.NoError(t, err) - contract2, err := cosmostypes.AccAddressFromBech32(k4.PublicKeyStr()) + contract2, err := cosmostypes.AccAddressFromBech32(accounts[3]) require.NoError(t, err) + logCfg := pgtest.NewQConfig(true) chainID := cosmostest.RandomChainID() two := int64(2) @@ -90,7 +95,8 @@ func TestTxm(t *testing.T) { t.Run("single msg", func(t *testing.T) { tc := newReaderWriterMock(t) tcFn := func() (cosmosclient.ReaderWriter, error) { return tc, nil } - txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, cfg, ks.Cosmos(), lggr, logCfg, nil) + loopKs := &keystore.CosmosLoopKeystore{Cosmos: ks.Cosmos()} + txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, cfg, loopKs, lggr, logCfg, nil) // Enqueue a single msg, then send it in a batch id1, err := txm.Enqueue(contract.String(), generateExecuteMsg(t, []byte(`1`), sender1, contract)) @@ -126,7 +132,8 @@ func TestTxm(t *testing.T) { t.Run("two msgs different accounts", func(t *testing.T) { tc := newReaderWriterMock(t) tcFn := func() (cosmosclient.ReaderWriter, error) { return tc, nil } - txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, cfg, ks.Cosmos(), lggr, pgtest.NewQConfig(true), nil) + loopKs := &keystore.CosmosLoopKeystore{Cosmos: ks.Cosmos()} + txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, cfg, loopKs, lggr, pgtest.NewQConfig(true), nil) id1, err := txm.Enqueue(contract.String(), generateExecuteMsg(t, []byte(`0`), sender1, contract)) require.NoError(t, err) @@ -181,7 +188,8 @@ func TestTxm(t *testing.T) { t.Run("two msgs different contracts", func(t *testing.T) { tc := newReaderWriterMock(t) tcFn := func() (cosmosclient.ReaderWriter, error) { return tc, nil } - txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, cfg, ks.Cosmos(), lggr, pgtest.NewQConfig(true), nil) + loopKs := &keystore.CosmosLoopKeystore{Cosmos: ks.Cosmos()} + txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, cfg, loopKs, lggr, pgtest.NewQConfig(true), nil) id1, err := txm.Enqueue(contract.String(), generateExecuteMsg(t, []byte(`0`), sender1, contract)) require.NoError(t, err) @@ -244,7 +252,8 @@ func TestTxm(t *testing.T) { TxResponse: &cosmostypes.TxResponse{TxHash: "0x123"}, }, errors.New("not found")).Twice() tcFn := func() (cosmosclient.ReaderWriter, error) { return tc, nil } - txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, cfg, ks.Cosmos(), lggr, pgtest.NewQConfig(true), nil) + loopKs := &keystore.CosmosLoopKeystore{Cosmos: ks.Cosmos()} + txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, cfg, loopKs, lggr, pgtest.NewQConfig(true), nil) i, err := txm.ORM().InsertMsg("blah", "", []byte{0x01}) require.NoError(t, err) txh := "0x123" @@ -274,7 +283,8 @@ func TestTxm(t *testing.T) { TxResponse: &cosmostypes.TxResponse{TxHash: txHash3}, }, nil).Once() tcFn := func() (cosmosclient.ReaderWriter, error) { return tc, nil } - txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, cfg, ks.Cosmos(), lggr, pgtest.NewQConfig(true), nil) + loopKs := &keystore.CosmosLoopKeystore{Cosmos: ks.Cosmos()} + txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, cfg, loopKs, lggr, pgtest.NewQConfig(true), nil) // Insert and broadcast 3 msgs with different txhashes. id1, err := txm.ORM().InsertMsg("blah", "", []byte{0x01}) @@ -317,7 +327,8 @@ func TestTxm(t *testing.T) { TxMsgTimeout: &timeout, }} cfgShortExpiry.SetDefaults() - txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, cfgShortExpiry, ks.Cosmos(), lggr, pgtest.NewQConfig(true), nil) + loopKs := &keystore.CosmosLoopKeystore{Cosmos: ks.Cosmos()} + txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, cfgShortExpiry, loopKs, lggr, pgtest.NewQConfig(true), nil) // Send a single one expired id1, err := txm.ORM().InsertMsg("blah", "", []byte{0x03}) @@ -362,7 +373,8 @@ func TestTxm(t *testing.T) { MaxMsgsPerBatch: &two, }} cfgMaxMsgs.SetDefaults() - txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, cfgMaxMsgs, ks.Cosmos(), lggr, pgtest.NewQConfig(true), nil) + loopKs := &keystore.CosmosLoopKeystore{Cosmos: ks.Cosmos()} + txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, cfgMaxMsgs, loopKs, lggr, pgtest.NewQConfig(true), nil) // Leftover started is processed msg1 := generateExecuteMsg(t, []byte{0x03}, sender1, contract) diff --git a/core/chains/cosmos/cosmostxm/txm_test.go b/core/chains/cosmos/cosmostxm/txm_test.go index 7bfd76e1d8c..9966ff44a5b 100644 --- a/core/chains/cosmos/cosmostxm/txm_test.go +++ b/core/chains/cosmos/cosmostxm/txm_test.go @@ -63,11 +63,17 @@ func TestTxm_Integration(t *testing.T) { tc, err := cosmosclient.NewClient(chainID, tendermintURL, cosmos.DefaultRequestTimeout, lggr) require.NoError(t, err) + loopKs := &keystore.CosmosLoopKeystore{Cosmos: ks.Cosmos()} + keystoreAdapter := cosmostxm.NewKeystoreAdapter(loopKs, *cosmosChain.Bech32Prefix) + // First create a transmitter key and fund it with 1k native tokens require.NoError(t, ks.Unlock("blah")) - transmitterKey, err := ks.Cosmos().Create() + err = ks.Cosmos().EnsureKey() + require.NoError(t, err) + ksAccounts, err := keystoreAdapter.Accounts(testutils.Context(t)) require.NoError(t, err) - transmitterID, err := sdk.AccAddressFromBech32(transmitterKey.PublicKeyStr()) + transmitterAddress := ksAccounts[0] + transmitterID, err := sdk.AccAddressFromBech32(transmitterAddress) require.NoError(t, err) an, sn, err := tc.Account(accounts[0].Address) require.NoError(t, err) @@ -82,13 +88,13 @@ func TestTxm_Integration(t *testing.T) { // the chainlink-cosmos repo instead of copying it to cores testdata contractID := cosmosclient.DeployTestContract(t, tendermintURL, chainID, *cosmosChain.FeeToken, accounts[0], cosmosclient.Account{ Name: "transmitter", - PrivateKey: cosmostxm.NewKeyWrapper(transmitterKey), + PrivateKey: cosmostxm.NewKeyWrapper(keystoreAdapter, transmitterAddress), Address: transmitterID, }, tc, testdir, "../../../testdata/cosmos/my_first_contract.wasm") tcFn := func() (cosmosclient.ReaderWriter, error) { return tc, nil } // Start txm - txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, &chainConfig, ks.Cosmos(), lggr, pgtest.NewQConfig(true), eb) + txm := cosmostxm.NewTxm(db, tcFn, *gpe, chainID, &chainConfig, loopKs, lggr, pgtest.NewQConfig(true), eb) require.NoError(t, txm.Start(testutils.Context(t))) // Change the contract state diff --git a/core/services/chainlink/relayer_factory.go b/core/services/chainlink/relayer_factory.go index 6321b575b13..94d0e43403a 100644 --- a/core/services/chainlink/relayer_factory.go +++ b/core/services/chainlink/relayer_factory.go @@ -227,7 +227,10 @@ type CosmosFactoryConfig struct { func (r *RelayerFactory) NewCosmos(ctx context.Context, config CosmosFactoryConfig) (map[relay.ID]cosmos.LoopRelayerChainer, error) { relayers := make(map[relay.ID]cosmos.LoopRelayerChainer) - var lggr = r.Logger.Named("Cosmos") + var ( + lggr = r.Logger.Named("Cosmos") + loopKs = &keystore.CosmosLoopKeystore{Cosmos: config.Keystore} + ) // create one relayer per chain id for _, chainCfg := range config.CosmosConfigs { @@ -237,7 +240,7 @@ func (r *RelayerFactory) NewCosmos(ctx context.Context, config CosmosFactoryConf QueryConfig: r.QConfig, Logger: lggr.Named(relayId.ChainID.String()), DB: r.DB, - KeyStore: config.Keystore, + KeyStore: loopKs, EventBroadcaster: config.EventBroadcaster, } opts.Configs = cosmos.NewConfigs(cosmos.CosmosConfigs{chainCfg}) diff --git a/core/services/keystore/cosmos.go b/core/services/keystore/cosmos.go index 07abafa3efd..c06dfcdbcea 100644 --- a/core/services/keystore/cosmos.go +++ b/core/services/keystore/cosmos.go @@ -1,10 +1,12 @@ package keystore import ( + "context" "fmt" "github.com/pkg/errors" + "github.com/smartcontractkit/chainlink-relay/pkg/loop" "github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/cosmoskey" ) @@ -144,3 +146,38 @@ func (ks *cosmos) getByID(id string) (cosmoskey.Key, error) { } return key, nil } + +// CosmosLoopKeystore implements the [github.com/smartcontractkit/chainlink-relay/pkg/loop.Keystore] interface and +// handles signing for Cosmos messages. +type CosmosLoopKeystore struct { + Cosmos +} + +var _ loop.Keystore = &CosmosLoopKeystore{} + +func (lk *CosmosLoopKeystore) Sign(ctx context.Context, id string, hash []byte) ([]byte, error) { + k, err := lk.Get(id) + if err != nil { + return nil, err + } + // loopp spec requires passing nil hash to check existence of id + if hash == nil { + return nil, nil + } + + return k.ToPrivKey().Sign(hash) +} + +func (lk *CosmosLoopKeystore) Accounts(ctx context.Context) ([]string, error) { + keys, err := lk.GetAll() + if err != nil { + return nil, err + } + + accounts := []string{} + for _, k := range keys { + accounts = append(accounts, k.PublicKeyStr()) + } + + return accounts, nil +} diff --git a/core/services/keystore/keys/cosmoskey/key.go b/core/services/keystore/keys/cosmoskey/key.go index 6783232e8b7..3e516a21ab5 100644 --- a/core/services/keystore/keys/cosmoskey/key.go +++ b/core/services/keystore/keys/cosmoskey/key.go @@ -12,7 +12,6 @@ import ( "github.com/ethereum/go-ethereum/crypto" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - "github.com/cosmos/cosmos-sdk/types" ) var secpSigningAlgo, _ = keyring.NewSigningAlgoFromString(string(hd.Secp256k1Type), []keyring.SignatureAlgo{hd.Secp256k1}) @@ -78,10 +77,8 @@ func (key Key) PublicKey() (pubKey cryptotypes.PubKey) { return key.k.PubKey() } -// PublicKeyStr returns the cosmos address of the public key func (key Key) PublicKeyStr() string { - addr := types.AccAddress(key.k.PubKey().Address()) - return addr.String() + return fmt.Sprintf("%X", key.k.PubKey().Bytes()) } func (key Key) Raw() Raw {