Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: use loop.Keystore, support arbitrarily prefixed Cosmos addresses #10416

Merged
merged 4 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions core/chains/cosmos/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions core/chains/cosmos/cosmostxm/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -28,3 +29,16 @@ 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()
err := ka.updateMappingLocked()
if err != nil {
ka.mutex.Unlock()
return nil, err
}
addresses := maps.Keys(ka.addressToPubKey)
ka.mutex.Unlock()
cfal marked this conversation as resolved.
Show resolved Hide resolved

return addresses, nil
}
70 changes: 38 additions & 32 deletions core/chains/cosmos/cosmostxm/key_wrapper.go
Original file line number Diff line number Diff line change
@@ -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{}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats the reasoning behind empty pubkey vs panicking?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reasoning was that we don't want the node/process to crash - for example, if account was mistyped. addresses can be specified via TOML for transmitter address, although i didn't bother to trace the code path to see if user input could actually end up here.

}
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 "<redacted>"
}

// Type nop
func (k KeyWrapper) Type() string {
return ""
func (a *KeyWrapper) ProtoMessage() {
// no-op
}
129 changes: 129 additions & 0 deletions core/chains/cosmos/cosmostxm/keystore_adapter.go
Original file line number Diff line number Diff line change
@@ -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
cfal marked this conversation as resolved.
Show resolved Hide resolved
}

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]
cfal marked this conversation as resolved.
Show resolved Hide resolved
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
}
Loading