Zeroichi Arakawa
これまで Flow のトランザクション手数料は固定でしたが、2022 年 6 月 1日より、処理内容に応じて手数料が決定されるように変わりました。この記事では、新しく適用されたトランザクション手数料の仕様(Segmented Transaction Fees)について説明します。
※本記事は、公式ドキュメント「Segmented Transaction Fees」を日本語に翻訳して、いくつかの補足を加筆したものです。
トランザクション手数料の必要性を理解する
手数料の構造を理解する
トランザクション・コストを見積もる
Execution の上限値を設定する
Effort 削減のため Cadence コードを最適化する
ユーザーを教育する
もっと知るには
FAQs
補足
このガイドでは、トランザクション手数料が重要である理由とその計算方法、そして実装における手数料の扱い方について説明します。具体的には、トランザクションにかかるコストを見積もる方法、コストの上限を設定する方法、トランザクション・コストを削減するために Cadence コードを最適化する方法について説明します。
このガイドの最後には、手数料についてユーザを教育する方法と、トランザクション手数料の実装について詳しく知ることができる情報が記載されています。
**注記:**トランザクション・コストの実装は、コミュニティによる FLIP のプロセスに基づいています。この作業は現在進行中です。このプロセスに参加するには、後述の「もっと知るには」のセクションに進んでください。
ネットワークへの影響に応じた公正な価格設定を行うためには、セグメント化されたトランザクション手数料が不可欠です。例えば、より重いオペレーションは、トランザクションの処理と伝播に多くのリソースを必要とします。一方で、一般的なオペレーションは安い価格になります。
手数料は、ネットワーク上の悪意のある行為(スパムなど)を実行しにくくし、ネットワーク全体のセキュリティを向上させます。
Flow アーキテクチャは、高いスループットを目標としています。システムに余裕を持たせることで、短時間のスパイクをより適切に処理できます。
手数料は、Execution fee(実行の手数料)、Inclusion fee(全体的な手数料)、Network surge factor(ネットワーク状況に応じた変動要素)という 3 つの要素に基づいて計算されます。
Inclusion fee / Execution fee は、Inclusion effort / Execution effort(労力)と、それらに関連するコストの乗数で表現されます。最終的なトランザクション手数料の計算は次のようになります。
inclusionFee = inclusionEffort * inclusionEffortCost;
executionFee = executionEffort * executionEffortCost;
totalFee = (inclusionFee + executionFee) * surgeFactor;
**注記:**コスト関数についてもっと知りたい方は、FLIP 753 をご覧ください。
トランザクションの Execution effort は、トランザクションがとるコードパスとそのアクションによって決まります。Execution effort コストを伴うアクションは、大きく 4 つの種類に分けられます。
通常の Cadence の行、ループ、関数呼び出し
ストレージからのデータ読み込み(読み込みバイト数)
ストレージへのデータ書き込み(書き込みバイト数)
アカウント作成
コストの概要
コストの範囲をより理解するために、現状の executionEffortCost
と inclusiveEffortCost
パラメータを前提に、一般的なトランザクション・タイプとその Execution コストをいくつか紹介します。
トランザクションの Inclusion effort は、以下のために必要な作業です。
トランザクションをブロックに含める
ノードからノードへトランザクション情報を送る
トランザクションの署名を検証する
現在、Inclusion effort は常に1.0であり、Inclusion effort コストは 0.000001
に固定されています。
**注記:**Inclusion effort は、常にトランザクション・コードを実行することなく計算できます。
将来的には、Inclusion コストはトランザクションのバイトサイズと必要な署名の数に影響されます。
**注記:**可変の Inclusion コストへのアップデートが、今後リリースされる Spork で実施される予定です。
将来的に、処理に必要なトランザクションの流入が増加したり、トランザクションの処理能力が低下したりして、ネットワークがビジー状態になった場合に、Network surge(変動要素)が適用されるようになる予定です。現在、Network surge は 1.0 に固定されています。
ストレージ手数料
ストレージ手数料は、トランザクション手数料とは異なる方法で実装されています。詳細については、**Storing Data on Flow guide **を参照。要約すると、ストレージ手数料は、オンチェーンでのデータ保管に関連するコストです。
コストの見積もりは 2 段階のプロセスで行われます。まず、エミュレータまたはテストネットで Execution effort を調べる必要があります。次に、その値を使って、JavaScript FCL SDK または Go FCL SDK で最終的な手数料を計算します。
Execution effort を調べるには、トランザクションを実行して、発行されたイベントの詳細を確認するのがよいです。
Flow エミュレータの場合:
Flow CLI を使ってエミュレーターを起動します。トランザクションを実行して、発行されたイベントを見てみましょう。
0|emulator | time="2022-04-06T17:13:22-07:00" level=info msg="⭐ Transaction executed" computationUsed=3 txID=a782c2210c0c1f2a6637b20604d37353346bd5389005e4bff6ec7bcf507fac06
computationUsed
フィールドが表示されます。この値を次のステップで使用するので、メモしてください。
テストネットの場合:
テストネットの場合、トランザクション完了後に Flowscan のようなエクスプローラーで、トランザクションの詳細と発行されたイベントを確認します。Flowscan の場合、該当のトランザクションを開いて、FlowFees
コントラクトから FeesDeducted
というイベントを探すことができます。
右側に表示されているイベントデータには、FeeParameters 構造体のフィールド群があります。
surgeFactor
inclusionEffort
executionEffort
このうち、executionEffort
の値をメモしてください。この値は、次のステップで使用します。
トランザクションにかかるコストは、メインネット/テストネットそれぞれで、以下の FCL スクリプトで計算できます。
メインネットの場合:
import FlowFees from 0xf919ee77447b7497
pub fun main(
inclusionEffort: UFix64,
executionEffort: UFix64
): UFix64 {
return FlowFees.computeFees(inclusionEffort: inclusionEffort,
executionEffort: executionEffort)
}
テストネットの場合:
import FlowFees from 0x912d5440f7e3769e
pub fun main(
inclusionEffort: UFix64,
executionEffort: UFix64
): UFix64 {
return FlowFees.computeFees(inclusionEffort: inclusionEffort,
executionEffort: executionEffort)
}
FCL SDK では、各トランザクションの Execution effort の上限値を設定できます。予期せぬ動作を避けて、ユーザーを保護するために、妥当な最大値を設定する必要があります。最終的なトランザクション手数料は、この最大値以下の実際の Execution effort から計算されます。
**注記:**上限値は、ユーザーが支払う最終的な手数料に対するものではないことに留意してください。上限値は、Execution effort に対するものです。
高すぎず、低すぎない上限値を設定することが重要です。高すぎる場合、支払者はトランザクション送信前にもっと資金を用意する必要が出てきます。逆に低すぎる場合は、トランザクションの実行が失敗して、すべてのステート変更が取り消されてしまいます。
FCL JS SDK の場合
mutate
関数に limit
パラメータを設定する必要があります。例:
import * as fcl from "@onflow/fcl"
const transactionId = await fcl.mutate({
cadence: `
transaction {
execute {
log("Hello from execute")
}
}
`,
proposer: fcl.currentUser,
payer: fcl.currentUser,
limit: 100
})
const transaction = await fcl.tx(transactionId).onceSealed();
console.log(transaction;)
FCL Go SDK の場合
手数料の上限を設定するには SetGasLimit
メソッドを呼び出す必要があります。例:
import (
"github.com/onflow/flow-go-sdk"
"github.com/onflow/flow-go-sdk/crypto"
)
var (
myAddress flow.Address
myAccountKey flow.AccountKey
myPrivateKey crypto.PrivateKey
)
tx := flow.NewTransaction().
SetScript(
[]byte("transaction { execute { log(\"Hello, World!\") } }")).
SetGasLimit(100).
SetProposalKey(
myAddress, myAccountKey.Index, myAccountKey.SequenceNumber).
SetPayer(myAddress)
最適化は、トランザクションの実行時間の短縮につながります。以下は、実践例です。これらは網羅的なものではなく、あくまでいくつかの例です。
関数呼び出しを制限する
関数を呼び出すときは、必ず必要なものだけにしてください。場合によっては、前提条件をチェックすることで、余計な呼び出しをなくせるかもしれません。
for obj in sampleList {
/// 呼び出しが必要かどうか確認する
if obj.id != nil {
functionCall(obj)
}
}
ループと繰り返し処理を制限する
リストに対して繰り返し処理を行う場合は、サブセットではなく、すべての要素に対して繰り返し処理を行わなければならないことを注意してください。ループは、時間の経過とともにサイズが大きくなりすぎないようにします。可能な限りループは制限しましょう。
// 長いリストを繰り返し処理すると、コストがかかることがある
pub fun sum(list: [Int]): Int {
var total = 0
var i = 0
// リストが大きくなりすぎると、これはできなくなる可能性がある
while i < list.length {
total = total + list[i]
}
return total
}
// トランザクション(とスクリプト)を、
// 作業をより小さな断片(chunk)にできるように設計することを検討する
pub fun partialSum(list: [Int], start: Int, end: Int): Int {
var partialTotal = 0
var i = start
while i < end {
partialTotal = partialTotal + list[i]
}
return partialTotal
}
関数呼び出しの影響を理解する
いくつかの関数は、他の関数よりも実行の手間がかかります。どのような関数呼び出しが行われ、どのような実行を伴うかを慎重に検討する必要があります。
// 他の関数(または自身)をたくさん呼び出すような関数は、コストが高くなる可能性がある
pub fun fib(_ x: Int): Int {
if x == 1 || x== 0 {
return x
}
// 再帰ごとに +2 回 の関数呼び出し
return fib(x-1) + fib(x-2)
}
// コスト削減のため、単一ステートメントによる関数のインライン化を検討する
pub fun add(_ a: Int, _ b: Int): Int {
// 単一ステートメント; インライン化する価値がある
return a + b
}
過剰な load とsave を避ける
コストのかかる load と save の操作を避け、可能な限り**参照を borrow** してください。例:
transaction {
prepare(acct: AuthAccount) {
// 保存されている vault への参照を borrow することで、
// vault をストレージから取り出すよりもはるかに低コストで処理できる
let vault <- acct.borrow<&ExampleToken.Vault>
(from: /storage/exampleToken)
let burnVault <- vault.withdraw(amount: 10)
destroy burnVault
// 参照を使うだけなので `save` は必要ない
}
}
***注記:***要求されたリソースが存在しない場合、読み取りコストは発生しません。
トランザクションあたりのアカウント作成数を制限する
アカウントの作成とキーの追加にはコストがかかります。アカウントやキーの作成は必要なときだけにするようにしましょう。
トランザクションを実行する前にユーザーの残高を確認する
ユーザーの残高が、最大手数料をカバーするのに十分であることを確認する必要があります。FT 送金については、最大手数料に加えて、送金する金額をカバーする必要があります。
ウォレットは最終的なトランザクション・コストを表示しますが、アプリケーション内でユーザーを教育することによって、ユーザー体験をよりよくできます。
ユーザーがノン・カストディアル・ウォレットを使っている場合、トランザクション手数料を支払う必要があり、手数料について理解したいと思うかもしれません。以下は、その提案です。
ネットワーク利用状況によってコストが異なる可能性があることを説明する
推奨されるメッセージ:「手数料は、ネットワークのセキュリティを向上させます。ネットワークへの影響に応じた公正な価格設定のためにフレキシブルに変化します。」
Network surge がなくなるのを待つのも選択肢の一つであることを説明する
Network surge が発生すると、必然的に手数料が高くなります。ネットワークの利用が急増している間にトランザクションを送信したいユーザーは、コストを削減するためにトランザクションをあとで送信することを検討することができます。
ウォレットが資金不足でトランザクションを許可しない可能性があることを説明する
動的な手数料が最高水準まで上がった場合、ユーザーの資金がトランザクション実行に十分でない可能性があります。資金を足すか、ネットワークの混雑が緩和されたときに試すべきことをユーザーに知らせます。
トランザクション手数料について詳しく知ることができる場所がいくつかあります:
注記:Flow のトランザクション手数料の導入についてご意見があれば、こちらのフォーラム投稿にフィードバックを残してください。
この手数料の変更はいつから有効になりますか?
2022 年 4 月 6 日の Spork でアップデートがデプロイされて、6 月 1 日 の ウィークリー・エポック移行で有効化されました。
なぜトランザクションが失敗した場合でも手数料が徴収されるのですか?
トランザクションのブロードキャストと検証には実行が必要なため、コストが適切に差し引かれます。
平均以上の Execution コストとなるのはどの処理ですか?
Execution コストに平均というものはありません。すべての機能は、実装されたロジックに基づいて大きく変化します。最適化のベストプラクティスを確認し、コストを削減できるかどうかを判断する必要があります。
Ledger などのハードウェアウォレットは、セグメント化された手数料をサポートしていますか?
はい。
最も低い Execution コストの値は何ですか?
最も低い Execution コストの値は 1 です。これは、トランザクションに、データの読み取りや書き込みを行わない関数呼び出しやループが 1 つ含まるものを意味します。
実際に支払うことなく、メインネットでのトランザクション・コストを求められますか?
2 つのプロセスでコストを見積もることができます。1)トランザクションの Execution コストを求めて(エミュレータまたはテストネット)、2)FCL SDK のメソッドを使って最終的なトランザクション手数料を計算します。
テストネットの手数料はメインネットの手数料に対してどれくらいの精度になりますか?
最終的な手数料は、Network surge factor によって決まります。テストネットの surge factor はメインネットのものとは異なるため、メインネットとテストネットの見積もりにはばらつきがあると考えておく必要があります。
Blocto を使っていますが、手数料を支払ったことがありません。なぜでしょうか?
それは、Blocto がトランザクションの支払者として機能しているからです。ノン・カストディアル・ウォレットの場合、ユーザーがトランザクション手数料を支払うことになりますが、アプリ側が(彼らが選択すれば)手数料のスポンサーになることもできます。
今回変更されたトランザクション仕様では、現状のトランザクションの統計が調査され、手数料の平均が約 95 % になるように各処理のコストが決定されました。そのため、ユーザーが支払う手数料は平均的に少し安くなっています。
また、現状ほとんどのトランザクションは Blocto ウォレットを使って送信されており、その手数料は Blocto の運営元によって支払われています。そのため、今回の仕様変更によるユーザーへの影響は小さいです。
ただし、Cadence コードの各処理のコストが明確化されたため、開発者にとっては、気にしなければならないことが増えています。個人的には、手数料を抑えることに気を使いすぎて、可読性が犠牲にならないことを願うばかりです。
今後、ノン・カストディアル・ウォレットの利用者数が増えたり、ネットワークがビジーになってきた際に、この仕様変更は特に効いてくるだろうと思います。