🐸

Tron ネットワークにおけるトランザクション手数料の求め方

2023/10/30に公開

この記事は株式会社 Ginco のテックブログとして書いています。

ネットワークを利用する際の手数料はトランザクション処理やネットワークのセキュリティを維持するために必要であり、ユーザーが優先順位を選択する仕組みです。この仕組みを読み解くことは、トランザクションの適切な手数料を設定ができるようになることだけではなく、ブロックチェーンを理解することにも繋がります。この記事では「Tron」ブロックチェーンを題材に、その特徴的な手数料の計算メカニズムを読み解いていきます。手数料の詳細な計算をするにはトランザクションのバイト数を求めることが必須になりますが、記事の長さの都合上詳細な手数料の求め方については補足という形で別の記事で触れます。

はじめに

Tronにおいて適切なトランザクション手数料を設定するには、まずはTronネットワークのリソースモデルであるBandwidthを理解することが重要になります。その後に手数料計算について詳しく見ていきます。

前提:Bandwidth とは

Bandwidth とはTronネットワーク独自のリソースモデルの一つで、あらゆるタイプのトランザクションを実行する際に消費されるリソースです。BandwidthはEthereumネットワーク上のGasのようなものであり、アカウント毎に制限が設定されております。そしてその制限内であればユーザーは自由にトランザクションを送信することができます。

トランザクションはバイト配列の形でネットワークに送られ、1バイトにつき1 Bandwidthを消費するため、トランザクションのバイト数とBandwidthは等しくなります。そのため、Bandwidth は手数料がかからずに送信できるバイト数と捉えることができます。

ユーザーは所有するBandwidthを全て消費した後にトランザクションを送信すると、手数料はBandwidthではなくTRXから支払われます。

各外部所有アカウント(EOA)に毎日無料で付与されるBandwidthは600ポイントでになりますが、TRXをステーキングすることで、その量に応じてより多くのBandwidthを得られます。

このBandwidthポイントはアカウントの上限に限らず600ポイントを24時間で徐々に回復していきます。
※2023年7月のアップデートで付与されるBandwidthの上限が1500から600に変更されました。

BandwidthやTron のリソースモデルについて詳しく知りたい方はこちらの公式ページを参照してください。

トランザクションの手数料計算

手数料の計算式

今回は実際に私が発行したTronのNile Testnetにある10TRXを送金しているトランザクションを例に解説していきたいと思います。

$ curl --request POST \
  --url 'https://api.nileex.io/wallet/gettransactionbyid' \
  --header 'Content-Type: application/json' \
  --data '{
    "value": "a049e49dac17e5f30c56d49e096cba343c6dfb311b7a8f1581d3c13a40156480",
    "visible": true
  }'

まず、トランザクションの手数料の計算式をみてみます。

公式ページによるとトランザクションの手数料(Bandwidth / TRX) はトランザクションのバイト数*1000 SUNで算出できます。SUNはTRXの最小単位であり、1SUNは0.000001TRXに相当します(10のマイナス6乗)に相当します。

①...Transaction Fee [SUN] = 1000 [SUN/Byte]  * Transaction Size [Byte]

Source

トランザクションのバイト数の求め方

次に、トランザクションのバイト数の算出方法について説明します。

  • トランザクションバイト数の計算式

    Tron Protocolのソースコードによると、トランザクションの基本的な構成要素は raw_datasignatureret の三つになります。

    message Transaction {
    
      raw raw_data = 1;
      // only support size = 1,  repeated list here for muti-sig extension
      repeated bytes signature = 2;
      repeated Result ret = 5;
    }
    

    Source: java-tron/protocol/src/main/protos/core/Tron.proto

    しかし、公式ページによるとトランザクションのフォーマットは下記のようにraw_datasignatureの二つから構成されているとされており、ret フィールドが省かれていることがわかります。

    {
        "raw_data": 
        {
            "contract": [{<-->}],
            "ref_block_bytes": "c145",
            "ref_block_hash": "c56bd8a3b3341d9d",
            "expiration": 1646796363000,
            "data": "74657374",
            "timestamp": 1646796304152,
            "fee_limit":10000000000
        },
        "signature":["47b1f77b3e30cfbbfa41d795dd34475865240617dd1c5a7bad526f5fd89e52cd057c80b665cc2431efab53520e2b1b92a0425033baee915df858ca1c588b0a1800" ] 
    }
    

    Source

    これはソースコードを辿ると、トランザクションのバイト数の計算が行われる際、Transaction メッセージのretフィールドは省いて計算される代わりに定数MAX_RESULT_SIZE_IN_TXがbytesize(バイト数)に加算されていることがわかります。

    MAX_RESULT_SIZE_IN_TXについては Constant.javaにて、64と定義されています。

    public static long consumeBandWidthSize(
        final TransactionCapsule transactionCapsule,
        ChainBaseManager chainBaseManager) {
        long bytesSize;
    
        boolean supportVM = chainBaseManager.getDynamicPropertiesStore().supportVM();
        if (supportVM) {
          // ここの .clearRet()でret fieldを省いてトランザクションのバイト数を計算している
          bytesSize = transactionCapsule.getInstance().toBuilder().clearRet().build().getSerializedSize();
        } else {
          bytesSize = transactionCapsule.getSerializedSize();
        }
    
        List<Transaction.Contract> contracts = transactionCapsule.getInstance().getRawData().getContractList();
        for (Transaction.Contract contract : contracts) {
          if (contract.getType() == Contract.ContractType.ShieldedTransferContract) {
            continue;
          }
          if (supportVM) {
             // ret fieldを省いて算出したトランザクションのバイト数にMAX_RESULT_SIZE_IN_TX = 64 を加算
            bytesSize += Constant.MAX_RESULT_SIZE_IN_TX;
          }
        }
    
        return bytesSize;
      }
    

    Source: java-tron/actuator/src/main/java/org/tron/core/utils/TransactionUtil.java

    上記を踏まえるとトランザクションのバイト数の計算式を下記のようであると仮定できます。

    ...Transaction Byte Size = raw_data + signature + 64(=MAX_RESULT_SIZE_IN_TX)
    

