Tron ネットワークにおけるトランザクション手数料の求め方
この記事は株式会社 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]
トランザクションのバイト数の求め方
次に、トランザクションのバイト数の算出方法について説明します。
-
トランザクションバイト数の計算式
Tron Protocolのソースコードによると、トランザクションの基本的な構成要素は
raw_data
、signature
、ret
の三つになります。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_data
、signature
の二つから構成されているとされており、ret
フィールドが省かれていることがわかります。{ "raw_data": { "contract": [{<-->}], "ref_block_bytes": "c145", "ref_block_hash": "c56bd8a3b3341d9d", "expiration": 1646796363000, "data": "74657374", "timestamp": 1646796304152, "fee_limit":10000000000 }, "signature":["47b1f77b3e30cfbbfa41d795dd34475865240617dd1c5a7bad526f5fd89e52cd057c80b665cc2431efab53520e2b1b92a0425033baee915df858ca1c588b0a1800" ] }
これはソースコードを辿ると、トランザクションのバイト数の計算が行われる際、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_data
とsignature
のバイト数を求めるにあたり、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_bytes
…BlockId(16進数)デコード後のバイト列の6バイト目~8バイト目。2バイト
ref_block_hash
…BlockId(16進数)デコード後のバイト列の8バイト目から16バイト目。8バイト
contract
…104バイト
(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_data
とsignature
の計算方法がわかったので③トランザクションのバイト数を求める式に数字を代入します。
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 ではブロックチェーンを学びたい方、ウォレットについて詳しくなりたい方を募集していますので下記リンクから是非ご応募ください。
長くなりましたが最後まで読んでいただきありがとうございました。
参考文献
Discussion