Skip to content

Commit

Permalink
core: use loop.Keystore, support arbitrarily prefixed Cosmos addresses
Browse files Browse the repository at this point in the history
  • Loading branch information
cfal authored and calvwang9 committed Sep 4, 2023
1 parent 11490c6 commit f25affc
Show file tree
Hide file tree
Showing 10 changed files with 306 additions and 103 deletions.
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()

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{}}
}
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
}

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
}
Loading

0 comments on commit f25affc

Please sign in to comment.