raw_dataとsignatureのバイト数の求め方

raw_datasignatureのバイト数を求めるにあたり、Protocol Buffers (以下Protobuf)への理解が少しだけ必要ですが、ボリュームが大きくなるので本編では割愛して後編で解説します。

ProtobufはGoogleが開発したデータシリアライゼーション形式の一つで、Protobufでmessageがシリアライズされる際は下記のいずれかの形式でエンコーディングされます。どちらのメッセージ形式でシリアライズされるかというのはバイト数を求める上で大変重要になります。

Tagは1バイト で表現され、Tag Length ValueはDataのバイト数の長さを表し、Dataが128バイト 以下なら1バイト で表現され、128バイト 以上なら2バイト で表現されます。

# Message形式
1) Tag(Field Number+Wire Type): {Tag Length Value (optional): Data}
2) Tag(Field Number+Wire Type): Data

Transactionメッセージのraw_dataとsignatureはいずれもTag Length Value がエンコードされたメッセージの構造に含まれるので、これらを考慮してバイト数を計算します。

以上を踏まえるとトランザクションのバイト数を求める式は下記のようになります。

③...Transaction byte size = (Tag + Tag Length Value + raw_data) + (Tag + Tag Length Value + signature) + 64(=MAX_RESULT_SIZE_IN_TX)

raw_dataのバイト数

Raw Data の構成要素はこのようになります。

message Transaction {
  enum ContractType {
    AccountCreateContract = 0;
    TransferContract = 1;
    TransferAssetContract = 2;
    ...
  }
	message Contract {
         ...
		ContractType type = 1;
    google.protobuf.Any parameter = 2;
    bytes provider = 3;
    bytes ContractName = 4;
    int32 Permission_id = 5;
  }
	message raw {
	  bytes ref_block_bytes = 1;
	  int64 ref_block_num = 3;
	  bytes ref_block_hash = 4; 
	  int64 expiration = 8; 
	  repeated authority auths = 9;
	  // data not used
	  bytes data = 10;
	  //only support size = 1,  repeated list here for extension
	  repeated Contract contract = 11; 
	  // scripts not used
	  bytes scripts = 12;
	  int64 timestamp = 14;
	  int64 fee_limit = 18;
	}
  raw raw_data = 1;
  repeated bytes signature = 2;
  repeated Result ret = 5;
}

Source: java-tron/protocol/src/main/protos/core/Tron.proto

今回私が発行したトランザクションのRawDataはこちらになります。

message raw {
	bytes ref_block_bytes = 1;
	bytes ref_block_hash = 4; 
	int64 expiration = 8; 
	repeated Contract contract = 11; 
	int64 timestamp = 14
}
"raw_data":{
    "contract":[
    {
      "parameter":{
        "value":{
          "amount":10000000,
          "owner_address":"TNFxWSGqqYbwWUjG3gocapWioSg8Ftmmk9",
          "to_address":"TER4ok6iJGBya3NCrN5KFfZQT6vraPY5jZ"
        },
        "type_url":"type.googleapis.com/protocol.TransferContract"
      },
      "type":"TransferContract"
    }
  ],
  "ref_block_bytes":"3595",
  "ref_block_hash":"21463c48cf19256b",
  "expiration":1684489811000,
  "timestamp":1684407011000
},

ref_block_bytesBlockId(16進数)デコード後のバイト列の6バイト目~8バイト目。2バイト

ref_block_hashBlockId(16進数)デコード後のバイト列の8バイト目から16バイト目。8バイト

contract104バイト (contract dataの中身により変動)

timestamp… 公式ページのデータを参考に13桁の int64を使用⇒ Base128varintでエンコードしたら 。6バイト

expiration…timestampと同じバイト数。6バイト

Contract の部分はトランザクションタイプによりますが、基本contract が計算式に加わると128バイト を超えるのでraw_data のTag Length Valueは2バイト になります。

