From 56df3aa52ca6fe6d5b4ca42c4325b142c6663c0f Mon Sep 17 00:00:00 2001 From: Lazar Date: Mon, 20 Jan 2025 15:38:06 +0100 Subject: [PATCH] moving relayer code --- client/client/client.go | 9 +- client/client/tx.go | 33 +- client/config/babylon_config.go | 6 +- client/config/babylon_query_config.go | 2 +- client/relayer/account.go | 75 +++ client/relayer/bech32_hack.go | 28 + client/relayer/broadcast.go | 18 + client/relayer/chain_provider.go | 223 ++++++++ client/relayer/client_wrapper.go | 580 +++++++++++++++++++ client/relayer/codec.go | 117 ++++ client/relayer/feegrant.go | 37 ++ client/relayer/grpc_query.go | 216 ++++++++ client/relayer/keys.go | 266 +++++++++ client/relayer/log.go | 189 +++++++ client/relayer/msg.go | 65 +++ client/relayer/provider.go | 310 +++++++++++ client/relayer/query.go | 143 +++++ client/relayer/tx.go | 770 ++++++++++++++++++++++++++ 18 files changed, 3061 insertions(+), 26 deletions(-) create mode 100644 client/relayer/account.go create mode 100644 client/relayer/bech32_hack.go create mode 100644 client/relayer/broadcast.go create mode 100644 client/relayer/chain_provider.go create mode 100644 client/relayer/client_wrapper.go create mode 100644 client/relayer/codec.go create mode 100644 client/relayer/feegrant.go create mode 100644 client/relayer/grpc_query.go create mode 100644 client/relayer/keys.go create mode 100644 client/relayer/log.go create mode 100644 client/relayer/msg.go create mode 100644 client/relayer/provider.go create mode 100644 client/relayer/query.go create mode 100644 client/relayer/tx.go diff --git a/client/client/client.go b/client/client/client.go index e641c96bc..964915493 100644 --- a/client/client/client.go +++ b/client/client/client.go @@ -2,20 +2,20 @@ package client import ( "context" + relayerclient "github.com/babylonlabs-io/babylon/client/relayer" "time" bbn "github.com/babylonlabs-io/babylon/app" "github.com/babylonlabs-io/babylon/client/config" "github.com/babylonlabs-io/babylon/client/query" rpchttp "github.com/cometbft/cometbft/rpc/client/http" - "github.com/cosmos/relayer/v2/relayer/chains/cosmos" "go.uber.org/zap" ) type Client struct { *query.QueryClient - provider *cosmos.CosmosProvider + provider *relayerclient.CosmosProvider timeout time.Duration logger *zap.Logger cfg *config.BabylonConfig @@ -44,20 +44,19 @@ func New(cfg *config.BabylonConfig, logger *zap.Logger) (*Client, error) { provider, err := cfg.ToCosmosProviderConfig().NewProvider( zapLogger, "", // TODO: set home path - true, "babylon", ) if err != nil { return nil, err } - cp := provider.(*cosmos.CosmosProvider) + cp := provider.(*relayerclient.CosmosProvider) cp.PCfg.KeyDirectory = cfg.KeyDirectory // Create tmp Babylon app to retrieve and register codecs // Need to override this manually as otherwise option from config is ignored encCfg := bbn.GetEncodingConfig() - cp.Cdc = cosmos.Codec{ + cp.Cdc = relayerclient.Codec{ InterfaceRegistry: encCfg.InterfaceRegistry, Marshaler: encCfg.Codec, TxConfig: encCfg.TxConfig, diff --git a/client/client/tx.go b/client/client/tx.go index 891738002..f00822f89 100644 --- a/client/client/tx.go +++ b/client/client/tx.go @@ -3,6 +3,7 @@ package client import ( "context" "fmt" + relayerclient "github.com/babylonlabs-io/babylon/client/relayer" "sync" signingv1beta1 "cosmossdk.io/api/cosmos/tx/signing/v1beta1" @@ -21,16 +22,14 @@ import ( txtypes "github.com/cosmos/cosmos-sdk/types/tx" "github.com/cosmos/cosmos-sdk/types/tx/signing" authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" - "github.com/cosmos/relayer/v2/relayer/chains/cosmos" - pv "github.com/cosmos/relayer/v2/relayer/provider" "go.uber.org/zap" ) // ToProviderMsgs converts a list of sdk.Msg to a list of provider.RelayerMessage -func ToProviderMsgs(msgs []sdk.Msg) []pv.RelayerMessage { - relayerMsgs := []pv.RelayerMessage{} +func ToProviderMsgs(msgs []sdk.Msg) []relayerclient.RelayerMessage { + relayerMsgs := make([]relayerclient.RelayerMessage, 0, len(msgs)) for _, m := range msgs { - relayerMsgs = append(relayerMsgs, cosmos.NewCosmosMessage(m, func(signer string) {})) + relayerMsgs = append(relayerMsgs, relayerclient.NewCosmosMessage(m, func(signer string) {})) } return relayerMsgs } @@ -48,7 +47,7 @@ func (c *Client) SendMsgsToMempool(ctx context.Context, msgs []sdk.Msg) error { if err := retry.Do(func() error { var sendMsgErr error krErr := c.accessKeyWithLock(func() { - sendMsgErr = c.provider.SendMessagesToMempool(ctx, relayerMsgs, "", ctx, []func(*pv.RelayerTxResponse, error){}) + sendMsgErr = c.provider.SendMessagesToMempool(ctx, relayerMsgs, "", ctx, []func(*relayerclient.RelayerTxResponse, error){}) }) if krErr != nil { c.logger.Error("unrecoverable err when submitting the tx, skip retrying", zap.Error(krErr)) @@ -67,21 +66,21 @@ func (c *Client) SendMsgsToMempool(ctx context.Context, msgs []sdk.Msg) error { // ReliablySendMsg reliable sends a message to the chain. // It utilizes a file lock as well as a keyring lock to ensure atomic access. // TODO: needs tests -func (c *Client) ReliablySendMsg(ctx context.Context, msg sdk.Msg, expectedErrors []*errors.Error, unrecoverableErrors []*errors.Error) (*pv.RelayerTxResponse, error) { +func (c *Client) ReliablySendMsg(ctx context.Context, msg sdk.Msg, expectedErrors []*errors.Error, unrecoverableErrors []*errors.Error) (*relayerclient.RelayerTxResponse, error) { return c.ReliablySendMsgs(ctx, []sdk.Msg{msg}, expectedErrors, unrecoverableErrors) } // ReliablySendMsgs reliably sends a list of messages to the chain. // It utilizes a file lock as well as a keyring lock to ensure atomic access. // TODO: needs tests -func (c *Client) ReliablySendMsgs(ctx context.Context, msgs []sdk.Msg, expectedErrors []*errors.Error, unrecoverableErrors []*errors.Error) (*pv.RelayerTxResponse, error) { +func (c *Client) ReliablySendMsgs(ctx context.Context, msgs []sdk.Msg, expectedErrors []*errors.Error, unrecoverableErrors []*errors.Error) (*relayerclient.RelayerTxResponse, error) { var ( - rlyResp *pv.RelayerTxResponse + rlyResp *relayerclient.RelayerTxResponse callbackErr error wg sync.WaitGroup ) - callback := func(rtr *pv.RelayerTxResponse, err error) { + callback := func(rtr *relayerclient.RelayerTxResponse, err error) { rlyResp = rtr callbackErr = err wg.Done() @@ -96,7 +95,7 @@ func (c *Client) ReliablySendMsgs(ctx context.Context, msgs []sdk.Msg, expectedE if err := retry.Do(func() error { var sendMsgErr error krErr := c.accessKeyWithLock(func() { - sendMsgErr = c.provider.SendMessagesToMempool(ctx, relayerMsgs, "", ctx, []func(*pv.RelayerTxResponse, error){callback}) + sendMsgErr = c.provider.SendMessagesToMempool(ctx, relayerMsgs, "", ctx, []func(*relayerclient.RelayerTxResponse, error){callback}) }) if krErr != nil { c.logger.Error("unrecoverable err when submitting the tx, skip retrying", zap.Error(krErr)) @@ -147,9 +146,9 @@ func (c *Client) ReliablySendMsgs(ctx context.Context, msgs []sdk.Msg, expectedE // ReliablySendMsgsWithSigner reliably sends a list of messages to the chain. // It utilizes the signer private key to sign all msgs -func (c *Client) ReliablySendMsgsWithSigner(ctx context.Context, signerAddr sdk.AccAddress, signerPvKey *secp256k1.PrivKey, msgs []sdk.Msg, expectedErrors []*errors.Error, unrecoverableErrors []*errors.Error) (*pv.RelayerTxResponse, error) { +func (c *Client) ReliablySendMsgsWithSigner(ctx context.Context, signerAddr sdk.AccAddress, signerPvKey *secp256k1.PrivKey, msgs []sdk.Msg, expectedErrors []*errors.Error, unrecoverableErrors []*errors.Error) (*relayerclient.RelayerTxResponse, error) { var ( - rlyResp *pv.RelayerTxResponse + rlyResp *relayerclient.RelayerTxResponse callbackErr error wg sync.WaitGroup ) @@ -209,9 +208,9 @@ func (c *Client) SendMessageWithSigner( ctx context.Context, signerAddr sdk.AccAddress, signerPvKey *secp256k1.PrivKey, - relayerMsgs []pv.RelayerMessage, + relayerMsgs []relayerclient.RelayerMessage, ) (result *coretypes.ResultBroadcastTx, err error) { - cMsgs := cosmos.CosmosMsgs(relayerMsgs...) + cMsgs := relayerclient.CosmosMsgs(relayerMsgs...) var ( num, seq uint64 ) @@ -486,11 +485,11 @@ func Sign( // - we do not support cancellation of submitting messages // - the only timeout is the block inclusion timeout i.e block-timeout // TODO: To properly support cancellation we need to expose ctx in our client calls -func (c *Client) InsertBTCSpvProof(ctx context.Context, msg *btcctypes.MsgInsertBTCSpvProof) (*pv.RelayerTxResponse, error) { +func (c *Client) InsertBTCSpvProof(ctx context.Context, msg *btcctypes.MsgInsertBTCSpvProof) (*relayerclient.RelayerTxResponse, error) { return c.ReliablySendMsg(ctx, msg, []*errors.Error{}, []*errors.Error{}) } -func (c *Client) InsertHeaders(ctx context.Context, msg *btclctypes.MsgInsertHeaders) (*pv.RelayerTxResponse, error) { +func (c *Client) InsertHeaders(ctx context.Context, msg *btclctypes.MsgInsertHeaders) (*relayerclient.RelayerTxResponse, error) { return c.ReliablySendMsg(ctx, msg, []*errors.Error{}, []*errors.Error{}) } diff --git a/client/config/babylon_config.go b/client/config/babylon_config.go index 680e5fb1a..e903d9780 100644 --- a/client/config/babylon_config.go +++ b/client/config/babylon_config.go @@ -2,7 +2,7 @@ package config import ( "fmt" - "github.com/cosmos/relayer/v2/relayer/chains/cosmos" + relayerclient "github.com/babylonlabs-io/babylon/client/relayer" "net/url" "os" "path/filepath" @@ -42,8 +42,8 @@ func (cfg *BabylonConfig) Validate() error { return nil } -func (cfg *BabylonConfig) ToCosmosProviderConfig() cosmos.CosmosProviderConfig { - return cosmos.CosmosProviderConfig{ +func (cfg *BabylonConfig) ToCosmosProviderConfig() relayerclient.CosmosProviderConfig { + return relayerclient.CosmosProviderConfig{ Key: cfg.Key, ChainID: cfg.ChainID, RPCAddr: cfg.RPCAddr, diff --git a/client/config/babylon_query_config.go b/client/config/babylon_query_config.go index 5a378f221..ff4026cb4 100644 --- a/client/config/babylon_query_config.go +++ b/client/config/babylon_query_config.go @@ -6,7 +6,7 @@ import ( "time" ) -// BabylonConfig defines configuration for the Babylon query client +// BabylonQueryConfig defines configuration for the Babylon query client type BabylonQueryConfig struct { RPCAddr string `mapstructure:"rpc-addr"` Timeout time.Duration `mapstructure:"timeout"` diff --git a/client/relayer/account.go b/client/relayer/account.go new file mode 100644 index 000000000..b9ca8caba --- /dev/null +++ b/client/relayer/account.go @@ -0,0 +1,75 @@ +package relayerclient + +import ( + "context" + "fmt" + "strconv" + + "github.com/cosmos/cosmos-sdk/client" + sdk "github.com/cosmos/cosmos-sdk/types" + grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +var _ client.AccountRetriever = &CosmosProvider{} + +// GetAccount queries for an account given an address and a block height. An +// error is returned if the query or decoding fails. +func (cc *CosmosProvider) GetAccount(clientCtx client.Context, addr sdk.AccAddress) (client.Account, error) { + account, _, err := cc.GetAccountWithHeight(clientCtx, addr) + return account, err +} + +// GetAccountWithHeight queries for an account given an address. Returns the +// height of the query with the account. An error is returned if the query +// or decoding fails. +func (cc *CosmosProvider) GetAccountWithHeight(_ client.Context, addr sdk.AccAddress) (client.Account, int64, error) { + var header metadata.MD + address, err := cc.EncodeBech32AccAddr(addr) + if err != nil { + return nil, 0, err + } + + queryClient := authtypes.NewQueryClient(cc) + res, err := queryClient.Account(context.Background(), &authtypes.QueryAccountRequest{Address: address}, grpc.Header(&header)) + if err != nil { + return nil, 0, err + } + + blockHeight := header.Get(grpctypes.GRPCBlockHeightHeader) + if l := len(blockHeight); l != 1 { + return nil, 0, fmt.Errorf("unexpected '%s' header length; got %d, expected: %d", grpctypes.GRPCBlockHeightHeader, l, 1) + } + + nBlockHeight, err := strconv.Atoi(blockHeight[0]) + if err != nil { + return nil, 0, fmt.Errorf("failed to parse block height: %w", err) + } + + var acc authtypes.AccountI + if err := cc.Cdc.InterfaceRegistry.UnpackAny(res.Account, &acc); err != nil { + return nil, 0, err + } + + return acc, int64(nBlockHeight), nil +} + +// EnsureExists returns an error if no account exists for the given address else nil. +func (cc *CosmosProvider) EnsureExists(clientCtx client.Context, addr sdk.AccAddress) error { + if _, err := cc.GetAccount(clientCtx, addr); err != nil { + return err + } + return nil +} + +// GetAccountNumberSequence returns sequence and account number for the given address. +// It returns an error if the account couldn't be retrieved from the state. +func (cc *CosmosProvider) GetAccountNumberSequence(clientCtx client.Context, addr sdk.AccAddress) (uint64, uint64, error) { + acc, err := cc.GetAccount(clientCtx, addr) + if err != nil { + return 0, 0, err + } + return acc.GetAccountNumber(), acc.GetSequence(), nil +} diff --git a/client/relayer/bech32_hack.go b/client/relayer/bech32_hack.go new file mode 100644 index 000000000..a888690a9 --- /dev/null +++ b/client/relayer/bech32_hack.go @@ -0,0 +1,28 @@ +package relayerclient + +import ( + "sync" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// This file is cursed and this mutex is too +// you don't want none of this dewey cox. +var sdkConfigMutex sync.Mutex + +// SetSDKContext sets the SDK config to the proper bech32 prefixes. +// Don't use this unless you know what you're doing. +// TODO: :dagger: :knife: :chainsaw: remove this function +func (cc *CosmosProvider) SetSDKContext() func() { + return SetSDKConfigContext(cc.PCfg.AccountPrefix) +} + +// SetSDKConfigContext sets the SDK config to the given bech32 prefixes +func SetSDKConfigContext(prefix string) func() { + sdkConfigMutex.Lock() + sdkConf := sdk.GetConfig() + sdkConf.SetBech32PrefixForAccount(prefix, prefix+"pub") + sdkConf.SetBech32PrefixForValidator(prefix+"valoper", prefix+"valoperpub") + sdkConf.SetBech32PrefixForConsensusNode(prefix+"valcons", prefix+"valconspub") + return sdkConfigMutex.Unlock +} diff --git a/client/relayer/broadcast.go b/client/relayer/broadcast.go new file mode 100644 index 000000000..42605d0e8 --- /dev/null +++ b/client/relayer/broadcast.go @@ -0,0 +1,18 @@ +package relayerclient + +import ( + codectypes "github.com/cosmos/cosmos-sdk/codec/types" +) + +const ( + ErrTimeoutAfterWaitingForTxBroadcast _err = "timed out after waiting for tx to get included in the block" +) + +type _err string + +func (e _err) Error() string { return string(e) } + +// todo(lazar) remove this +type intoAny interface { + AsAny() *codectypes.Any +} diff --git a/client/relayer/chain_provider.go b/client/relayer/chain_provider.go new file mode 100644 index 000000000..a6725ae99 --- /dev/null +++ b/client/relayer/chain_provider.go @@ -0,0 +1,223 @@ +package relayerclient + +import ( + "context" + "fmt" + "time" + + "github.com/cometbft/cometbft/types" + "github.com/cosmos/gogoproto/proto" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + commitmenttypes "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types" + ibcexported "github.com/cosmos/ibc-go/v8/modules/core/exported" + tendermint "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type BroadcastMode string + +const ( + BroadcastModeSingle BroadcastMode = "single" + BroadcastModeBatch BroadcastMode = "batch" +) + +type ProviderConfig interface { + NewProvider(log *zap.Logger, homepath string, chainName string) (ChainProvider, error) + Validate() error + BroadcastMode() BroadcastMode +} + +type RelayerMessage interface { + Type() string + MsgBytes() ([]byte, error) +} + +type RelayerTxResponse struct { + Height int64 + TxHash string + Codespace string + Code uint32 + Data string + Events []RelayerEvent +} + +type RelayerEvent struct { + EventType string + Attributes map[string]string +} + +// loggableEvents is an unexported wrapper type for a slice of RelayerEvent, +// to satisfy the zapcore.ArrayMarshaler interface. +type loggableEvents []RelayerEvent + +// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface. +func (e RelayerEvent) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddString("event_type", e.EventType) + for k, v := range e.Attributes { + enc.AddString("event_attr: "+k, v) + } + return nil +} + +// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface. +func (es loggableEvents) MarshalLogArray(enc zapcore.ArrayEncoder) error { + for _, e := range es { + enc.AppendObject(e) + } + return nil +} + +// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface. +func (r RelayerTxResponse) MarshalLogObject(enc zapcore.ObjectEncoder) error { + enc.AddInt64("height", r.Height) + enc.AddString("tx_hash", r.TxHash) + enc.AddString("codespace", r.Codespace) + enc.AddUint32("code", r.Code) + enc.AddString("data", r.Data) + enc.AddArray("events", loggableEvents(r.Events)) + return nil +} + +type KeyProvider interface { + CreateKeystore(path string) error + KeystoreCreated(path string) bool + AddKey(name string, coinType uint32, signingAlgorithm string) (output *KeyOutput, err error) + UseKey(key string) error + RestoreKey(name, mnemonic string, coinType uint32, signingAlgorithm string) (address string, err error) + ShowAddress(name string) (address string, err error) + ListAddresses() (map[string]string, error) + DeleteKey(name string) error + KeyExists(name string) bool + ExportPrivKeyArmor(keyName string) (armor string, err error) +} + +type ChainProvider interface { + QueryProvider + KeyProvider + + Init(ctx context.Context) error + + SendMessagesToMempool( + ctx context.Context, + msgs []RelayerMessage, + memo string, + asyncCtx context.Context, + asyncCallbacks []func(*RelayerTxResponse, error), + ) error + + ChainName() string + ChainId() string + Type() string + ProviderConfig() ProviderConfig + Key() string + Address() (string, error) + Timeout() string + WaitForNBlocks(ctx context.Context, n int64) error + Sprint(toPrint proto.Message) (string, error) + + SetRpcAddr(rpcAddr string) error +} + +type QueryProvider interface { + BlockTime(ctx context.Context, height int64) (time.Time, error) + QueryTx(ctx context.Context, hashHex string) (*RelayerTxResponse, error) + QueryTxs(ctx context.Context, page, limit int, events []string) ([]*RelayerTxResponse, error) +} + +// KeyOutput contains mnemonic and address of key +type KeyOutput struct { + Mnemonic string `json:"mnemonic" yaml:"mnemonic"` + Address string `json:"address" yaml:"address"` +} + +// TimeoutHeightError is used during packet validation to inform the PathProcessor +// that the current chain height has exceeded the packet height timeout so that +// a MsgTimeout can be assembled for the counterparty chain. +type TimeoutHeightError struct { + latestHeight uint64 + timeoutHeight uint64 +} + +func (t *TimeoutHeightError) Error() string { + return fmt.Sprintf("latest height %d is greater than expiration height: %d", t.latestHeight, t.timeoutHeight) +} + +func NewTimeoutHeightError(latestHeight, timeoutHeight uint64) *TimeoutHeightError { + return &TimeoutHeightError{latestHeight, timeoutHeight} +} + +// TimeoutTimestampError is used during packet validation to inform the PathProcessor +// that current block timestamp has exceeded the packet timestamp timeout so that +// a MsgTimeout can be assembled for the counterparty chain. +type TimeoutTimestampError struct { + latestTimestamp uint64 + timeoutTimestamp uint64 +} + +func (t *TimeoutTimestampError) Error() string { + return fmt.Sprintf("latest block timestamp %d is greater than expiration timestamp: %d", t.latestTimestamp, t.timeoutTimestamp) +} + +func NewTimeoutTimestampError(latestTimestamp, timeoutTimestamp uint64) *TimeoutTimestampError { + return &TimeoutTimestampError{latestTimestamp, timeoutTimestamp} +} + +type TimeoutOnCloseError struct { + msg string +} + +func (t *TimeoutOnCloseError) Error() string { + return fmt.Sprintf("packet timeout on close error: %s", t.msg) +} + +func NewTimeoutOnCloseError(msg string) *TimeoutOnCloseError { + return &TimeoutOnCloseError{msg} +} + +type TendermintIBCHeader struct { + SignedHeader *types.SignedHeader + ValidatorSet *types.ValidatorSet + TrustedValidators *types.ValidatorSet + TrustedHeight clienttypes.Height +} + +func (h TendermintIBCHeader) Height() uint64 { + return uint64(h.SignedHeader.Height) +} + +func (h TendermintIBCHeader) ConsensusState() ibcexported.ConsensusState { + return &tendermint.ConsensusState{ + Timestamp: h.SignedHeader.Time, + Root: commitmenttypes.NewMerkleRoot(h.SignedHeader.AppHash), + NextValidatorsHash: h.SignedHeader.NextValidatorsHash, + } +} + +func (h TendermintIBCHeader) NextValidatorsHash() []byte { + return h.SignedHeader.NextValidatorsHash +} + +func (h TendermintIBCHeader) TMHeader() (*tendermint.Header, error) { + valSet, err := h.ValidatorSet.ToProto() + if err != nil { + return nil, err + } + + trustedVals, err := h.TrustedValidators.ToProto() + if err != nil { + return nil, err + } + + return &tendermint.Header{ + SignedHeader: h.SignedHeader.ToProto(), + ValidatorSet: valSet, + TrustedHeight: h.TrustedHeight, + TrustedValidators: trustedVals, + }, nil +} + +type ExtensionOption struct { + Type string `json:"type"` + Value string `json:"value"` +} diff --git a/client/relayer/client_wrapper.go b/client/relayer/client_wrapper.go new file mode 100644 index 000000000..d350cebc4 --- /dev/null +++ b/client/relayer/client_wrapper.go @@ -0,0 +1,580 @@ +package relayerclient + +import ( + "context" + + "github.com/cometbft/cometbft/abci/types" + cometcrypto "github.com/cometbft/cometbft/crypto" + ced25519 "github.com/cometbft/cometbft/crypto/ed25519" + "github.com/cometbft/cometbft/crypto/merkle" + csecp256k1 "github.com/cometbft/cometbft/crypto/secp256k1" + csr25519 "github.com/cometbft/cometbft/crypto/sr25519" + "github.com/cometbft/cometbft/libs/bytes" + "github.com/cometbft/cometbft/p2p" + "github.com/cometbft/cometbft/proto/tendermint/crypto" + rpcclient "github.com/cometbft/cometbft/rpc/client" + coretypes "github.com/cometbft/cometbft/rpc/core/types" + tmtypes "github.com/cometbft/cometbft/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sltypes "github.com/strangelove-ventures/cometbft-client/abci/types" + "github.com/strangelove-ventures/cometbft-client/client" + slcrypto "github.com/strangelove-ventures/cometbft-client/crypto" + "github.com/strangelove-ventures/cometbft-client/crypto/ed25519" + "github.com/strangelove-ventures/cometbft-client/crypto/secp256k1" + "github.com/strangelove-ventures/cometbft-client/crypto/sr25519" + slbytes "github.com/strangelove-ventures/cometbft-client/libs/bytes" + slclient "github.com/strangelove-ventures/cometbft-client/rpc/client" + coretypes2 "github.com/strangelove-ventures/cometbft-client/rpc/core/types" + types2 "github.com/strangelove-ventures/cometbft-client/types" +) + +// RPCClient wraps our slimmed down CometBFT client and converts the returned types to the upstream CometBFT types. +// This is useful so that it can be used in any function calls that expect the upstream types. +type RPCClient struct { + c *client.Client +} + +func NewRPCClient(c *client.Client) RPCClient { + return RPCClient{c: c} +} + +func (r RPCClient) ABCIInfo(ctx context.Context) (*coretypes.ResultABCIInfo, error) { + res, err := r.c.ABCIInfo(ctx) + if err != nil { + return nil, err + } + + return &coretypes.ResultABCIInfo{ + Response: types.ResponseInfo{ + Data: res.Response.Data, + Version: res.Response.Version, + AppVersion: res.Response.AppVersion, + LastBlockHeight: res.Response.LastBlockHeight, + LastBlockAppHash: res.Response.LastBlockAppHash, + }, + }, nil +} + +func (r RPCClient) ABCIQuery( + ctx context.Context, + path string, + data bytes.HexBytes, +) (*coretypes.ResultABCIQuery, error) { + res, err := r.c.ABCIQuery(ctx, path, slbytes.HexBytes(data)) + if err != nil { + return nil, err + } + + return convertResultABCIQuery(res), nil +} + +func (r RPCClient) ABCIQueryWithOptions( + ctx context.Context, + path string, + data bytes.HexBytes, + opts rpcclient.ABCIQueryOptions, +) (*coretypes.ResultABCIQuery, error) { + o := slclient.ABCIQueryOptions{ + Height: opts.Height, + Prove: opts.Prove, + } + + res, err := r.c.ABCIQueryWithOptions(ctx, path, slbytes.HexBytes(data), o) + if err != nil { + return nil, err + } + + return convertResultABCIQuery(res), nil +} + +func (r RPCClient) BroadcastTxCommit(ctx context.Context, tx tmtypes.Tx) (*coretypes.ResultBroadcastTxCommit, error) { + res, err := r.c.BroadcastTxCommit(ctx, types2.Tx(tx)) + if err != nil { + return nil, err + } + + return &coretypes.ResultBroadcastTxCommit{ + CheckTx: types.ResponseCheckTx{ + Code: res.CheckTx.Code, + Data: res.CheckTx.Data, + Log: res.CheckTx.Log, + Info: res.CheckTx.Info, + GasWanted: res.CheckTx.GasWanted, + GasUsed: res.CheckTx.GasUsed, + Events: convertEvents(res.CheckTx.Events), + Codespace: res.CheckTx.Codespace, + }, + TxResult: types.ExecTxResult{ + Code: res.TxResult.Code, + Data: res.TxResult.Data, + Log: res.TxResult.Log, + Info: res.TxResult.Info, + GasWanted: res.TxResult.GasWanted, + GasUsed: res.TxResult.GasUsed, + Events: convertEvents(res.TxResult.Events), + Codespace: res.TxResult.Codespace, + }, + Hash: bytes.HexBytes(res.Hash), + Height: res.Height, + }, nil +} + +func (r RPCClient) BroadcastTxAsync(ctx context.Context, tx tmtypes.Tx) (*coretypes.ResultBroadcastTx, error) { + res, err := r.c.BroadcastTxAsync(ctx, types2.Tx(tx)) + if err != nil { + return nil, err + } + + return &coretypes.ResultBroadcastTx{ + Code: res.Code, + Data: bytes.HexBytes(res.Data), + Log: res.Log, + Codespace: res.Codespace, + Hash: bytes.HexBytes(res.Hash), + }, nil +} + +func (r RPCClient) BroadcastTxSync(ctx context.Context, tx tmtypes.Tx) (*coretypes.ResultBroadcastTx, error) { + res, err := r.c.BroadcastTxSync(ctx, types2.Tx(tx)) + if err != nil { + return nil, err + } + + return &coretypes.ResultBroadcastTx{ + Code: res.Code, + Data: bytes.HexBytes(res.Data), + Log: res.Log, + Codespace: res.Codespace, + Hash: bytes.HexBytes(res.Hash), + }, nil +} + +func (r RPCClient) Validators( + ctx context.Context, + height *int64, + page, perPage *int, +) (*coretypes.ResultValidators, error) { + res, err := r.c.Validators(ctx, height, page, perPage) + if err != nil { + return nil, err + } + + vals := make([]*tmtypes.Validator, len(res.Validators)) + for i, val := range res.Validators { + vals[i] = &tmtypes.Validator{ + Address: tmtypes.Address(val.Address), + PubKey: convertPubKey(val.PubKey), + VotingPower: val.VotingPower, + ProposerPriority: val.ProposerPriority, + } + } + + return &coretypes.ResultValidators{ + BlockHeight: res.BlockHeight, + Validators: vals, + Count: res.Count, + Total: res.Total, + }, nil +} + +func (r RPCClient) Status(ctx context.Context) (*coretypes.ResultStatus, error) { + res, err := r.c.Status(ctx) + if err != nil { + return nil, err + } + + return &coretypes.ResultStatus{ + NodeInfo: p2p.DefaultNodeInfo{ + ProtocolVersion: p2p.ProtocolVersion{ + P2P: res.NodeInfo.ProtocolVersion.P2P, + Block: res.NodeInfo.ProtocolVersion.Block, + App: res.NodeInfo.ProtocolVersion.App, + }, + DefaultNodeID: p2p.ID(res.NodeInfo.DefaultNodeID), + ListenAddr: res.NodeInfo.ListenAddr, + Network: res.NodeInfo.Network, + Version: res.NodeInfo.Version, + Channels: bytes.HexBytes(res.NodeInfo.Channels), + Moniker: res.NodeInfo.Moniker, + Other: p2p.DefaultNodeInfoOther{ + TxIndex: res.NodeInfo.Other.TxIndex, + RPCAddress: res.NodeInfo.Other.RPCAddress, + }, + }, + SyncInfo: coretypes.SyncInfo{ + LatestBlockHash: bytes.HexBytes(res.SyncInfo.LatestBlockHash), + LatestAppHash: bytes.HexBytes(res.SyncInfo.LatestAppHash), + LatestBlockHeight: res.SyncInfo.LatestBlockHeight, + LatestBlockTime: res.SyncInfo.LatestBlockTime, + EarliestBlockHash: bytes.HexBytes(res.SyncInfo.EarliestBlockHash), + EarliestAppHash: bytes.HexBytes(res.SyncInfo.EarliestAppHash), + EarliestBlockHeight: res.SyncInfo.EarliestBlockHeight, + EarliestBlockTime: res.SyncInfo.EarliestBlockTime, + CatchingUp: res.SyncInfo.CatchingUp, + }, + ValidatorInfo: coretypes.ValidatorInfo{ + Address: bytes.HexBytes(res.ValidatorInfo.Address), + PubKey: convertPubKey(res.ValidatorInfo.PubKey), + VotingPower: res.ValidatorInfo.VotingPower, + }, + }, nil +} + +func (r RPCClient) Block(ctx context.Context, height *int64) (*coretypes.ResultBlock, error) { + res, err := r.c.Block(ctx, height) + if err != nil { + return nil, err + } + + return &coretypes.ResultBlock{ + BlockID: convertBlockID(res.BlockID), + Block: convertBlock(res.Block), + }, nil +} + +func (r RPCClient) BlockByHash(ctx context.Context, hash []byte) (*coretypes.ResultBlock, error) { + res, err := r.c.BlockByHash(ctx, hash) + if err != nil { + return nil, err + } + + return &coretypes.ResultBlock{ + BlockID: convertBlockID(res.BlockID), + Block: convertBlock(res.Block), + }, nil +} + +func (r RPCClient) BlockResults(ctx context.Context, height *int64) (*coretypes.ResultBlockResults, error) { + res, err := r.c.BlockResults(ctx, height) + if err != nil { + return nil, err + } + + txs := make([]*types.ExecTxResult, len(res.TxResponses)) + for i, tx := range res.TxResponses { + txs[i] = &types.ExecTxResult{ + Code: tx.Code, + Data: tx.Data, + Log: tx.Log, + Info: tx.Info, + GasWanted: tx.GasWanted, + GasUsed: tx.GasUsed, + Events: converStringEvents(tx.Events), + Codespace: tx.Codespace, + } + } + + return &coretypes.ResultBlockResults{ + Height: res.Height, + TxsResults: txs, + FinalizeBlockEvents: converStringEvents(res.Events), + ValidatorUpdates: nil, + ConsensusParamUpdates: nil, + AppHash: res.AppHash, + }, nil +} + +func (r RPCClient) BlockchainInfo( + ctx context.Context, + minHeight, maxHeight int64, +) (*coretypes.ResultBlockchainInfo, error) { + res, err := r.c.BlockchainInfo(ctx, minHeight, maxHeight) + if err != nil { + return nil, err + } + + meta := make([]*tmtypes.BlockMeta, len(res.BlockMetas)) + for i, m := range res.BlockMetas { + meta[i] = &tmtypes.BlockMeta{ + BlockID: tmtypes.BlockID{ + Hash: bytes.HexBytes(m.BlockID.Hash), + PartSetHeader: tmtypes.PartSetHeader{ + Total: m.BlockID.PartSetHeader.Total, + Hash: bytes.HexBytes(m.BlockID.PartSetHeader.Hash), + }, + }, + BlockSize: m.BlockSize, + Header: convertHeader(m.Header), + NumTxs: m.NumTxs, + } + } + + return &coretypes.ResultBlockchainInfo{ + LastHeight: res.LastHeight, + BlockMetas: meta, + }, nil +} + +func (r RPCClient) Commit(ctx context.Context, height *int64) (*coretypes.ResultCommit, error) { + res, err := r.c.Commit(ctx, height) + if err != nil { + return nil, err + } + + signatures := make([]tmtypes.CommitSig, len(res.Commit.Signatures)) + for i, sig := range res.Commit.Signatures { + signatures[i] = tmtypes.CommitSig{ + BlockIDFlag: tmtypes.BlockIDFlag(sig.BlockIDFlag), + ValidatorAddress: tmtypes.Address(sig.ValidatorAddress), + Timestamp: sig.Timestamp, + Signature: sig.Signature, + } + } + + header := convertHeader(*res.SignedHeader.Header) + return &coretypes.ResultCommit{ + SignedHeader: tmtypes.SignedHeader{ + Header: &header, + Commit: &tmtypes.Commit{ + Height: res.Commit.Height, + Round: res.Commit.Round, + BlockID: convertBlockID(res.Commit.BlockID), + Signatures: signatures, + }, + }, + CanonicalCommit: res.CanonicalCommit, + }, nil +} + +func (r RPCClient) Tx(ctx context.Context, hash []byte, prove bool) (*coretypes.ResultTx, error) { + res, err := r.c.Tx(ctx, hash, prove) + if err != nil { + return nil, err + } + + return convertResultTx(res), nil +} + +func (r RPCClient) TxSearch( + ctx context.Context, + query string, + prove bool, + page, perPage *int, + orderBy string, +) (*coretypes.ResultTxSearch, error) { + res, err := r.c.TxSearch(ctx, query, prove, page, perPage, orderBy) + if err != nil { + return nil, err + } + + txs := make([]*coretypes.ResultTx, len(res)) + for i, tx := range res { + txs[i] = convertResultTx(tx) + } + + return &coretypes.ResultTxSearch{ + Txs: txs, + TotalCount: len(txs), + }, nil +} + +func (r RPCClient) BlockSearch( + ctx context.Context, + query string, + page, perPage *int, + orderBy string, +) (*coretypes.ResultBlockSearch, error) { + res, err := r.c.BlockSearch(ctx, query, page, perPage, orderBy) + if err != nil { + return nil, err + } + + blocks := make([]*coretypes.ResultBlock, len(res.Blocks)) + for i, block := range res.Blocks { + blocks[i] = &coretypes.ResultBlock{ + BlockID: convertBlockID(block.BlockID), + Block: convertBlock(block.Block), + } + } + + return &coretypes.ResultBlockSearch{ + Blocks: blocks, + TotalCount: res.TotalCount, + }, nil +} + +func convertProofOps(proofOps *sltypes.ProofOps) *crypto.ProofOps { + ops := make([]crypto.ProofOp, len(proofOps.Ops)) + for i, op := range proofOps.Ops { + ops[i] = crypto.ProofOp{ + Type: op.Type, + Key: op.Key, + Data: op.Data, + } + } + + return &crypto.ProofOps{Ops: ops} +} + +func convertEvents(events []sltypes.Event) []types.Event { + evts := make([]types.Event, len(events)) + + for i, evt := range events { + attributes := make([]types.EventAttribute, len(evt.Attributes)) + + for j, attr := range evt.Attributes { + attributes[j] = types.EventAttribute{ + Key: attr.Key, + Value: attr.Value, + Index: attr.Index, + } + } + + evts[i] = types.Event{ + Type: evt.Type, + Attributes: attributes, + } + } + + return evts +} + +func converStringEvents(events sdk.StringEvents) []types.Event { + evts := make([]types.Event, len(events)) + + for i, evt := range events { + attributes := make([]types.EventAttribute, len(evt.Attributes)) + + for j, attr := range evt.Attributes { + attributes[j] = types.EventAttribute{ + Key: attr.Key, + Value: attr.Value, + } + } + + evts[i] = types.Event{ + Type: evt.Type, + Attributes: attributes, + } + } + + return evts +} + +func convertHeader(header types2.Header) tmtypes.Header { + return tmtypes.Header{ + ChainID: header.ChainID, + Height: header.Height, + Time: header.Time, + LastBlockID: tmtypes.BlockID{ + Hash: bytes.HexBytes(header.LastBlockID.Hash), + PartSetHeader: tmtypes.PartSetHeader{ + Total: header.LastBlockID.PartSetHeader.Total, + Hash: bytes.HexBytes(header.LastBlockID.PartSetHeader.Hash), + }, + }, + LastCommitHash: bytes.HexBytes(header.LastCommitHash), + DataHash: bytes.HexBytes(header.DataHash), + ValidatorsHash: bytes.HexBytes(header.ValidatorsHash), + NextValidatorsHash: bytes.HexBytes(header.NextValidatorsHash), + ConsensusHash: bytes.HexBytes(header.ConsensusHash), + AppHash: bytes.HexBytes(header.AppHash), + LastResultsHash: bytes.HexBytes(header.LastResultsHash), + EvidenceHash: bytes.HexBytes(header.EvidenceHash), + ProposerAddress: tmtypes.Address(header.ProposerAddress), + } +} + +func convertBlockID(id types2.BlockID) tmtypes.BlockID { + return tmtypes.BlockID{ + Hash: bytes.HexBytes(id.Hash), + PartSetHeader: tmtypes.PartSetHeader{ + Total: id.PartSetHeader.Total, + Hash: bytes.HexBytes(id.PartSetHeader.Hash), + }, + } +} + +func convertBlock(block *types2.Block) *tmtypes.Block { + signatures := make([]tmtypes.CommitSig, len(block.LastCommit.Signatures)) + for i, sig := range block.LastCommit.Signatures { + signatures[i] = tmtypes.CommitSig{ + BlockIDFlag: tmtypes.BlockIDFlag(sig.BlockIDFlag), + ValidatorAddress: tmtypes.Address(sig.ValidatorAddress), + Timestamp: sig.Timestamp, + Signature: sig.Signature, + } + } + + txs := make([]tmtypes.Tx, len(block.Data.Txs)) + for i, tx := range block.Data.Txs { + txs[i] = tmtypes.Tx(tx) + } + + return &tmtypes.Block{ + Header: convertHeader(block.Header), + Data: tmtypes.Data{ + Txs: txs, + }, + Evidence: tmtypes.EvidenceData{}, + LastCommit: &tmtypes.Commit{ + Height: block.LastCommit.Height, + Round: block.LastCommit.Round, + BlockID: convertBlockID(block.LastCommit.BlockID), + Signatures: signatures, + }, + } +} + +func convertResultABCIQuery(res *coretypes2.ResultABCIQuery) *coretypes.ResultABCIQuery { + var ops *crypto.ProofOps + if res.Response.ProofOps != nil { + ops = convertProofOps(res.Response.ProofOps) + } + + return &coretypes.ResultABCIQuery{ + Response: types.ResponseQuery{ + Code: res.Response.Code, + Log: res.Response.Log, + Info: res.Response.Info, + Index: res.Response.Index, + Key: res.Response.Key, + Value: res.Response.Value, + ProofOps: ops, + Height: res.Response.Height, + Codespace: res.Response.Codespace, + }, + } +} + +func convertResultTx(res *client.TxResponse) *coretypes.ResultTx { + return &coretypes.ResultTx{ + Hash: bytes.HexBytes(res.Hash), + Height: res.Height, + Index: res.Index, + TxResult: types.ExecTxResult{ + Code: res.ExecTx.Code, + Data: res.ExecTx.Data, + Log: res.ExecTx.Log, + Info: res.ExecTx.Info, + GasWanted: res.ExecTx.GasWanted, + GasUsed: res.ExecTx.GasUsed, + Events: converStringEvents(res.ExecTx.Events), + Codespace: res.ExecTx.Codespace, + }, + Tx: tmtypes.Tx(res.Tx), + Proof: tmtypes.TxProof{ + RootHash: bytes.HexBytes(res.Proof.RootHash), + Data: tmtypes.Tx(res.Proof.Data), + Proof: merkle.Proof{ + Total: res.Proof.Proof.Total, + Index: res.Proof.Proof.Index, + LeafHash: res.Proof.Proof.LeafHash, + Aunts: res.Proof.Proof.Aunts, + }, + }, + } +} + +func convertPubKey(pk slcrypto.PubKey) cometcrypto.PubKey { + switch key := pk.(type) { + case ed25519.PubKey: + return ced25519.PubKey(key) + case secp256k1.PubKey: + return csecp256k1.PubKey(key) + case sr25519.PubKey: + return csr25519.PubKey(key) + default: + return nil + } +} diff --git a/client/relayer/codec.go b/client/relayer/codec.go new file mode 100644 index 000000000..60ffc2eb5 --- /dev/null +++ b/client/relayer/codec.go @@ -0,0 +1,117 @@ +package relayerclient + +import ( + feegrant "cosmossdk.io/x/feegrant/module" + "cosmossdk.io/x/tx/signing" + "cosmossdk.io/x/upgrade" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/address" + "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/std" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/tx" + authz "github.com/cosmos/cosmos-sdk/x/authz/module" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/crisis" + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/gov" + govclient "github.com/cosmos/cosmos-sdk/x/gov/client" + "github.com/cosmos/cosmos-sdk/x/mint" + "github.com/cosmos/cosmos-sdk/x/params" + paramsclient "github.com/cosmos/cosmos-sdk/x/params/client" + "github.com/cosmos/cosmos-sdk/x/slashing" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/gogoproto/proto" + "github.com/cosmos/ibc-go/modules/capability" + ibcfee "github.com/cosmos/ibc-go/v8/modules/apps/29-fee" + "github.com/cosmos/ibc-go/v8/modules/apps/transfer" + ibc "github.com/cosmos/ibc-go/v8/modules/core" + + cosmosmodule "github.com/cosmos/relayer/v2/relayer/chains/cosmos/module" + "github.com/cosmos/relayer/v2/relayer/chains/cosmos/stride" + ethermintcodecs "github.com/cosmos/relayer/v2/relayer/codecs/ethermint" + injectivecodecs "github.com/cosmos/relayer/v2/relayer/codecs/injective" +) + +var ModuleBasics = []module.AppModuleBasic{ + auth.AppModuleBasic{}, + authz.AppModuleBasic{}, + bank.AppModuleBasic{}, + capability.AppModuleBasic{}, + // TODO: add osmosis governance proposal types here + // TODO: add other proposal types here + gov.NewAppModuleBasic( + []govclient.ProposalHandler{ + paramsclient.ProposalHandler, + }, + ), + crisis.AppModuleBasic{}, + distribution.AppModuleBasic{}, + feegrant.AppModuleBasic{}, + mint.AppModuleBasic{}, + params.AppModuleBasic{}, + slashing.AppModuleBasic{}, + staking.AppModuleBasic{}, + upgrade.AppModuleBasic{}, + transfer.AppModuleBasic{}, + ibc.AppModuleBasic{}, + cosmosmodule.AppModuleBasic{}, + stride.AppModuleBasic{}, + ibcfee.AppModuleBasic{}, +} + +type Codec struct { + InterfaceRegistry types.InterfaceRegistry + Marshaler codec.Codec + TxConfig client.TxConfig + Amino *codec.LegacyAmino +} + +func MakeCodec(moduleBasics []module.AppModuleBasic, extraCodecs []string, accBech32Prefix, valBech32Prefix string) Codec { + modBasic := module.NewBasicManager(moduleBasics...) + encodingConfig := MakeCodecConfig(accBech32Prefix, valBech32Prefix) + std.RegisterLegacyAminoCodec(encodingConfig.Amino) + std.RegisterInterfaces(encodingConfig.InterfaceRegistry) + modBasic.RegisterLegacyAminoCodec(encodingConfig.Amino) + modBasic.RegisterInterfaces(encodingConfig.InterfaceRegistry) + for _, c := range extraCodecs { + switch c { + case "ethermint": + ethermintcodecs.RegisterInterfaces(encodingConfig.InterfaceRegistry) + encodingConfig.Amino.RegisterConcrete(ðermintcodecs.PubKey{}, ethermintcodecs.PubKeyName, nil) + encodingConfig.Amino.RegisterConcrete(ðermintcodecs.PrivKey{}, ethermintcodecs.PrivKeyName, nil) + case "injective": + injectivecodecs.RegisterInterfaces(encodingConfig.InterfaceRegistry) + encodingConfig.Amino.RegisterConcrete(&injectivecodecs.PubKey{}, injectivecodecs.PubKeyName, nil) + encodingConfig.Amino.RegisterConcrete(&injectivecodecs.PrivKey{}, injectivecodecs.PrivKeyName, nil) + } + } + + return encodingConfig +} + +func MakeCodecConfig(accBech32Prefix, valBech32Prefix string) Codec { + interfaceRegistry, err := types.NewInterfaceRegistryWithOptions(types.InterfaceRegistryOptions{ + ProtoFiles: proto.HybridResolver, + SigningOptions: signing.Options{ + AddressCodec: address.NewBech32Codec(accBech32Prefix), + ValidatorAddressCodec: address.NewBech32Codec(valBech32Prefix), + }, + }) + if err != nil { + panic(err) + } + marshaler := codec.NewProtoCodec(interfaceRegistry) + + done := SetSDKConfigContext(accBech32Prefix) + defer done() + + return Codec{ + InterfaceRegistry: interfaceRegistry, + Marshaler: marshaler, + TxConfig: tx.NewTxConfig(marshaler, tx.DefaultSignModes), + Amino: codec.NewLegacyAmino(), + } +} diff --git a/client/relayer/feegrant.go b/client/relayer/feegrant.go new file mode 100644 index 000000000..758bea17b --- /dev/null +++ b/client/relayer/feegrant.go @@ -0,0 +1,37 @@ +package relayerclient + +// GetTxFeeGrant Get the feegrant params to use for the next TX. If feegrants are not configured for the chain client, the default key will be used for TX signing. +// Otherwise, a configured feegrantee will be chosen for TX signing in round-robin fashion. +func (cc *CosmosProvider) GetTxFeeGrant() (txSignerKey string, feeGranterKeyOrAddr string) { + // By default, we should sign TXs with the ChainClient's default key + txSignerKey = cc.PCfg.Key + + if cc.PCfg.FeeGrants == nil { + return + } + + // Use the ChainClient's configured Feegranter key for the next TX. + feeGranterKeyOrAddr = cc.PCfg.FeeGrants.GranterKeyOrAddr + + // The ChainClient Feegrant configuration has never been verified on chain. + // Don't use Feegrants as it could cause the TX to fail on chain. + if feeGranterKeyOrAddr == "" || cc.PCfg.FeeGrants.BlockHeightVerified <= 0 { + feeGranterKeyOrAddr = "" + return + } + + // Pick the next managed grantee in the list as the TX signer + lastGranteeIdx := cc.PCfg.FeeGrants.GranteeLastSignerIndex + + if lastGranteeIdx >= 0 && lastGranteeIdx <= len(cc.PCfg.FeeGrants.ManagedGrantees)-1 { + txSignerKey = cc.PCfg.FeeGrants.ManagedGrantees[lastGranteeIdx] + cc.PCfg.FeeGrants.GranteeLastSignerIndex = cc.PCfg.FeeGrants.GranteeLastSignerIndex + 1 + + // Restart the round robin at 0 if we reached the end of the list of grantees + if cc.PCfg.FeeGrants.GranteeLastSignerIndex == len(cc.PCfg.FeeGrants.ManagedGrantees) { + cc.PCfg.FeeGrants.GranteeLastSignerIndex = 0 + } + } + + return +} diff --git a/client/relayer/grpc_query.go b/client/relayer/grpc_query.go new file mode 100644 index 000000000..3a426ab50 --- /dev/null +++ b/client/relayer/grpc_query.go @@ -0,0 +1,216 @@ +package relayerclient + +import ( + "context" + "fmt" + "reflect" + "strconv" + "sync" + "time" + + sdkerrors "cosmossdk.io/errors" + abci "github.com/cometbft/cometbft/abci/types" + "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + legacyerrors "github.com/cosmos/cosmos-sdk/types/errors" + grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" + "github.com/cosmos/cosmos-sdk/types/tx" + gogogrpc "github.com/cosmos/gogoproto/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/encoding" + "google.golang.org/grpc/encoding/proto" + "google.golang.org/grpc/mem" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +var _ gogogrpc.ClientConn = &CosmosProvider{} + +var protoCodec = encoding.GetCodecV2(proto.Name) + +// Invoke implements the grpc ClientConn.Invoke method +func (cc *CosmosProvider) Invoke(ctx context.Context, method string, req, reply interface{}, opts ...grpc.CallOption) (err error) { + // Two things can happen here: + // 1. either we're broadcasting a Tx, in which call we call Tendermint's broadcast endpoint directly, + // 2. or we are querying for state, in which case we call ABCI's Querier. + + // In both cases, we don't allow empty request req (it will panic unexpectedly). + if reflect.ValueOf(req).IsNil() { + return sdkerrors.Wrap(legacyerrors.ErrInvalidRequest, "request cannot be nil") + } + + // Case 1. Broadcasting a Tx. + if reqProto, ok := req.(*tx.BroadcastTxRequest); ok { + if !ok { + return sdkerrors.Wrapf(legacyerrors.ErrInvalidRequest, "expected %T, got %T", (*tx.BroadcastTxRequest)(nil), req) + } + resProto, ok := reply.(*tx.BroadcastTxResponse) + if !ok { + return sdkerrors.Wrapf(legacyerrors.ErrInvalidRequest, "expected %T, got %T", (*tx.BroadcastTxResponse)(nil), req) + } + + broadcastRes, err := cc.TxServiceBroadcast(ctx, reqProto) + if err != nil { + return err + } + *resProto = *broadcastRes + return err + } + + // Case 2. Querying state. + inMd, _ := metadata.FromOutgoingContext(ctx) + abciRes, outMd, err := cc.RunGRPCQuery(ctx, method, req, inMd) + if err != nil { + return err + } + + if err = protoCodec.Unmarshal([]mem.Buffer{mem.NewBuffer(&abciRes.Value, nil)}, reply); err != nil { + return err + } + + for _, callOpt := range opts { + header, ok := callOpt.(grpc.HeaderCallOption) + if !ok { + continue + } + + *header.HeaderAddr = outMd + } + + if cc.Cdc.InterfaceRegistry != nil { + return types.UnpackInterfaces(reply, cc.Cdc.Marshaler) + } + + return nil +} + +// NewStream implements the grpc ClientConn.NewStream method +func (cc *CosmosProvider) NewStream(context.Context, *grpc.StreamDesc, string, ...grpc.CallOption) (grpc.ClientStream, error) { + return nil, fmt.Errorf("streaming rpc not supported") +} + +// RunGRPCQuery runs a gRPC query from the clientCtx, given all necessary +// arguments for the gRPC method, and returns the ABCI response. It is used +// to factorize code between client (Invoke) and server (RegisterGRPCServer) +// gRPC handlers. +func (cc *CosmosProvider) RunGRPCQuery(ctx context.Context, method string, req interface{}, md metadata.MD) (abci.ResponseQuery, metadata.MD, error) { + reqBz, err := protoCodec.Marshal(req) + if err != nil { + return abci.ResponseQuery{}, nil, err + } + + // parse height header + if heights := md.Get(grpctypes.GRPCBlockHeightHeader); len(heights) > 0 { + height, err := strconv.ParseInt(heights[0], 10, 64) + if err != nil { + return abci.ResponseQuery{}, nil, err + } + if height < 0 { + return abci.ResponseQuery{}, nil, sdkerrors.Wrapf( + legacyerrors.ErrInvalidRequest, + "client.Context.Invoke: height (%d) from %q must be >= 0", height, grpctypes.GRPCBlockHeightHeader) + } + + } + + height, err := GetHeightFromMetadata(md) + if err != nil { + return abci.ResponseQuery{}, nil, err + } + + prove, err := GetProveFromMetadata(md) + if err != nil { + return abci.ResponseQuery{}, nil, err + } + + abciReq := abci.RequestQuery{ + Path: method, + Data: reqBz.Materialize(), + Height: height, + Prove: prove, + } + + abciRes, err := cc.QueryABCI(ctx, abciReq) + if err != nil { + return abci.ResponseQuery{}, nil, err + } + + // Create header metadata. For now the headers contain: + // - block height + // We then parse all the call options, if the call option is a + // HeaderCallOption, then we manually set the value of that header to the + // metadata. + md = metadata.Pairs(grpctypes.GRPCBlockHeightHeader, strconv.FormatInt(abciRes.Height, 10)) + + return abciRes, md, nil +} + +// TxServiceBroadcast is a helper function to broadcast a Tx with the correct gRPC types +// from the tx service. Calls `clientCtx.BroadcastTx` under the hood. +func (cc *CosmosProvider) TxServiceBroadcast(ctx context.Context, req *tx.BroadcastTxRequest) (*tx.BroadcastTxResponse, error) { + if req == nil || req.TxBytes == nil { + return nil, status.Error(codes.InvalidArgument, "invalid empty tx") + } + + var ( + blockTimeout = defaultBroadcastWaitTimeout + err error + rlyResp *RelayerTxResponse + callbackErr error + wg sync.WaitGroup + ) + + if cc.PCfg.BlockTimeout != "" { + blockTimeout, err = time.ParseDuration(cc.PCfg.BlockTimeout) + if err != nil { + // Did you call Validate() method on CosmosProviderConfig struct + // before coming here? + return nil, err + } + } + + callback := func(rtr *RelayerTxResponse, err error) { + rlyResp = rtr + callbackErr = err + wg.Done() + } + + wg.Add(1) + + if err := cc.broadcastTx(ctx, req.TxBytes, nil, nil, ctx, blockTimeout, []func(*RelayerTxResponse, error){callback}); err != nil { + return nil, err + } + + wg.Wait() + + if callbackErr != nil { + return nil, callbackErr + } + + return &tx.BroadcastTxResponse{ + TxResponse: &sdk.TxResponse{ + Height: rlyResp.Height, + TxHash: rlyResp.TxHash, + Codespace: rlyResp.Codespace, + Code: rlyResp.Code, + Data: rlyResp.Data, + }, + }, nil +} + +func GetHeightFromMetadata(md metadata.MD) (int64, error) { + height := md.Get(grpctypes.GRPCBlockHeightHeader) + if len(height) == 1 { + return strconv.ParseInt(height[0], 10, 64) + } + return 0, nil +} + +func GetProveFromMetadata(md metadata.MD) (bool, error) { + prove := md.Get("x-cosmos-query-prove") + if len(prove) == 1 { + return strconv.ParseBool(prove[0]) + } + return false, nil +} diff --git a/client/relayer/keys.go b/client/relayer/keys.go new file mode 100644 index 000000000..2f92df685 --- /dev/null +++ b/client/relayer/keys.go @@ -0,0 +1,266 @@ +package relayerclient + +import ( + "errors" + "os" + + ckeys "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/go-bip39" + "github.com/cosmos/relayer/v2/relayer/chains/cosmos/keys/sr25519" + "github.com/cosmos/relayer/v2/relayer/codecs/ethermint" + "github.com/cosmos/relayer/v2/relayer/codecs/injective" +) + +const ethereumCoinType = uint32(60) + +var ( + // SupportedAlgorithms defines the list of signing algorithms used on Evmos: + // - secp256k1 (Cosmos) + // - sr25519 (Cosmos) + // - eth_secp256k1 (Ethereum, Injective) + SupportedAlgorithms = keyring.SigningAlgoList{hd.Secp256k1, sr25519.Sr25519, ethermint.EthSecp256k1, injective.EthSecp256k1} + // SupportedAlgorithmsLedger defines the list of signing algorithms used on Evmos for the Ledger device: + // - secp256k1 (Cosmos) + // - sr25519 (Cosmos) + // - eth_secp256k1 (Ethereum, Injective) + SupportedAlgorithmsLedger = keyring.SigningAlgoList{hd.Secp256k1, sr25519.Sr25519, ethermint.EthSecp256k1, injective.EthSecp256k1} +) + +// KeyringAlgoOptions defines a function keys options for the ethereum Secp256k1 curve. +// It supports secp256k1 and eth_secp256k1 keys for accounts. +func KeyringAlgoOptions() keyring.Option { + return func(options *keyring.Options) { + options.SupportedAlgos = SupportedAlgorithms + options.SupportedAlgosLedger = SupportedAlgorithmsLedger + } +} + +// CreateKeystore initializes a new instance of a keyring at the specified path in the local filesystem. +func (cc *CosmosProvider) CreateKeystore(path string) error { + keybase, err := keyring.New(cc.PCfg.ChainID, cc.PCfg.KeyringBackend, cc.PCfg.KeyDirectory, cc.Input, cc.Cdc.Marshaler, KeyringAlgoOptions()) + if err != nil { + return err + } + cc.Keybase = keybase + return nil +} + +// KeystoreCreated returns true if there is an existing keystore instance at the specified path, it returns false otherwise. +func (cc *CosmosProvider) KeystoreCreated(path string) bool { + if _, err := os.Stat(cc.PCfg.KeyDirectory); errors.Is(err, os.ErrNotExist) { + return false + } else if cc.Keybase == nil { + return false + } + return true +} + +// AddKey generates a new mnemonic which is then converted to a private key and BIP-39 HD Path and persists it to the keystore. +// It fails if there is an existing key with the same address. +func (cc *CosmosProvider) AddKey(name string, coinType uint32, signingAlgorithm string) (output *KeyOutput, err error) { + ko, err := cc.KeyAddOrRestore(name, coinType, signingAlgorithm) + if err != nil { + return nil, err + } + return ko, nil +} + +// UseKey Updates config.yaml chain with the specified key. +// It fails config is already using the same key or if the key does not exist +func (cc *CosmosProvider) UseKey(key string) error { + cc.PCfg.Key = key + return nil +} + +// RestoreKey converts a mnemonic to a private key and BIP-39 HD Path and persists it to the keystore. +// It fails if there is an existing key with the same address. +func (cc *CosmosProvider) RestoreKey(name, mnemonic string, coinType uint32, signingAlgorithm string) (address string, err error) { + ko, err := cc.KeyAddOrRestore(name, coinType, signingAlgorithm, mnemonic) + if err != nil { + return "", err + } + return ko.Address, nil +} + +// KeyAddOrRestore either generates a new mnemonic or uses the specified mnemonic and converts it to a private key +// and BIP-39 HD Path which is then persisted to the keystore. It fails if there is an existing key with the same address. +func (cc *CosmosProvider) KeyAddOrRestore(keyName string, coinType uint32, signingAlgorithm string, mnemonic ...string) (*KeyOutput, error) { + var mnemonicStr string + var err error + + var algo keyring.SignatureAlgo + switch signingAlgorithm { + case string(hd.Sr25519Type): + algo = sr25519.Sr25519 + default: + algo = hd.Secp256k1 + } + + if len(mnemonic) > 0 { + mnemonicStr = mnemonic[0] + } else { + mnemonicStr, err = CreateMnemonic() + if err != nil { + return nil, err + } + } + + if coinType == ethereumCoinType { + algo = keyring.SignatureAlgo(ethermint.EthSecp256k1) + for _, codec := range cc.PCfg.ExtraCodecs { + if codec == "injective" { + algo = keyring.SignatureAlgo(injective.EthSecp256k1) + } + } + } + + done := SetSDKConfigContext(cc.PCfg.AccountPrefix) + + info, err := cc.Keybase.NewAccount(keyName, mnemonicStr, "", hd.CreateHDPath(coinType, 0, 0).String(), algo) + if err != nil { + return nil, err + } + + done() + + acc, err := info.GetAddress() + if err != nil { + return nil, err + } + + out, err := cc.EncodeBech32AccAddr(acc) + if err != nil { + return nil, err + } + return &KeyOutput{Mnemonic: mnemonicStr, Address: out}, nil +} + +// ShowAddress retrieves a key by name from the keystore and returns the bech32 encoded string representation of that key. +func (cc *CosmosProvider) ShowAddress(name string) (address string, err error) { + info, err := cc.Keybase.Key(name) + if err != nil { + return "", err + } + acc, err := info.GetAddress() + if err != nil { + return "", nil + } + out, err := cc.EncodeBech32AccAddr(acc) + if err != nil { + return "", err + } + return out, nil +} + +// ListAddresses returns a map of bech32 encoded strings representing all keys currently in the keystore. +func (cc *CosmosProvider) ListAddresses() (map[string]string, error) { + out := map[string]string{} + info, err := cc.Keybase.List() + if err != nil { + return nil, err + } + for _, k := range info { + acc, err := k.GetAddress() + if err != nil { + return nil, err + } + addr, err := cc.EncodeBech32AccAddr(acc) + if err != nil { + return nil, err + } + out[k.Name] = addr + } + return out, nil +} + +// DeleteKey removes a key from the keystore for the specified name. +func (cc *CosmosProvider) DeleteKey(name string) error { + if err := cc.Keybase.Delete(name); err != nil { + return err + } + return nil +} + +// KeyExists returns true if a key with the specified name exists in the keystore, it returns false otherwise. +func (cc *CosmosProvider) KeyExists(name string) bool { + k, err := cc.Keybase.Key(name) + if err != nil { + return false + } + + return k.Name == name + +} + +// ExportPrivKeyArmor returns a private key in ASCII armored format. +// It returns an error if the key does not exist or a wrong encryption passphrase is supplied. +func (cc *CosmosProvider) ExportPrivKeyArmor(keyName string) (armor string, err error) { + return cc.Keybase.ExportPrivKeyArmor(keyName, ckeys.DefaultKeyPass) +} + +// GetKeyAddress returns the account address representation for the currently configured key. +func (cc *CosmosProvider) GetKeyAddress(key string) (sdk.AccAddress, error) { + info, err := cc.Keybase.Key(key) + if err != nil { + return nil, err + } + return info.GetAddress() +} + +// CreateMnemonic generates a new mnemonic. +func CreateMnemonic() (string, error) { + entropySeed, err := bip39.NewEntropy(256) + if err != nil { + return "", err + } + mnemonic, err := bip39.NewMnemonic(entropySeed) + if err != nil { + return "", err + } + return mnemonic, nil +} + +// EncodeBech32AccAddr returns the string bech32 representation for the specified account address. +// It returns an empty sting if the byte slice is 0-length. +// It returns an error if the bech32 conversion fails or the prefix is empty. +func (cc *CosmosProvider) EncodeBech32AccAddr(addr sdk.AccAddress) (string, error) { + return sdk.Bech32ifyAddressBytes(cc.PCfg.AccountPrefix, addr) +} + +func (cc *CosmosProvider) DecodeBech32AccAddr(addr string) (sdk.AccAddress, error) { + return sdk.GetFromBech32(addr, cc.PCfg.AccountPrefix) +} + +func (cc *CosmosProvider) GetKeyAddressForKey(key string) (sdk.AccAddress, error) { + info, err := cc.Keybase.Key(key) + if err != nil { + return nil, err + } + return info.GetAddress() +} + +func (cc *CosmosProvider) KeyFromKeyOrAddress(keyOrAddress string) (string, error) { + switch { + case keyOrAddress == "": + return cc.PCfg.Key, nil + case cc.KeyExists(keyOrAddress): + return keyOrAddress, nil + default: + acc, err := cc.DecodeBech32AccAddr(keyOrAddress) + if err != nil { + return "", err + } + + done := SetSDKConfigContext(cc.PCfg.AccountPrefix) + defer done() + + kr, err := cc.Keybase.KeyByAddress(acc) + if err != nil { + return "", err + } + return kr.Name, nil + } +} diff --git a/client/relayer/log.go b/client/relayer/log.go new file mode 100644 index 000000000..12483e1f8 --- /dev/null +++ b/client/relayer/log.go @@ -0,0 +1,189 @@ +package relayerclient + +import ( + "errors" + "reflect" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + typestx "github.com/cosmos/cosmos-sdk/types/tx" + feetypes "github.com/cosmos/ibc-go/v8/modules/apps/29-fee/types" + transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + chantypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// getChannelsIfPresent scans the events for channel tags +func getChannelsIfPresent(events []RelayerEvent) []zapcore.Field { + channelTags := []string{srcChanTag, dstChanTag} + var fields []zap.Field + + // While a transaction may have multiple messages, we just need to first + // a pair of channels + foundTag := map[string]struct{}{} + + for _, event := range events { + for _, tag := range channelTags { + for attributeKey, attributeValue := range event.Attributes { + if attributeKey == tag { + // Only append the tag once + // TODO: what if they are different? + if _, ok := foundTag[tag]; !ok { + fields = append(fields, zap.String(tag, attributeValue)) + foundTag[tag] = struct{}{} + } + } + } + } + } + return fields +} + +// LogFailedTx takes the transaction and the messages to create it and logs the appropriate data +func (cc *CosmosProvider) LogFailedTx(res *RelayerTxResponse, err error, msgs []RelayerMessage) { + // Include the chain_id + fields := []zapcore.Field{zap.String("chain_id", cc.ChainId())} + + // Extract the channels from the events, if present + if res != nil { + channels := getChannelsIfPresent(res.Events) + fields = append(fields, channels...) + } + fields = append(fields, msgTypesField(msgs)) + + if err != nil { + + if errors.Is(err, chantypes.ErrRedundantTx) { + cc.log.Debug("Redundant message(s)", fields...) + return + } + + // Make a copy since we may continue to the warning + errorFields := append(fields, zap.Error(err)) + cc.log.Error( + "Failed sending cosmos transaction", + errorFields..., + ) + + if res == nil { + return + } + } + + if res.Code != 0 { + if sdkErr := cc.sdkError(res.Codespace, res.Code); err != nil { + fields = append(fields, zap.NamedError("sdk_error", sdkErr)) + } + fields = append(fields, zap.Object("response", res)) + cc.log.Warn( + "Sent transaction but received failure response", + fields..., + ) + } +} + +// LogSuccessTx take the transaction and the messages to create it and logs the appropriate data +func (cc *CosmosProvider) LogSuccessTx(res *sdk.TxResponse, msgs []RelayerMessage) { + // Include the chain_id + fields := []zapcore.Field{zap.String("chain_id", cc.ChainId())} + + // Extract the channels from the events, if present + if res != nil { + events := parseEventsFromTxResponse(res) + fields = append(fields, getChannelsIfPresent(events)...) + } + + // Include the gas used + fields = append(fields, zap.Int64("gas_used", res.GasUsed)) + + // Extract fees and fee_payer if present + cdc := codec.NewProtoCodec(cc.Cdc.InterfaceRegistry) + + var m sdk.Msg + if err := cc.Cdc.InterfaceRegistry.UnpackAny(res.Tx, &m); err == nil { + if tx, ok := m.(*typestx.Tx); ok { + fields = append(fields, zap.Stringer("fees", tx.GetFee())) + if feePayer := getFeePayer(cc.log, cdc, tx); feePayer != "" { + fields = append(fields, zap.String("fee_payer", feePayer)) + } + } else { + cc.log.Debug( + "Failed to convert message to Tx type", + zap.Stringer("type", reflect.TypeOf(m)), + ) + } + } else { + cc.log.Debug("Failed to unpack response Tx into sdk.Msg", zap.Error(err)) + } + + // Include the height, msgType, and tx_hash + fields = append(fields, + zap.Int64("height", res.Height), + msgTypesField(msgs), + zap.String("tx_hash", res.TxHash), + ) + + // Log the successful transaction with fields + cc.log.Info( + "Successful transaction", + fields..., + ) +} + +func msgTypesField(msgs []RelayerMessage) zap.Field { + msgTypes := make([]string, len(msgs)) + for i, m := range msgs { + msgTypes[i] = m.Type() + } + return zap.Strings("msg_types", msgTypes) +} + +// getFeePayer returns the bech32 address of the fee payer of a transaction. +// This uses the fee payer field if set, +// otherwise falls back to the address of whoever signed the first message. +func getFeePayer(log *zap.Logger, cdc *codec.ProtoCodec, tx *typestx.Tx) string { + payer := tx.AuthInfo.Fee.Payer + if payer != "" { + return payer + } + + switch firstMsg := tx.GetMsgs()[0].(type) { + case *transfertypes.MsgTransfer: + // There is a possible data race around concurrent map access + // in the cosmos sdk when it converts the address from bech32. + // We don't need the address conversion; just the sender is all that + // GetSigners is doing under the hood anyway. + return firstMsg.Sender + case *clienttypes.MsgCreateClient: + // Without this particular special case, there is a panic in ibc-go + // due to the sdk config singleton expecting one bech32 prefix but seeing another. + return firstMsg.Signer + case *clienttypes.MsgUpdateClient: + // Same failure mode as MsgCreateClient. + return firstMsg.Signer + case *clienttypes.MsgUpgradeClient: + return firstMsg.Signer + case *clienttypes.MsgSubmitMisbehaviour: + // Same failure mode as MsgCreateClient. + return firstMsg.Signer + case *feetypes.MsgRegisterPayee: + return firstMsg.Relayer + case *feetypes.MsgRegisterCounterpartyPayee: + return firstMsg.Relayer + case *feetypes.MsgPayPacketFee: + return firstMsg.Signer + case *feetypes.MsgPayPacketFeeAsync: + return firstMsg.PacketFee.RefundAddress + default: + signers, _, err := cdc.GetMsgV1Signers(firstMsg) + if err != nil { + log.Info("Could not get signers for msg when attempting to get the fee payer", zap.Error(err)) + return "" + } + + return string(signers[0]) + } +} diff --git a/client/relayer/msg.go b/client/relayer/msg.go new file mode 100644 index 000000000..eec3259f3 --- /dev/null +++ b/client/relayer/msg.go @@ -0,0 +1,65 @@ +package relayerclient + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/gogoproto/proto" + "go.uber.org/zap/zapcore" +) + +type CosmosMessage struct { + Msg sdk.Msg + SetSigner func(string) //callback to update the Msg Signer field + FeegrantDisabled bool //marks whether this message type should ALWAYS disable feegranting (use the default signer) +} + +func NewCosmosMessage(msg sdk.Msg, optionalSetSigner func(string)) RelayerMessage { + return CosmosMessage{ + Msg: msg, + SetSigner: optionalSetSigner, + } +} + +func CosmosMsg(rm RelayerMessage) sdk.Msg { + if val, ok := rm.(CosmosMessage); !ok { + fmt.Printf("got data of type %T but wanted provider.CosmosMessage \n", val) + return nil + } else { + return val.Msg + } +} + +func CosmosMsgs(rm ...RelayerMessage) []sdk.Msg { + sdkMsgs := make([]sdk.Msg, 0) + for _, rMsg := range rm { + if val, ok := rMsg.(CosmosMessage); !ok { + fmt.Printf("got data of type %T but wanted CosmosMessage \n", rMsg) + return nil + } else { + sdkMsgs = append(sdkMsgs, val.Msg) + } + } + return sdkMsgs +} + +func (cm CosmosMessage) Type() string { + return sdk.MsgTypeURL(cm.Msg) +} + +func (cm CosmosMessage) MsgBytes() ([]byte, error) { + return proto.Marshal(cm.Msg) +} + +// MarshalLogObject is used to encode cm to a zap logger with the zap.Object field type. +func (cm CosmosMessage) MarshalLogObject(enc zapcore.ObjectEncoder) error { + // Using plain json.Marshal or calling cm.Msg.String() both fail miserably here. + // There is probably a better way to encode the message than this. + j, err := codec.NewLegacyAmino().MarshalJSON(cm.Msg) + if err != nil { + return err + } + enc.AddByteString("msg_json", j) + return nil +} diff --git a/client/relayer/provider.go b/client/relayer/provider.go new file mode 100644 index 000000000..596080751 --- /dev/null +++ b/client/relayer/provider.go @@ -0,0 +1,310 @@ +package relayerclient + +import ( + "context" + "fmt" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/gogoproto/proto" + commitmenttypes "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types" + "github.com/cosmos/relayer/v2/relayer/codecs/ethermint" + "github.com/strangelove-ventures/cometbft-client/client" + "go.uber.org/zap" + "io" + "os" + "path" + "sync" + "time" +) + +type CosmosProviderConfig struct { + KeyDirectory string `json:"key-directory" yaml:"key-directory"` + Key string `json:"key" yaml:"key"` + ChainName string `json:"-" yaml:"-"` + ChainID string `json:"chain-id" yaml:"chain-id"` + RPCAddr string `json:"rpc-addr" yaml:"rpc-addr"` + AccountPrefix string `json:"account-prefix" yaml:"account-prefix"` + KeyringBackend string `json:"keyring-backend" yaml:"keyring-backend"` + GasAdjustment float64 `json:"gas-adjustment" yaml:"gas-adjustment"` + GasPrices string `json:"gas-prices" yaml:"gas-prices"` + MinGasAmount uint64 `json:"min-gas-amount" yaml:"min-gas-amount"` + MaxGasAmount uint64 `json:"max-gas-amount" yaml:"max-gas-amount"` + Debug bool `json:"debug" yaml:"debug"` + Timeout string `json:"timeout" yaml:"timeout"` + BlockTimeout string `json:"block-timeout" yaml:"block-timeout"` + OutputFormat string `json:"output-format" yaml:"output-format"` + SignModeStr string `json:"sign-mode" yaml:"sign-mode"` + ExtraCodecs []string `json:"extra-codecs" yaml:"extra-codecs"` + Modules []module.AppModuleBasic `json:"-" yaml:"-"` + Slip44 *int `json:"coin-type" yaml:"coin-type"` + SigningAlgorithm string `json:"signing-algorithm" yaml:"signing-algorithm"` + Broadcast BroadcastMode `json:"broadcast-mode" yaml:"broadcast-mode"` + MinLoopDuration time.Duration `json:"min-loop-duration" yaml:"min-loop-duration"` + ExtensionOptions []ExtensionOption `json:"extension-options" yaml:"extension-options"` + + // If FeeGrantConfiguration is set, TXs submitted by the ChainClient will be signed by the FeeGrantees in a round-robin fashion by default. + FeeGrants *FeeGrantConfiguration `json:"feegrants" yaml:"feegrants"` +} + +// FeeGrantConfiguration By default, TXs will be signed by the feegrantees 'ManagedGrantees' keys in a round robin fashion. +// Clients can use other signing keys by invoking 'tx.SendMsgsWith' and specifying the signing key. +type FeeGrantConfiguration struct { + GranteesWanted int `json:"num_grantees" yaml:"num_grantees"` + // Normally this is the default ChainClient key + GranterKeyOrAddr string `json:"granter" yaml:"granter"` + // Whether we control the granter private key (if not, someone else must authorize our feegrants) + IsExternalGranter bool `json:"external_granter" yaml:"external_granter"` + // List of keys (by name) that this FeeGranter manages + ManagedGrantees []string `json:"grantees" yaml:"grantees"` + // Last checked on chain (0 means grants never checked and may not exist) + BlockHeightVerified int64 `json:"block_last_verified" yaml:"block_last_verified"` + // Index of the last ManagedGrantee used as a TX signer + GranteeLastSignerIndex int +} + +type CosmosProvider struct { + log *zap.Logger + + PCfg CosmosProviderConfig + Keybase keyring.Keyring + KeyringOptions []keyring.Option + RPCClient RPCClient + //LightProvider provtypes.Provider + Input io.Reader + Output io.Writer + Cdc Codec + + //nextAccountSeq uint64 + feegrantMu sync.Mutex + + // the map key is the TX signer, which can either be 'default' (provider key) or a feegrantee + // the purpose of the map is to lock on the signer from TX creation through submission, + // thus making TX sequencing errors less likely. + walletStateMap map[string]*WalletState + + // metrics to monitor the provider + TotalFees sdk.Coins + totalFeesMu sync.Mutex + + // for comet < v0.37, decode tm events as base64 todo remove this + cometLegacyEncoding bool + + // for comet < v0.38, use legacy RPC client for ResultsBlockResults todo remove this + cometLegacyBlockResults bool +} + +func (pc CosmosProviderConfig) BroadcastMode() BroadcastMode { + return pc.Broadcast +} + +type WalletState struct { + NextAccountSequence uint64 + Mu sync.Mutex +} + +// NewProvider validates the CosmosProviderConfig, instantiates a ChainClient and then instantiates a CosmosProvider +func (pc CosmosProviderConfig) NewProvider(log *zap.Logger, homepath string, chainName string) (ChainProvider, error) { + if err := pc.Validate(); err != nil { + return nil, err + } + + pc.KeyDirectory = keysDir(homepath, pc.ChainID) + + pc.ChainName = chainName + pc.Modules = append([]module.AppModuleBasic{}, ModuleBasics...) + + if pc.Broadcast == "" { + pc.Broadcast = BroadcastModeBatch + } + + cp := &CosmosProvider{ + log: log, + PCfg: pc, + KeyringOptions: []keyring.Option{ethermint.EthSecp256k1Option()}, + Input: os.Stdin, + Output: os.Stdout, + walletStateMap: map[string]*WalletState{}, + + // TODO: this is a bit of a hack, we should probably have a better way to inject modules + Cdc: MakeCodec(pc.Modules, pc.ExtraCodecs, pc.AccountPrefix, pc.AccountPrefix+"valoper"), + } + + return cp, nil +} + +func (pc CosmosProviderConfig) Validate() error { + if _, err := time.ParseDuration(pc.Timeout); err != nil { + return fmt.Errorf("invalid Timeout: %w", err) + } + return nil +} + +// keysDir returns a string representing the path on the local filesystem where the keystore will be initialized. +func keysDir(home, chainID string) string { + return path.Join(home, "keys", chainID) +} + +func (cc *CosmosProvider) ProviderConfig() ProviderConfig { + return cc.PCfg +} + +func (cc *CosmosProvider) ChainId() string { + return cc.PCfg.ChainID +} + +func (cc *CosmosProvider) ChainName() string { + return cc.PCfg.ChainName +} + +func (cc *CosmosProvider) Type() string { + return "cosmos" +} + +func (cc *CosmosProvider) Key() string { + return cc.PCfg.Key +} + +func (cc *CosmosProvider) Timeout() string { + return cc.PCfg.Timeout +} + +// CommitmentPrefix returns the commitment prefix for Cosmos +func (cc *CosmosProvider) CommitmentPrefix() commitmenttypes.MerklePrefix { + return defaultChainPrefix +} + +// Address returns the chains configured address as a string +func (cc *CosmosProvider) Address() (string, error) { + info, err := cc.Keybase.Key(cc.PCfg.Key) + if err != nil { + return "", err + } + + acc, err := info.GetAddress() + if err != nil { + return "", err + } + + out, err := cc.EncodeBech32AccAddr(acc) + if err != nil { + return "", err + } + + return out, err +} + +func (cc *CosmosProvider) MustEncodeAccAddr(addr sdk.AccAddress) string { + enc, err := cc.EncodeBech32AccAddr(addr) + if err != nil { + panic(err) + } + return enc +} + +// AccountFromKeyOrAddress returns an account from either a key or an address. +// If 'keyOrAddress' is the empty string, this returns the default key's address. +func (cc *CosmosProvider) AccountFromKeyOrAddress(keyOrAddress string) (out sdk.AccAddress, err error) { + switch { + case keyOrAddress == "": + out, err = cc.GetKeyAddress(cc.PCfg.Key) + case cc.KeyExists(keyOrAddress): + out, err = cc.GetKeyAddress(keyOrAddress) + default: + out, err = sdk.GetFromBech32(keyOrAddress, cc.PCfg.AccountPrefix) + } + return +} + +// Sprint returns the json representation of the specified proto message. +func (cc *CosmosProvider) Sprint(toPrint proto.Message) (string, error) { + out, err := cc.Cdc.Marshaler.MarshalJSON(toPrint) + if err != nil { + return "", err + } + return string(out), nil +} + +// SetPCAddr sets the rpc-addr for the chain. +// It will fail if the rpcAddr is invalid(not a url). +func (cc *CosmosProvider) SetRpcAddr(rpcAddr string) error { + cc.PCfg.RPCAddr = rpcAddr + return nil +} + +// Init initializes the keystore, RPC client, amd light client provider. +// Once initialization is complete an attempt to query the underlying node's tendermint version is performed. +// NOTE: Init must be called after creating a new instance of CosmosProvider. +func (cc *CosmosProvider) Init(ctx context.Context) error { + keybase, err := keyring.New( + cc.PCfg.ChainID, + cc.PCfg.KeyringBackend, + cc.PCfg.KeyDirectory, + cc.Input, + cc.Cdc.Marshaler, + cc.KeyringOptions..., + ) + if err != nil { + return err + } + // TODO: figure out how to deal with input or maybe just make all keyring backends test? + + timeout, err := time.ParseDuration(cc.PCfg.Timeout) + if err != nil { + return err + } + + c, err := client.NewClient(cc.PCfg.RPCAddr, timeout) + if err != nil { + return err + } + + //lightprovider, err := prov.New(cc.PCfg.ChainID, cc.PCfg.RPCAddr) todo check if needed + //if err != nil { + // return err + //} + + rpcClient := NewRPCClient(c) + + cc.RPCClient = rpcClient + //cc.LightProvider = lightprovider + cc.Keybase = keybase + + return nil +} + +// WaitForNBlocks blocks until the next block on a given chain +func (cc *CosmosProvider) WaitForNBlocks(ctx context.Context, n int64) error { + var initial int64 + h, err := cc.RPCClient.Status(ctx) + if err != nil { + return err + } + if h.SyncInfo.CatchingUp { + return fmt.Errorf("chain catching up") + } + initial = h.SyncInfo.LatestBlockHeight + for { + h, err = cc.RPCClient.Status(ctx) + if err != nil { + return err + } + if h.SyncInfo.LatestBlockHeight > initial+n { + return nil + } + select { + case <-time.After(10 * time.Millisecond): + // Nothing to do. + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func (cc *CosmosProvider) BlockTime(ctx context.Context, height int64) (time.Time, error) { + resultBlock, err := cc.RPCClient.Block(ctx, &height) + if err != nil { + return time.Time{}, err + } + return resultBlock.Block.Time, nil +} diff --git a/client/relayer/query.go b/client/relayer/query.go new file mode 100644 index 000000000..2459ef28e --- /dev/null +++ b/client/relayer/query.go @@ -0,0 +1,143 @@ +package relayerclient + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + abci "github.com/cometbft/cometbft/abci/types" + "github.com/cosmos/cosmos-sdk/x/params/types/proposal" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "strconv" + "strings" + "time" +) + +func (cc *CosmosProvider) queryParamsSubspaceTime(ctx context.Context, subspace string, key string) (time.Duration, error) { + queryClient := proposal.NewQueryClient(cc) + + params := proposal.QueryParamsRequest{Subspace: subspace, Key: key} + + res, err := queryClient.Params(ctx, ¶ms) + + if err != nil { + return 0, fmt.Errorf("failed to make %s params request: %w", subspace, err) + } + + if res.Param.Value == "" { + return 0, fmt.Errorf("%s %s is empty", subspace, key) + } + + unbondingValue, err := strconv.ParseUint(strings.ReplaceAll(res.Param.Value, `"`, ""), 10, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse %s from %s param: %w", key, subspace, err) + } + + return time.Duration(unbondingValue), nil +} + +// QueryUnbondingPeriod returns the unbonding period of the chain +func (cc *CosmosProvider) QueryUnbondingPeriod(ctx context.Context) (time.Duration, error) { + + // Attempt ICS query + consumerUnbondingPeriod, consumerErr := cc.queryParamsSubspaceTime(ctx, "ccvconsumer", "UnbondingPeriod") + if consumerErr == nil { + return consumerUnbondingPeriod, nil + } + + //Attempt Staking query. + unbondingPeriod, stakingParamsErr := cc.queryParamsSubspaceTime(ctx, "staking", "UnbondingTime") + if stakingParamsErr == nil { + return unbondingPeriod, nil + } + + // Fallback + req := stakingtypes.QueryParamsRequest{} + queryClient := stakingtypes.NewQueryClient(cc) + res, err := queryClient.Params(ctx, &req) + if err == nil { + return res.Params.UnbondingTime, nil + + } + + return 0, + fmt.Errorf("failed to query unbonding period from ccvconsumer, staking & fallback : %w: %s : %s", consumerErr, stakingParamsErr.Error(), err.Error()) +} + +// QueryTx takes a transaction hash and returns the transaction +func (cc *CosmosProvider) QueryTx(ctx context.Context, hashHex string) (*RelayerTxResponse, error) { + hash, err := hex.DecodeString(hashHex) + if err != nil { + return nil, err + } + + resp, err := cc.RPCClient.Tx(ctx, hash, true) + if err != nil { + return nil, err + } + + events := parseEventsFromResponseDeliverTx(resp.TxResult.Events) + + return &RelayerTxResponse{ + Height: resp.Height, + TxHash: string(hash), + Code: resp.TxResult.Code, + Data: string(resp.TxResult.Data), + Events: events, + }, nil +} + +// QueryTxs returns an array of transactions given a tag +func (cc *CosmosProvider) QueryTxs(ctx context.Context, page, limit int, events []string) ([]*RelayerTxResponse, error) { + if len(events) == 0 { + return nil, errors.New("must declare at least one event to search") + } + + if page <= 0 { + return nil, errors.New("page must greater than 0") + } + + if limit <= 0 { + return nil, errors.New("limit must greater than 0") + } + + res, err := cc.RPCClient.TxSearch(ctx, strings.Join(events, " AND "), true, &page, &limit, "") + if err != nil { + return nil, err + } + + // Currently, we only call QueryTxs() in two spots and in both of them we are expecting there to only be, + // at most, one tx in the response. Because of this we don't want to initialize the slice with an initial size. + var txResps []*RelayerTxResponse + for _, tx := range res.Txs { + relayerEvents := parseEventsFromResponseDeliverTx(tx.TxResult.Events) + txResps = append(txResps, &RelayerTxResponse{ + Height: tx.Height, + TxHash: string(tx.Hash), + Code: tx.TxResult.Code, + Data: string(tx.TxResult.Data), + Events: relayerEvents, + }) + } + return txResps, nil +} + +// parseEventsFromResponseDeliverTx parses the events from a ResponseDeliverTx and builds a slice +// of provider.RelayerEvent's. +func parseEventsFromResponseDeliverTx(events []abci.Event) []RelayerEvent { + var rlyEvents []RelayerEvent + + for _, event := range events { + attributes := make(map[string]string) + for _, attribute := range event.Attributes { + attributes[attribute.Key] = attribute.Value + } + + rlyEvents = append(rlyEvents, RelayerEvent{ + EventType: event.Type, + Attributes: attributes, + }) + } + + return rlyEvents +} diff --git a/client/relayer/tx.go b/client/relayer/tx.go new file mode 100644 index 000000000..4deea7c72 --- /dev/null +++ b/client/relayer/tx.go @@ -0,0 +1,770 @@ +package relayerclient + +import ( + "context" + sdkerrors "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + "cosmossdk.io/store/rootmulti" + "errors" + "fmt" + abci "github.com/cometbft/cometbft/abci/types" + client2 "github.com/cometbft/cometbft/rpc/client" + coretypes "github.com/cometbft/cometbft/rpc/core/types" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + legacyerrors "github.com/cosmos/cosmos-sdk/types/errors" + txtypes "github.com/cosmos/cosmos-sdk/types/tx" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/cosmos/relayer/v2/relayer/ethermint" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "math" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "github.com/avast/retry-go/v4" + commitmenttypes "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types" +) + +// Variables used for retries +var ( + rtyAttNum = uint(5) + rtyAtt = retry.Attempts(rtyAttNum) + rtyDel = retry.Delay(time.Millisecond * 400) + rtyErr = retry.LastErrorOnly(true) + accountSeqRegex = regexp.MustCompile("account sequence mismatch, expected ([0-9]+), got ([0-9]+)") + defaultBroadcastWaitTimeout = 10 * time.Minute + errUnknown = "unknown" +) + +// Default IBC settings +var ( + defaultChainPrefix = commitmenttypes.NewMerklePrefix([]byte("ibc")) +) + +// Strings for parsing events +var ( + spTag = "send_packet" + waTag = "write_acknowledgement" + srcChanTag = "packet_src_channel" + dstChanTag = "packet_dst_channel" +) + +var seqGuardSingleton sync.Mutex + +// Gets the sequence guard. If it doesn't exist, initialized and returns it. +func ensureSequenceGuard(cc *CosmosProvider, key string) *WalletState { + seqGuardSingleton.Lock() + defer seqGuardSingleton.Unlock() + + if cc.walletStateMap == nil { + cc.walletStateMap = map[string]*WalletState{} + } + + sequenceGuard, ok := cc.walletStateMap[key] + if !ok { + cc.walletStateMap[key] = &WalletState{} + return cc.walletStateMap[key] + } + + return sequenceGuard +} + +// QueryABCI performs an ABCI query and returns the appropriate response and error sdk error code. +func (cc *CosmosProvider) QueryABCI(ctx context.Context, req abci.RequestQuery) (abci.ResponseQuery, error) { + opts := client2.ABCIQueryOptions{ + Height: req.Height, + Prove: req.Prove, + } + + result, err := cc.RPCClient.ABCIQueryWithOptions(ctx, req.Path, req.Data, opts) + if err != nil { + return abci.ResponseQuery{}, err + } + + if !result.Response.IsOK() { + return abci.ResponseQuery{}, sdkErrorToGRPCError(result.Response) + } + + // data from trusted node or subspace query doesn't need verification + if !opts.Prove || !isQueryStoreWithProof(req.Path) { + return result.Response, nil + } + + return result.Response, nil +} + +// broadcastTx broadcasts a transaction with the given raw bytes and then, in an async goroutine, waits for the tx to be included in the block. +// The wait will end after either the asyncTimeout has run out or the asyncCtx exits. +// If there is no error broadcasting, the asyncCallback will be called with success/failure of the wait for block inclusion. +func (cc *CosmosProvider) broadcastTx( + ctx context.Context, // context for tx broadcast + tx []byte, // raw tx to be broadcasted + msgs []RelayerMessage, // used for logging only + fees sdk.Coins, // used for metrics + + asyncCtx context.Context, // context for async wait for block inclusion after successful tx broadcast + asyncTimeout time.Duration, // timeout for waiting for block inclusion + asyncCallbacks []func(*RelayerTxResponse, error), // callback for success/fail of the wait for block inclusion +) error { + res, err := cc.RPCClient.BroadcastTxSync(ctx, tx) + isErr := err != nil + isFailed := res != nil && res.Code != 0 + if isErr || isFailed { + if isErr && res == nil { + // There are some cases where BroadcastTxSync will return an error but the associated + // ResultBroadcastTx will be nil. + return err + } + rlyResp := &RelayerTxResponse{ + TxHash: res.Hash.String(), + Codespace: res.Codespace, + Code: res.Code, + Data: res.Data.String(), + } + if isFailed { + err = cc.sdkError(res.Codespace, res.Code) + if err == nil { + err = fmt.Errorf("transaction failed to execute: codespace: %s, code: %d, log: %s", res.Codespace, res.Code, res.Log) + } + } + cc.LogFailedTx(rlyResp, err, msgs) + return err + } + + // TODO: maybe we need to check if the node has tx indexing enabled? + // if not, we need to find a new way to block until inclusion in a block + + go cc.waitForTx(asyncCtx, res.Hash, msgs, asyncTimeout, asyncCallbacks) + + return nil +} + +// waitForTx waits for a transaction to be included in a block, logs success/fail, then invokes callback. +// This is intended to be called as an async goroutine. +func (cc *CosmosProvider) waitForTx( + ctx context.Context, + txHash []byte, + msgs []RelayerMessage, // used for logging only + waitTimeout time.Duration, + callbacks []func(*RelayerTxResponse, error), +) { + res, err := cc.waitForBlockInclusion(ctx, txHash, waitTimeout) + if err != nil { + cc.log.Error("Failed to wait for block inclusion", zap.Error(err)) + if len(callbacks) > 0 { + for _, cb := range callbacks { + // Call each callback in order since waitForTx is already invoked asynchronously + cb(nil, err) + } + } + return + } + + rlyResp := &RelayerTxResponse{ + Height: res.Height, + TxHash: res.TxHash, + Codespace: res.Codespace, + Code: res.Code, + Data: res.Data, + Events: parseEventsFromTxResponse(res), + } + + // transaction was executed, log the success or failure using the tx response code + // NOTE: error is nil, logic should use the returned error to determine if the + // transaction was successfully executed. + + if res.Code != 0 { + // Check for any registered SDK errors + err := cc.sdkError(res.Codespace, res.Code) + if err == nil { + err = fmt.Errorf("transaction failed to execute: codespace: %s, code: %d, log: %s", res.Codespace, res.Code, res.RawLog) + } + if len(callbacks) > 0 { + for _, cb := range callbacks { + // Call each callback in order since waitForTx is already invoked asynchronously + cb(nil, err) + } + } + cc.LogFailedTx(rlyResp, nil, msgs) + return + } + + if len(callbacks) > 0 { + for _, cb := range callbacks { + //Call each callback in order since waitForTx is already invoked asyncronously + cb(rlyResp, nil) + } + } + cc.LogSuccessTx(res, msgs) +} + +// waitForBlockInclusion will wait for a transaction to be included in a block, up to waitTimeout or context cancellation. +func (cc *CosmosProvider) waitForBlockInclusion( + ctx context.Context, + txHash []byte, + waitTimeout time.Duration, +) (*sdk.TxResponse, error) { + exitAfter := time.After(waitTimeout) + for { + select { + case <-exitAfter: + return nil, fmt.Errorf("timed out after: %d; %w", waitTimeout, ErrTimeoutAfterWaitingForTxBroadcast) + // This fixed poll is fine because it's only for logging and updating prometheus metrics currently. + case <-time.After(time.Millisecond * 100): + res, err := cc.RPCClient.Tx(ctx, txHash, false) + if err == nil { + return cc.mkTxResult(res) + } + if strings.Contains(err.Error(), "transaction indexing is disabled") { + return nil, fmt.Errorf("cannot determine success/failure of tx because transaction indexing is disabled on rpc url") + } + case <-ctx.Done(): + return nil, ctx.Err() + } + } +} + +// mkTxResult decodes a comet transaction into an SDK TxResponse. +func (cc *CosmosProvider) mkTxResult(resTx *coretypes.ResultTx) (*sdk.TxResponse, error) { + txbz, err := cc.Cdc.TxConfig.TxDecoder()(resTx.Tx) + if err != nil { + return nil, err + } + + p, ok := txbz.(intoAny) + if !ok { + return nil, fmt.Errorf("expecting a type implementing intoAny, got: %T", txbz) + } + + any := p.AsAny() + return sdk.NewResponseResultTx(resTx, any, ""), nil +} + +func sdkErrorToGRPCError(resp abci.ResponseQuery) error { + switch resp.Code { + case legacyerrors.ErrInvalidRequest.ABCICode(): + return status.Error(codes.InvalidArgument, resp.Log) + case legacyerrors.ErrUnauthorized.ABCICode(): + return status.Error(codes.Unauthenticated, resp.Log) + case legacyerrors.ErrKeyNotFound.ABCICode(): + return status.Error(codes.NotFound, resp.Log) + default: + return status.Error(codes.Unknown, resp.Log) + } +} + +// isQueryStoreWithProof expects a format like /// +// queryType must be "store" and subpath must be "key" to require a proof. +func isQueryStoreWithProof(path string) bool { + if !strings.HasPrefix(path, "/") { + return false + } + + paths := strings.SplitN(path[1:], "/", 3) + + switch { + case len(paths) != 3: + return false + case paths[0] != "store": + return false + case rootmulti.RequireProof("/" + paths[2]): + return true + } + + return false +} + +// sdkError will return the Cosmos SDK registered error for a given codespace/code combo if registered, otherwise nil. +func (cc *CosmosProvider) sdkError(codespace string, code uint32) error { + // ABCIError will return an error other than "unknown" if syncRes.Code is a registered error in syncRes.Codespace + // This catches all of the sdk errors https://github.com/cosmos/cosmos-sdk/blob/f10f5e5974d2ecbf9efc05bc0bfe1c99fdeed4b6/types/errors/errors.go + err := errors.Unwrap(sdkerrors.ABCIError(codespace, code, "error broadcasting transaction")) + if err.Error() != errUnknown { + return err + } + return nil +} + +func parseEventsFromTxResponse(resp *sdk.TxResponse) []RelayerEvent { + var events []RelayerEvent + + if resp == nil { + return events + } + + for _, logs := range resp.Logs { + for _, event := range logs.Events { + attributes := make(map[string]string) + for _, attribute := range event.Attributes { + attributes[attribute.Key] = attribute.Value + } + events = append(events, RelayerEvent{ + EventType: event.Type, + Attributes: attributes, + }) + } + } + + // After SDK v0.50, indexed events are no longer provided in the logs on + // transaction execution, the response events can be directly used + if len(events) == 0 { + for _, event := range resp.Events { + attributes := make(map[string]string) + for _, attribute := range event.Attributes { + attributes[attribute.Key] = attribute.Value + } + events = append(events, RelayerEvent{ + EventType: event.Type, + Attributes: attributes, + }) + } + } + + return events +} + +// handleAccountSequenceMismatchError will parse the error string, e.g.: +// "account sequence mismatch, expected 10, got 9: incorrect account sequence" +// and update the next account sequence with the expected value. +func (cc *CosmosProvider) handleAccountSequenceMismatchError(sequenceGuard *WalletState, err error) { + if sequenceGuard == nil { + panic("sequence guard not configured") + } + + matches := accountSeqRegex.FindStringSubmatch(err.Error()) + if len(matches) == 0 { + return + } + nextSeq, err := strconv.ParseUint(matches[1], 10, 64) + if err != nil { + return + } + sequenceGuard.NextAccountSequence = nextSeq +} + +// SendMessagesToMempool simulates and broadcasts a transaction with the given msgs and memo. +// This method will return once the transaction has entered the mempool. +// In an async goroutine, will wait for the tx to be included in the block unless asyncCtx exits. +// If there is no error broadcasting, the asyncCallback will be called with success/failure of the wait for block inclusion. +func (cc *CosmosProvider) SendMessagesToMempool( + ctx context.Context, + msgs []RelayerMessage, + memo string, + + asyncCtx context.Context, + asyncCallbacks []func(*RelayerTxResponse, error), +) error { + txSignerKey, feegranterKeyOrAddr, err := cc.buildSignerConfig(msgs) + if err != nil { + return err + } + + sequenceGuard := ensureSequenceGuard(cc, txSignerKey) + sequenceGuard.Mu.Lock() + defer sequenceGuard.Mu.Unlock() + + txBytes, sequence, fees, err := cc.buildMessages(ctx, msgs, memo, 0, txSignerKey, feegranterKeyOrAddr, sequenceGuard) + if err != nil { + // Account sequence mismatch errors can happen on the simulated transaction also. + if strings.Contains(err.Error(), legacyerrors.ErrWrongSequence.Error()) { + cc.handleAccountSequenceMismatchError(sequenceGuard, err) + } + + return err + } + + if err := cc.broadcastTx(ctx, txBytes, msgs, fees, asyncCtx, defaultBroadcastWaitTimeout, asyncCallbacks); err != nil { + if strings.Contains(err.Error(), legacyerrors.ErrWrongSequence.Error()) { + cc.handleAccountSequenceMismatchError(sequenceGuard, err) + } + + return err + } + + // we had a successful tx broadcast with this sequence, so update it to the next + cc.updateNextAccountSequence(sequenceGuard, sequence+1) + return nil +} + +func (cc *CosmosProvider) updateNextAccountSequence(sequenceGuard *WalletState, seq uint64) { + if seq > sequenceGuard.NextAccountSequence { + sequenceGuard.NextAccountSequence = seq + } +} + +func (cc *CosmosProvider) buildSignerConfig(msgs []RelayerMessage) ( + txSignerKey string, + feegranterKeyOrAddr string, + err error, +) { + // Guard against race conditions when choosing a signer/feegranter + cc.feegrantMu.Lock() + defer cc.feegrantMu.Unlock() + + // Some messages have feegranting disabled. If any message in the TX disables feegrants, then the TX will not be feegranted. + isFeegrantEligible := cc.PCfg.FeeGrants != nil + + for _, curr := range msgs { + if cMsg, ok := curr.(CosmosMessage); ok { + if cMsg.FeegrantDisabled { + isFeegrantEligible = false + } + } + } + + // By default, we should sign TXs with the provider's default key + txSignerKey = cc.PCfg.Key + + if isFeegrantEligible { + txSignerKey, feegranterKeyOrAddr = cc.GetTxFeeGrant() + signerAcc, addrErr := cc.GetKeyAddressForKey(txSignerKey) + if addrErr != nil { + err = addrErr + return + } + + signerAccAddr, encodeErr := cc.EncodeBech32AccAddr(signerAcc) + if encodeErr != nil { + err = encodeErr + return + } + + // Overwrite the 'Signer' field in any Msgs that provide an 'optionalSetSigner' callback + for _, curr := range msgs { + if cMsg, ok := curr.(CosmosMessage); ok { + if cMsg.SetSigner != nil { + cMsg.SetSigner(signerAccAddr) + } + } + } + } + + return +} + +func (cc *CosmosProvider) buildMessages( + ctx context.Context, + msgs []RelayerMessage, + memo string, + gas uint64, + txSignerKey string, + feegranterKeyOrAddr string, + sequenceGuard *WalletState, +) ( + txBytes []byte, + sequence uint64, + fees sdk.Coins, + err error, +) { + done := cc.SetSDKContext() + defer done() + + cMsgs := CosmosMsgs(msgs...) + + txf, err := cc.PrepareFactory(cc.TxFactory(), txSignerKey) + if err != nil { + return nil, 0, sdk.Coins{}, err + } + + if memo != "" { + txf = txf.WithMemo(memo) + } + + sequence = txf.Sequence() + cc.updateNextAccountSequence(sequenceGuard, sequence) + if sequence < sequenceGuard.NextAccountSequence { + sequence = sequenceGuard.NextAccountSequence + txf = txf.WithSequence(sequence) + } + + // Cannot feegrant your own TX + if txSignerKey != feegranterKeyOrAddr && feegranterKeyOrAddr != "" { + var granterAddr sdk.AccAddress + if cc.PCfg.FeeGrants != nil && cc.PCfg.FeeGrants.IsExternalGranter { + granterAddr, err = cc.DecodeBech32AccAddr(feegranterKeyOrAddr) + if err != nil { + return nil, 0, sdk.Coins{}, err + } + } else { + granterAddr, err = cc.GetKeyAddressForKey(feegranterKeyOrAddr) + if err != nil { + return nil, 0, sdk.Coins{}, err + } + } + + txf = txf.WithFeeGranter(granterAddr) + } + + adjusted := gas + + if gas == 0 { + _, adjusted, err = cc.CalculateGas(ctx, txf, txSignerKey, cMsgs...) + + if err != nil { + return nil, 0, sdk.Coins{}, err + } + } + + // Set the gas amount on the transaction factory + txf = txf.WithGas(adjusted) + + // Build the transaction builder + txb, err := txf.BuildUnsignedTx(cMsgs...) + if err != nil { + return nil, 0, sdk.Coins{}, err + } + + if err = tx.Sign(ctx, txf, txSignerKey, txb, false); err != nil { + return nil, 0, sdk.Coins{}, err + } + + tx := txb.GetTx() + fees = tx.GetFee() + + // Generate the transaction bytes + txBytes, err = cc.Cdc.TxConfig.TxEncoder()(tx) + if err != nil { + return nil, 0, sdk.Coins{}, err + } + + return txBytes, txf.Sequence(), fees, nil +} + +// PrepareFactory mutates the tx factory with the appropriate account number, sequence number, and min gas settings. +func (cc *CosmosProvider) PrepareFactory(txf tx.Factory, signingKey string) (tx.Factory, error) { + var ( + err error + from sdk.AccAddress + num, seq uint64 + ) + + // Get key address and retry if fail + if err = retry.Do(func() error { + from, err = cc.GetKeyAddressForKey(signingKey) + if err != nil { + return err + } + return err + }, rtyAtt, rtyDel, rtyErr); err != nil { + return tx.Factory{}, err + } + + cliCtx := client.Context{}.WithClient(cc.RPCClient). + WithInterfaceRegistry(cc.Cdc.InterfaceRegistry). + WithChainID(cc.PCfg.ChainID). + WithCodec(cc.Cdc.Marshaler). + WithFromAddress(from) + + // Set the account number and sequence on the transaction factory and retry if fail + if err = retry.Do(func() error { + if err = txf.AccountRetriever().EnsureExists(cliCtx, from); err != nil { + return err + } + return err + }, rtyAtt, rtyDel, rtyErr); err != nil { + return txf, err + } + + // TODO: why this code? this may potentially require another query when we don't want one + initNum, initSeq := txf.AccountNumber(), txf.Sequence() + if initNum == 0 || initSeq == 0 { + if err = retry.Do(func() error { + num, seq, err = txf.AccountRetriever().GetAccountNumberSequence(cliCtx, from) + if err != nil { + return err + } + return err + }, rtyAtt, rtyDel, rtyErr); err != nil { + return txf, err + } + + if initNum == 0 { + txf = txf.WithAccountNumber(num) + } + + if initSeq == 0 { + txf = txf.WithSequence(seq) + } + } + + if cc.PCfg.MinGasAmount != 0 { + txf = txf.WithGas(cc.PCfg.MinGasAmount) + } + + if cc.PCfg.MaxGasAmount != 0 { + txf = txf.WithGas(cc.PCfg.MaxGasAmount) + } + txf, err = cc.SetWithExtensionOptions(txf) + if err != nil { + return tx.Factory{}, err + } + return txf, nil +} + +// SetWithExtensionOptions sets the dynamic fee extension options on the given +// transaction factory using the configuration options from the CosmosProvider. +// The function creates an extension option for each configuration option and +// serializes it into a byte slice before adding it to the list of extension +// options. The function returns the updated transaction factory with the new +// extension options or an error if the serialization fails or an invalid option +// value is encountered. +func (cc *CosmosProvider) SetWithExtensionOptions(txf tx.Factory) (tx.Factory, error) { + extOpts := make([]*types.Any, 0, len(cc.PCfg.ExtensionOptions)) + for _, opt := range cc.PCfg.ExtensionOptions { + max, ok := sdkmath.NewIntFromString(opt.Value) + if !ok { + return txf, fmt.Errorf("invalid opt value") + } + extensionOption := ethermint.ExtensionOptionDynamicFeeTx{ + MaxPriorityPrice: max, + } + extBytes, err := extensionOption.Marshal() + if err != nil { + return txf, err + } + extOpts = append(extOpts, &types.Any{ + TypeUrl: "/ethermint.types.v1.ExtensionOptionDynamicFeeTx", + Value: extBytes, + }) + } + return txf.WithExtensionOptions(extOpts...), nil +} + +// TxFactory instantiates a new tx factory with the appropriate configuration settings for this chain. +func (cc *CosmosProvider) TxFactory() tx.Factory { + return tx.Factory{}. + WithAccountRetriever(cc). + WithChainID(cc.PCfg.ChainID). + WithTxConfig(cc.Cdc.TxConfig). + WithGasAdjustment(cc.PCfg.GasAdjustment). + WithGasPrices(cc.PCfg.GasPrices). + WithKeybase(cc.Keybase). + WithSignMode(cc.PCfg.SignMode()) +} + +// SignMode returns the SDK sign mode type reflective of the specified sign mode in the config file. +func (pc CosmosProviderConfig) SignMode() signing.SignMode { + signMode := signing.SignMode_SIGN_MODE_UNSPECIFIED + switch pc.SignModeStr { + case "direct": + signMode = signing.SignMode_SIGN_MODE_DIRECT + case "amino-json": + signMode = signing.SignMode_SIGN_MODE_LEGACY_AMINO_JSON + } + return signMode +} + +// CalculateGas simulates a tx to generate the appropriate gas settings before broadcasting a tx. +func (cc *CosmosProvider) CalculateGas(ctx context.Context, txf tx.Factory, signingKey string, msgs ...sdk.Msg) (txtypes.SimulateResponse, uint64, error) { + keyInfo, err := cc.Keybase.Key(signingKey) + if err != nil { + return txtypes.SimulateResponse{}, 0, err + } + + var txBytes []byte + if err := retry.Do(func() error { + var err error + txBytes, err = BuildSimTx(keyInfo, txf, msgs...) + if err != nil { + return err + } + return nil + }, retry.Context(ctx), rtyAtt, rtyDel, rtyErr); err != nil { + return txtypes.SimulateResponse{}, 0, err + } + + simQuery := abci.RequestQuery{ + Path: "/cosmos.tx.v1beta1.Service/Simulate", + Data: txBytes, + } + + var res abci.ResponseQuery + if err := retry.Do(func() error { + var err error + res, err = cc.QueryABCI(ctx, simQuery) + if err != nil { + return err + } + return nil + }, retry.Context(ctx), rtyAtt, rtyDel, rtyErr); err != nil { + return txtypes.SimulateResponse{}, 0, err + } + + var simRes txtypes.SimulateResponse + if err := simRes.Unmarshal(res.Value); err != nil { + return txtypes.SimulateResponse{}, 0, err + } + + gas, err := cc.AdjustEstimatedGas(simRes.GasInfo.GasUsed) + return simRes, gas, err +} + +// BuildSimTx creates an unsigned tx with an empty single signature and returns +// the encoded transaction or an error if the unsigned transaction cannot be built. +func BuildSimTx(info *keyring.Record, txf tx.Factory, msgs ...sdk.Msg) ([]byte, error) { + txb, err := txf.BuildUnsignedTx(msgs...) + if err != nil { + return nil, err + } + + var pk cryptotypes.PubKey = &secp256k1.PubKey{} // use default public key type + + pk, err = info.GetPubKey() + if err != nil { + return nil, err + } + + // Create an empty signature literal as the ante handler will populate with a + // sentinel pubkey. + sig := signing.SignatureV2{ + PubKey: pk, + Data: &signing.SingleSignatureData{ + SignMode: txf.SignMode(), + }, + Sequence: txf.Sequence(), + } + if err := txb.SetSignatures(sig); err != nil { + return nil, err + } + + protoProvider, ok := txb.(protoTxProvider) + if !ok { + return nil, fmt.Errorf("cannot simulate amino tx") + } + + simReq := txtypes.SimulateRequest{Tx: protoProvider.GetProtoTx()} + return simReq.Marshal() +} + +// AdjustEstimatedGas adjusts the estimated gas usage by multiplying it by the gas adjustment factor +// and return estimated gas is higher than max gas error. If the gas usage is zero, the adjusted gas +// is also zero. +func (cc *CosmosProvider) AdjustEstimatedGas(gasUsed uint64) (uint64, error) { + if gasUsed == 0 { + return gasUsed, nil + } + if cc.PCfg.MaxGasAmount > 0 && gasUsed > cc.PCfg.MaxGasAmount { + return 0, fmt.Errorf("estimated gas %d is higher than max gas %d", gasUsed, cc.PCfg.MaxGasAmount) + } + gas := cc.PCfg.GasAdjustment * float64(gasUsed) + if math.IsInf(gas, 1) { + return 0, fmt.Errorf("infinite gas used") + } + return uint64(gas), nil +} + +// protoTxProvider is a type which can provide a proto transaction. It is a +// workaround to get access to the wrapper TxBuilder's method GetProtoTx(). +type protoTxProvider interface { + GetProtoTx() *txtypes.Tx +}