2021/10/16

[Golang]Cosmos TxのAminoエンコードされたバイト配列をデコードする

cosmos-sdkgrpcgolang

概要

最近少しだけCosmos Hub関連のプロジェクトに関わっており、
Cosmos nodeを立てて、RPCリクエストを試したりと・・色々と検証作業を行なっていました。

Cosmos Hubはgaiadというアプリケーションでブロックチェーンサーバーを動かして、そのネットワークで構成されています。

gaiadはブロックチェーンデータの検証・保存の他に、grpcのエンドポイントを(オプションで)公開することができます。
Cosmos Hubを使ったWebアプリケーションの構築には、そのgrpc apiを使ってノードとコミニケーションを取りながら開発していきます。

例えば、送金したいときは、送金用のgrpcエンドポイントに対して送信したいトランザクションのバイト文字列を送信することで実現できます。

そんな中・・・

grpcでは任意の戻り値を表すAnyと呼ばれるタイプがあり、それが曲者です。

Anyは

  • どんな種類のデータかを表すurl文字列
  • 実際のデータ(バイト配列)

を持っています。

戻り値を受け取ったクライアントは、このurlをもとに、バイト配列をうまくデコードして構造体に戻してあげる必要が出てきます。

gaiadgrpcエンドポイントの一つに、ブロックチェーンのブロック情報を取得するエンドポイントがあるのですが、その戻り値のブロックに含まれるトランザクションのデータがこの形式(さらにいうとさらにbase64エンコードされた文字列の形式)で記述されています。
なので・・ブロックの情報からどんなトランザクションが含まれているかを調べるのに、このバイト配列をデコードしてあげる必要があり、その簡易ツールを作成しました。

今回は、そのgrpcのサーバーからの戻り値(レスポンス)に含まれるトランザクションのバイト配列を、構造体に戻してjsonで表示する部分をGoで実装したものになります。

補足

  • このエンコード/デコード方式はAminoといい、proto3で使われているものの一部みたいでした。
  • 今回作ったツールはあくまでも検証用です。
  • gaiadの公開しているgrpcに対して、grpcurlがうまく使えませんでした。どうやらgogo/protobufが悪さをしているようでした。
    • gogo/protobufgrpcurlが内部で利用している、Server Reflectionに対応していないからみたいでした。
    • Rust製のtonicを使って呼び出したら普通に呼び出せました。tonic以外でも、refrectionを使わないプログラムであれば問題なく動作すると思います。

作ったGoコード

dependencies

go.mod

module github.com/your-name/your-app-name

go 1.16

require (
	github.com/cosmos/cosmos-sdk v0.44.1
)

// @see
// * https://github.com/cosmos/gaia/blob/main/go.mod#L121
replace google.golang.org/grpc => google.golang.org/grpc v1.33.2
replace github.com/tendermint/tendermint => github.com/tendermint/tendermint v0.34.13
replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1

codes

以下のような感じになりました。

引数のbase64EncodedStringはbase64エンコードされたトランザクションデータ文字列です。

import (
	"encoding/base64"
	"github.com/cosmos/cosmos-sdk/codec"
	codectypes "github.com/cosmos/cosmos-sdk/codec/types"
	cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
	txtypes "github.com/cosmos/cosmos-sdk/types/tx"
	authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
)


func decodeTx(base64EncodedString string) ([]byte, error) {
	txBytes, err := base64.StdEncoding.DecodeString(base64EncodedString)
	if err != nil {
		return nil, err
	}
	registry := codectypes.NewInterfaceRegistry()
	txtypes.RegisterInterfaces(registry)
	cryptocodec.RegisterInterfaces(registry)
	authtypes.RegisterInterfaces(registry)
	banktypes.RegisterInterfaces(registry)
	cdc := codec.NewProtoCodec(registry)

	var raw txtypes.TxRaw
	err = cdc.Unmarshal(txBytes, &raw)
	if err != nil {
		return nil, err
	}
	var body txtypes.TxBody
	err = cdc.Unmarshal(raw.BodyBytes, &body)
	if err != nil {
		return nil, err
	}
	var authInfo txtypes.AuthInfo
	err = cdc.Unmarshal(raw.AuthInfoBytes, &authInfo)
	if err != nil {
		return nil, err
	}
	theTx := &txtypes.Tx{
		Body:       &body,
		AuthInfo:   &authInfo,
		Signatures: raw.Signatures,
	}

	// encode to json
	return cdc.MarshalJSON(theTx)
}

ちなみに、上記の戻り値を文字列に変換して表示させると以下のようになりました。

base64EncodedString := "Co4BCosBChwvY29zbW9zLmJhbmsudjFiZXRhMS5Nc2dTZW5kEmsKLWNvc21vczE2Nmt2dWZ0eHJqaDU2OXJweDk2a2hrazJoZzNra2Fzc2g5bHoyaxItY29zbW9zMWYyNnh2anUwNGNnYXdrcDB6Y2x3MHBqeWZ3M2tjaHY2c2doeWp4GgsKBXN0YWtlEgIxMBJYClAKRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiEDqjU5/L2oENq1OQeu9mYfs1m55V8LUu5499pFLvGsPRcSBAoCCAEYARIEEMCaDBpA3gzsC3v+EKJAoOMxv4cANNO9/tRBzg7eFXSR5/cHERAJ+i+Yj89A0muH4L9kW3sxRPcvsWhhyUIAirzhq1somg=="
if result, err := decodeTx(base64EncodedString); err != nil {
  log.Fatal(err.Error())
} else {
  fmt.Println(string(result))
}
{
  "body": {
    "messages": [
      {
        "@type": "/cosmos.bank.v1beta1.MsgSend",
        "from_address": "cosmos166kvuftxrjh569rpx96khkk2hg3kkassh9lz2k",
        "to_address": "cosmos1f26xvju04cgawkp0zclw0pjyfw3kchv6sghyjx",
        "amount": [
          {
            "denom": "stake",
            "amount": "10"
          }
        ]
      }
    ],
    "memo": "",
    "timeout_height": "0",
    "extension_options": [],
    "non_critical_extension_options": []
  },
  "auth_info": {
    "signer_infos": [
      {
        "public_key": {
          "@type": "/cosmos.crypto.secp256k1.PubKey",
          "key": "A6o1Ofy9qBDatTkHrvZmH7NZueVfC1LuePfaRS7xrD0X"
        },
        "mode_info": {
          "single": {
            "mode": "SIGN_MODE_DIRECT"
          }
        },
        "sequence": "1"
      }
    ],
    "fee": {
      "amount": [],
      "gas_limit": "200000",
      "payer": "",
      "granter": ""
    }
  },
  "signatures": [
    "3gzsC3v+EKJAoOMxv4cANNO9/tRBzg7eFXSR5/cHERAJ+i+Yj89A0muH4L9kW3sxRPcvsWhhyUIAirzhq1somg=="
  ]
}

以上になります。