Overview
ICS27-GMP is the IBC v2 protocol for general cross-chain execution; its packets carry the payload versionics27-2. A sender on one chain asks a destination chain to execute a payload, and the destination executes it through an account derived deterministically from the packet metadata — no handshake, no per-account channel.
The Go implementation lives in modules/apps/27-gmp. The protocol itself is specified in ICS-027-GMP; read that for the normative rules this module implements. For the EVM counterpart, see ICS27 GMP and accounts.
The module fixes these identifiers (types/keys.go):
How it works
- A sender submits
MsgSendCallwith a source client, a receiver, a payload, a salt and a timeout. Keeper.SendCallbuilds aGMPPacketData, marshals it with the requested encoding, and sends it as an IBC v2 payload ongmpportwith versionics27-2(keeper/msg_server.go).- On the destination chain,
IBCModule.OnRecvPacketvalidates the ports and version, decodes the packet, and hands it toKeeper.OnRecvPacket. - The keeper derives — and, on first use, creates — the GMP account for
(destinationClient, sender, salt), then executes the payload’s messages as that account. - The marshaled
TxMsgDataof the executed messages is returned to the source chain inside anAcknowledgement.
(clientId, sender, salt), so it can be computed before any packet is sent.
Data structures
GMPPacketData
The IBC packet payload, defined in types/packet.go and constructed by types.NewGMPPacketData:
GMPPacketData implements exported.PacketData and exported.PacketDataProvider. GetPacketSender returns Sender; GetCustomPacketData returns Sender as the callback address for the src_callback key — so source callbacks are auto-registered for the sender — and otherwise parses Memo as a JSON object and returns the value under the requested key.
On the Cosmos side Receiver is not used to dispatch the call. The destination keeper executes the messages carried in Payload, and each message names its own handler through the SDK message router.
AccountIdentifier and ICS27Account
AccountIdentifier (from types/account.go) holds ClientId, Sender and Salt. ICS27Account pairs a bech32 Address with the AccountId that produced it. The keeper keeps both directions: Accounts is keyed by the (client, sender, salt) triple and AccountsByAddress by sdk.AccAddress (keeper/keeper.go).
Acknowledgement
A single field, Result []byte (types/ack.go). See Acknowledgements for what the Go module puts in it.
Sending a call
MsgSendCall
MsgSendCall is the only transaction message the module defines. types.NewMsgSendCall constructs it; ValidateBasic (types/msgs.go) enforces:
TimeoutTimestamp is passed through to core IBC v2 unchanged by Keeper.sendPacket, and core IBC v2 treats a packet timeout as absolute seconds since the Unix epoch: modules/core/04-channel/v2/keeper/packet.go converts the client’s nanosecond timestamp with time.Unix(0, ...).Unix() before comparing it, and compares the receive-side timeout against ctx.BlockTime().Unix(). Supply seconds.
The send is also bounded above. Core IBC v2 rejects a packet whose timeout is later than the current block time plus MaxTimeoutDelta, currently 24 hours, with ErrInvalidTimeout. A valid TimeoutTimestamp is therefore greater than 0 and no more than 24 hours past the block time of the transaction that sends it. The check runs when the packet is sent, not in ValidateBasic.
SendCall returns MsgSendCallResponse, whose Sequence is the sequence of the packet that was sent.
The autoCLI command for it is:
encoding is optional (types/autocli.go).
What SendCall does
Keeper.SendCall re-parses Sender as a bech32 address, builds the packet data, runs GMPPacketData.ValidateBasic, and then sends the packet. If Encoding is empty it falls back to types.EncodingABI. The payload is assembled with channeltypesv2.NewPayload(types.PortID, types.PortID, types.Version, encoding, data) and dispatched as a MsgSendPacket through the message router, so both the source and destination port are gmpport.
IBCModule.OnSendPacket re-checks the outgoing packet: both ports must be gmpport, both client IDs must be well formed, the packet data must decode and validate, and the packet data’s Sender must equal the signer of the send. A mismatch is rejected as unauthorized.
Encodings
The three encoding constants are defined intypes/packet.go:
EncodingABI is the default when MsgSendCall.Encoding is empty. It encodes GMPPacketData and Acknowledgement as Solidity ABI tuples so an EVM counterparty can decode them; the encoder and decoder are plain Go, in types/solidity_abi.go, built on go-ethereum’s accounts/abi package. GMPPacketData encodes as a single tuple of (string sender, string receiver, bytes salt, bytes payload, string memo) and Acknowledgement as a tuple of (bytes result). For what the counterparty does with those bytes, see ICS27 GMP and accounts.
UnmarshalPacketData and UnmarshalAcknowledgement re-marshal whatever they decoded and reject the input if the bytes do not round-trip exactly. Protobuf decoding additionally rejects unknown fields. This means an encoder that pads, reorders or adds fields will fail on receipt even though the data is nominally parseable.
GMP account derivation
types.BuildAddressPredictable derives the account address from an AccountIdentifier. It validates the client ID, requires a non-empty sender, then builds a key by concatenating each of ClientId, Sender and Salt prefixed with its own length as a big-endian uint64:
address.Module("gmp-accounts", key), truncated to types.AccountAddrLen (32) bytes.
Because the length prefixes are explicit, distinct triples cannot collide by concatenation. The clientId is always the destination client — the identifier on the chain deriving the account, taken from the received packet, never from the sender.
Keeper.getOrCreateICS27Account creates the account on first receipt: if nothing is stored for the triple, it computes the address, adopts an existing SDK account at that address if there is one, otherwise creates one via NewAccountWithAddress, and records it under both store keys.
Keeper.GetOrComputeICS27Address is the read-only counterpart — it returns the stored address if the account exists, and otherwise the computed one, without touching the store.
Queries
Two gRPC queries are exposed (keeper/query_server.go):
AccountAddress takes the salt as a hex string and decodes it with hex.DecodeString. AccountIdentifier only answers for accounts that have already been created.
Receiving a call
IBCModule.OnRecvPacket (ibc_module.go) returns a failure RecvPacketResult — not an error — whenever any of these fails:
- either port on the payload is not
gmpport - the payload version is not
ics27-2 - the packet data does not decode under the payload’s encoding, or does not round-trip
GMPPacketData.ValidateBasicfailsKeeper.OnRecvPacketreturns an error- the acknowledgement cannot be marshaled
ics27_gmp_recv_packet with success=false and the error string.
Keeper.OnRecvPacket (keeper/relay.go) then:
- Builds the
AccountIdentifierfrom the destination client, the packet’sSenderand itsSalt. - Gets or creates the ICS27 account and loads it from the account keeper.
- Calls
executeTxwith the packet’sPayload.
executeTx deserializes the payload as a CosmosTx — protobuf first, falling back to protojson, which must re-marshal to the same JSON — and then:
- rejects an empty message list;
- requires every message to have exactly one signer — a different count is
ErrInvalidPayload— and requires that signer to equal the derived GMP account, a mismatch beingibcerrors.ErrUnauthorized; - runs each message’s
ValidateBasicwhere implemented; - routes each message through the SDK message router inside a cached context, requiring exactly one non-nil response per message;
- writes the cache only if every message succeeded, so the payload executes atomically.
sdk.TxMsgData holding one Any response per message, proto-marshaled.
Payload shape on a Cosmos destination
A Cosmos destination expectsPayload to be a serialized CosmosTx containing the messages to execute, with the GMP account as the signer of each. types.SerializeCosmosTx builds one from a slice of proto.Message; types.DeserializeCosmosTx is what the keeper uses on the way in. Because the signer check compares against the derived address, the sender must compute the account address — with the query above, or by reimplementing the derivation — before building the payload.
Acknowledgements
On success the module wraps the proto-marshaledsdk.TxMsgData in types.NewAcknowledgement(result) and marshals it with the same encoding the packet used. So Acknowledgement.Result is not the return value of a single call: it is a TxMsgData whose MsgResponses are the responses of the messages in the payload, in order.
On failure, the module returns PacketStatus_Failure and core IBC writes the error acknowledgement; the GMP module does not construct one itself.
IBCModule.OnAcknowledgementPacket and OnTimeoutPacket are both no-ops in this module. To act on an acknowledgement or a timeout, wrap the GMP IBC module in the callbacks v2 middleware; IBCModule.UnmarshalPacketData implements api.PacketDataUnmarshaler for exactly that purpose, and GetCustomPacketData registers the packet sender as the source callback address.
Events
Defined intypes/events.go and emitted from internal/events:
Both carry
sender, receiver, salt, payload, memo, source_client, destination_client, source_port, destination_port and sequence; salt and payload are base64-encoded. ics27_gmp_recv_packet adds success, and error when the receive failed. Each is accompanied by a message event with module=gmp.
Errors
types/errors.go registers, under codespace gmp:
ErrInvalidCodec, ErrInvalidMemo, ErrInvalidSalt, ErrInvalidPayload, ErrInvalidTimeoutTimestamp, ErrInvalidEncoding, ErrAbiDecoding, ErrAbiEncoding, ErrAccountAlreadyExists, ErrAccountNotFound, ErrInvalidMsgRoute, ErrInvalidVersion.
ErrInvalidMsgRoute is the one most likely to surprise an integrator: it means the destination chain has no handler registered for a message type in the payload.
Integrating the module
keeper.NewKeeper takes the codec, a KVStoreService, an AccountKeeper, a MessageRouter and an authority string; it panics on an empty authority (keeper/keeper.go).
gmptypes.StoreKey, add gmp.NewAppModule(app.GMPKeeper) to the module manager, and route the IBC v2 port:
AccountKeeper dependency is narrow — NewAccountWithAddress, GetAccount, SetAccount (types/expected_keepers.go).
Genesis
GenesisState carries Ics27Accounts, a list of RegisteredICS27Account. Validation requires each account address and each AccountId.Sender to parse as bech32, each AccountId.ClientId to be a valid client identifier, and each salt to be within the 32-byte limit (types/genesis.go). The default genesis state is an empty list.
See also
- ICS-027-GMP specification
- ICS27 GMP and accounts — the EVM implementation
- Callbacks middleware — for acting on acknowledgements and timeouts