2024. 6. 24. 20:41ㆍBlockchain/Cosmos
Cosmos 체인은 모듈 형식으로 구성되어 있으며, 각 모듈 별로 트랜잭션을 구성할 수 있다. 이러한 트랜잭션들은 모듈의 keeper
내에 구현되어 있는 msg-server
에서 비즈니스 로직을 처리하기 위한 input 값 전달 매개체의 역할을 하며, 정상적인 트랜잭션일 시 (e.g. 정상적인 signature 등) 검증과정을 거쳐 최종적으로 state DB에 기록되어 진다.
EVM의 경우 일반적인 transaction 형식으로 코인 전송, 컨트랙트 배포/실행을 수행하지만 Cosmos는 다양한 형식의 트랜잭션(정확히는 트랜잭션 안의 메시지)을 구현할 수 있어 보다 dynamic한 로직을 구축할 수 있다. 해당 글은 Cosmos SDK 기반 체인들이 사용하는 트랜잭션에 대한 전반적인 기능과 형식들을 알아본다.
Proto
앞선 글에서와 같이 Cosmos는 gRPC로 기본적인 통신을 구현하기 때문에 트랜잭션에 포함되는 메시지도 proto파일로 선언한다. 기본적인 IBC transfer 메세지에 대한 proto 파일을 확인해보자.
syntax = "proto3";
package ibc.applications.transfer.v1;
option go_package = "github.com/cosmos/ibc-go/v7/modules/apps/transfer/types";
import "gogoproto/gogo.proto";
import "cosmos/base/v1beta1/coin.proto";
import "ibc/core/client/v1/client.proto";
// Msg defines the ibc/transfer Msg service.
service Msg {
// Transfer defines a rpc handler method for MsgTransfer.
rpc Transfer(MsgTransfer) returns (MsgTransferResponse);
}
// MsgTransfer defines a msg to transfer fungible tokens (i.e Coins) between
// ICS20 enabled chains. See ICS Spec here:
// https://github.com/cosmos/ibc/tree/master/spec/app/ics-020-fungible-token-transfer#data-structures
message MsgTransfer {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
// the port on which the packet will be sent
string source_port = 1 [(gogoproto.moretags) = "yaml:\"source_port\""];
// the channel by which the packet will be sent
string source_channel = 2 [(gogoproto.moretags) = "yaml:\"source_channel\""];
// the tokens to be transferred
cosmos.base.v1beta1.Coin token = 3 [(gogoproto.nullable) = false];
// the sender address
string sender = 4;
// the recipient address on the destination chain
string receiver = 5;
// Timeout height relative to the current block height.
// The timeout is disabled when set to 0.
ibc.core.client.v1.Height timeout_height = 6
[(gogoproto.moretags) = "yaml:\"timeout_height\"", (gogoproto.nullable) = false];
// Timeout timestamp in absolute nanoseconds since unix epoch.
// The timeout is disabled when set to 0.
uint64 timeout_timestamp = 7 [(gogoproto.moretags) = "yaml:\"timeout_timestamp\""];
// optional memo
string memo = 8;
}
// MsgTransferResponse defines the Msg/Transfer response type.
message MsgTransferResponse {
// sequence number of the transfer packet sent
uint64 sequence = 1;
}
IBC v7의 MsgTransfer
의 형식은 위와 같이 proto로 표현된다. 각각의 항목이 어떤것을 뜻하는지 잠깐 파악하고 넘어가본다.
package ibc.applications.transfer.v1
은 proto로 생성할 메시지의 디렉토리가 된다. 참고로 Cosmos에서 제공하는 script를 통해 proto를 사용하여 module/types
폴더 내에 tx.pb.go
를 컴파일 할 때에 메시지 디렉토리처럼 ~/ibc/applications/transfer/v1
내에 proto를 생성하지 않으면 오류가 발생한다. 즉, ibc.applications.transfer.v1
은 메시지를 구분하기 위한 URI라고 생각하면 편하다.
option go_pakage = "github.com/cosmos/ibc-go/v1/modules/apps/transfer/types;"
는 proto 파일을 컴파일하고 생성되는pb.go
파일이 위치할 디렉토리를 지정한다.
servie Msg { rpc Transfer(MsgTransfer) returns (MsgTransferResponse) }
는 메시지를 routing할 function 이름과 해당 function의 인수/결과값을 정의한다.
gRPC로 Transfer
를 호출할 시에 정의된 MsgTransfer
라는 메시지를 입력해야 하고, 모든 로직이 처리된 후에 MsgTransferResponse
라는 결과값이 호출자에게 response된다. Transfer
를 호출함에 따라 Cosmos 자체에서 라우팅 되어 해당 모듈의 keeper(여기서는 IBC keeper)로 인입되게 되고, 전달받은 메시지의 파라미터를 이용하여 로직을 처리한다.
참고로, transaction message의 명칭을 지을때 Msg~~
/Msg~~Response
라고 관용적으로 설정하며, query message의 경우 Query~~Request
/Query~~Response
로 설정하니, 메시지 명칭을 보고 tx인지 query인지 짐작할 수 있다.
Transaction에 들어갈 메시지(MsgTransfer
)는 option값과 함께 사용될 타입을 지정하여 proto file내에 정의한다.
최종적으로 proto 컴파일 후 tx.pb.go
에 ibc.applications.transfer.v1.MsgTransfer
타입의 메시지가 생성된다.
Tx 형식
위 예시로 든 IBC transfer 메시지를 이용하여 tx를 생성하면 아래와 같다.
{
"body":{
"messages":[
{
"@type":"/ibc.applications.transfer.v1.MsgTransfer",
"receiver":"aaa18k44cv3mkewrc2m6r07j3dqnktcnratln8k5sl",
"sender":"bbb18k44cv3mkewrc2m6r07j3dqnktcnratlmledpa",
"source_channel":"channel-0",
"source_port":"transfer",
"timeout_height":{
"revision_height":"0",
"revision_number":"0"
},
"timeout_timestamp":"0",
"token":{
"amount":"10",
"denom":"token"
}
}
],
"memo":"",
"timeout_height":0
},
"auth_info":{
"signer_infos":[
{
"public_key":{
"@type":"/cosmos.crypto.secp256k1.PubKey",
"key":"Ak3StQq9b13P4KBVcoJ5dg9gjyq7PdFuZwhD/3jyPo84"
},
"sequence":1,
"mode_info":{
"single":{
"mode":1
}
}
}
],
"fee":{
"amount":[
{
"amount":"0",
"denom":"token"
}
],
"gas_limit":"1",
"granter":"",
"payer":""
}
},
"signatures":[
"oFuf0FSrIJrkJguSB0FRT+1R+mp3nUHMNOKLfRdXaZ0ZgC4YsSgw+WxXO+YQgFZMEdEsQ3KuciY8P2BM8ag5fw=="
]
}
대부분의 cosmos tx는 위와 같은 형식을 따른다. body 내에 메시지들이 정의되어 있고, 서명 정보와 signature가 포함이 되어 있다.
예시로 든 tx는 실제 체인에서 동작하지 않을것이니 형식만 확인해보자.
가장 먼저 트랜잭션에 포함된 메시지이다. 메시지는 요청자가 수행할 동작을 체인에 파라미터를 전달하기 위헤 생성되며 메시지를 처리하는 로직 상에서 조건을 충족하지 않으면 error를 리턴해줄 것이다.
예를 들어 MsgTrasfer
에 있는 timeout_height
와 timeout_timestamp
는 둘 다 0이 되어서는 안된다. 둘 중 하나라도 IBC transfer를 처리하기 위한 정상적인 값이 입력되어야지만 처리가 가능하다. Keeper 내의 msg-server에서 이러한 파라미터 값들을 확인하고 비정상적일 시 오류를 응답해준다.
auth_info
에는 이 트랜잭션을 전송한 account의 정보와, tx를 수행하기 위한 gas fee가 입력되어야 한다.
signer_info
에는 tx sender의 public key와 sequence, sign mode가 입력된다. Public key는 해당 체인에서 사용하는 type의 public key만 사용해야 한다.(여기서는 cosmos secp256k1 타입, 다른 타입으로는 ethermint eth-secp256k1 타입이 대표적이다.)
Sequence는 account의 tx가 수행될 시점의 sequence가 입력이 되어야 하며, 불일치 할 시 cosmos의 ante-handler에서부터 오류가 발생될것이다.
mode_info
는 signing mode를 지정해야 하며, 일반적인 트랜잭션의 경우 DIRECT
가 필요하다. 해당 DIRECT
모드는 숫자로 1번이다.(다른 모드로는 LEGACY_AMINO_JSON이 존재하며 보통 multisig때 사용된다.)
fee
에는 트랜잭션을 구동할 gas fee를 설정한다. Fee로 설정될 native coin의 denomination과 gasLimit * gasPrice * gasAdjustment로 계산된 fee amount가 포함된다.
signature
에는 unsigned tx에서 tx sender의 private key로 암호화한 결과를 입력한다. 해당 signature를 public key로 복호화 할 시 이상이 없다면 트랜잭션은 처리가 될 준비가 된것이다. Unsigned tx를 암호화 하기 때문에 signature가 포함된 transaction에서 일부의 값을 변경하면(예를 들어 gas limit값을 1 낮추면) 복호화 시 정확한 값이 표출될 수 없기 때문에 에러가 발생하므로, signature 입력은 unsigned tx의 조립이 완료된 후 추가되어야 한다.
Decode tx
위의 json 형태의 tx는 인코딩된 tx를 복호화한 결과이다. 보통 cosmos 체인을 다루다 tx body를 확인 하려하면 대부분 base64 형태의 tx를 보게 될텐데, 이것을 단순히 base64 decoding을 사용하여 원문을 보려하면 깨진 글자만 확인할 수 있을것이다.
Cosmos에서 사용되는 transaction은 자체적인 codec을 사용하여 encoding/decoding을 진행하기 때문에 base64 decoding을 사용하면 안되고 codec을 이용하여 decode를 수행해야 한다.
이 codec은 Cosmos-SDK에서 역시 제공해서, encoding/decoding을 자유롭게 수행할 수 있다. Cosmos-SDK의 client/tx_config.go
에서 TxConfig
를 이용한다.
Decoding 하는 코드는 아래와 같다.
type EncodingConfig struct {
InterfaceRegistry types.InterfaceRegistry
Codec codec.Codec
TxConfig client.TxConfig
Amino *codec.LegacyAmino
}
func MakeEncodingConfig() EncodingConfig {
amino := codec.NewLegacyAmino()
interfaceRegistry := codectypes.NewInterfaceRegistry()
cdc := codec.NewProtoCodec(interfaceRegistry)
txCfg := tx.NewTxConfig(cdc, tx.DefaultSignModes)
return EncodingConfig{
InterfaceRegistry: interfaceRegistry,
Codec: cdc,
TxConfig: txCfg,
Amino: amino,
}
}
func decodeTx() (string, error) {
encodingConfig := MakeEncodingConfig
txbytes, err := base64.StdEncoding.DecodeString(EncodedByteString)
if err != nil {
return "", err
}
tx, err := encodingConfig.TxConfig.TxDecoder()(txbytes)
if err != nil {
return "", err
}
json, err := encodingConfig.TxConfig.TxJSONEncoder()(tx)
if err != nil {
return "", err
}
return string(json), nil
}
base64 형식의 tx data를 decoding하여 byte 형식으로 생성하고, Cosmos-SDK에서 제공하는 TxConfig
를 사용하여 Tx
타입으로 변환하고 TxJSONEncoder
로 jsonByte로 변환하면 사람이 확인할 수 있는 tx body를 얻어 낼 수 있다.
Conclusion
Cosmos SDK 기반 체인들에서 사용되는 transaction 형식과 transaction에 들어가는 데이터에 대해서 간략히 알아보았다. 모든 Cosmos app chain들은 표준처럼 위의 방식대로 트랜잭션을 생성하기 때문에 트랜잭션의 형태를 알고 있다면 각 자신의 로직에 맞게 메시지를 커스터마이징하거나 특정 트랜잭션이 어떤 기능을 수행할 수 있을 시, 어떤 모듈에서 수행되는 트랜잭션인지 빠르게 파악 할 수 있을것이다.
'Blockchain > Cosmos' 카테고리의 다른 글
Evmos의 Ethereum Tx 처리 (1) | 2024.01.23 |
---|---|
Cosmos에서의 gRPC (0) | 2023.09.15 |
Inter-Blockchain Communication 구조와 Relayer (0) | 2023.07.07 |
Cosmos state query 방법 (0) | 2023.06.22 |
Cosmos SDK 기반 체인 다중 validator 환경 구성 (0) | 2023.06.15 |