# Wire Type
ref_block_bytes = bytes = 2 (Has Tag Length Value)
Contract(embedded messages) = 2 (Has Tag Length Value)
int64 = 0 (No Tag Length Value)

# 式  ※以下、Tag=T、TagLengthValue=TLVと表記します
raw_data = (T + TLV + ref_block_bytes) + (T + TLV + ref_block_hash) + (T + TLV + contract) + (T + timestamp) + (T + expiration)

         = (1 + 1 + 2) + (1 + 1 + 8) + (1 + 1 + 104) + (1 + 6) + (1 + 6)
         = 4 + 10 + 106 + 7 + 7
         = 134

raw_data = 134 bytes

上記と踏まえると、計算式は下記の通りになります。

Tag + Tag Length Value + raw_data = 1 + 2 + raw_data

signature のバイト数

次にSignature のバイト数を求めます。

Signature のサイズは固定長であり、公式によると65 バイトであることがわかります。

r:::38b7dac5ee932ac1bf2bc62c05b792cd93c3b4af61dc02dbb4b93dacb758123f
s:::08bf123eabe77480787d664ca280dc1f20d9205725320658c39c6c143fd5642d
v:::0

r = 32 bytes
s = 32 bytes
v = 1 byte

r + s + v = 65 bytes

ただしこれはあくまで署名が一つであり、署名が複数存在するマルチシグの場合も考慮に入れる必要があります。

コードにもあるとおりsignatureにはrepeated というキーワードがあり、これはsignatureフィールドが複数の値を持つことを示しています。

署名人数が1増えると、T + TLV + Dataの形式のsignatureがバイト配列に追加されるため、署名数が1増える毎にバイト配列は下記のように67(1+1+65)増えることになります。

署名が3名の場合、2: signatureの形式が下記のようになる。
1: raw_data (TLV(データ長)あり)
2: signature (TLV(データ長)あり)
2: signature (TLV(データ長)あり)
2: signature (TLV(データ長)あり)

そのためsignatureの計算式は下記のようになります。

## シングルシグの場合
signature = M * (T + TLV + signature)
          = 1 * (1 + 1 + 65)
          = 67 bytes

## マルチシグ(M=2)の場合 
signature = M * (T + TLV + signature)
          = 2 * (1 + 1 + 65)
          = 134 bytes

以上より、raw_datasignatureの計算方法がわかったので③トランザクションのバイト数を求める式に数字を代入します。

Transaction byte size = (Tag + Tag Length Value + raw_data) + (M * (Tag + Tag Length Value + signature)) + 64
  • シングルシグの場合

    署名者 M of N = 1 of 1

    bandwidth 268

    Transaction byte size = (Tag + Tag Length Value + raw_data) + (M * ( + signature)) + 64
                          = (1 + 2 + raw_data) + (1 * (1 + 1 + 65)) + 64
                          = (3 + raw_data) + 67 + 64
                          = 268
    
    Transaction byte size = 268 bytes
    
    ※raw_data は134バイトであり、Base128varint に従うと 134 > 128 であるためTLVは2バイトで表現される。
    
  • マルチシグの場合

    署名者 M of N = 2 of 3

    bandwidth 335

    Transaction byte size = (Tag + Tag Length Value + raw_data) + (M * ( + signature)) + 64
                          = (1 + 2 + raw_data) + (2 * (1 + 1 + 65)) + 64
                          = (3 + raw_data) + 134 + 64
                          = 335
    
    Transaction byte size = 335 bytes
    

最後に記事冒頭で確認したトランザクション手数料の計算式を用いて、求めたトランザクションバイト数を代入したら正しい手数料が算出できます。

①...Transaction Fee [SUN] = 1000 [SUN/Byte] * Transaction Size [Byte]

今回発行した268 バイトトランザクションの送信にTRXを消費して送る場合はこのようになります。

Transaction Fee [SUN] = 1000 [SUN/Byte]  * Transaction Size [Byte]
                      = 1000 * ((3 + raw_data) + (M * (2 + 65)) + 64)
                      = 1000 * 268
                      = 268000 SUN

Transaction Fee = 0.268 TRX

最後に

この記事では、TronネットワークにおけるTransaction手数料のメカニズムについて解説しました。トランザクション手数料をどのように計算したら良いか知りたい方の一助となる内容にできたのではないかと思います。次回の記事では詳細な手数料の求め方を解説する際にProtobufやエンコーディング方式について触れるので、興味ある方はぜひご一読ください。

株式会社 Ginco ではブロックチェーンを学びたい方、ウォレットについて詳しくなりたい方を募集していますので下記リンクから是非ご応募ください。

長くなりましたが最後まで読んでいただきありがとうございました。

株式会社Ginco の全ての求人一覧

参考文献

https://developers.tron.network/docs/resource-model
https://github.com/tronprotocol/java-tron

https://pkg.go.dev/github.com/golang/protobuf/proto

https://developers.tron.network/v3.7/docs/account

https://developers.tron.network/reference/address

https://protobuf.dev/programming-guides/encoding/

Discussion