2021.01.07
UX向上の取組の最前線 – イーサリアムでのガスレストランザクションとオートメーション(前編)
こんにちは。次世代システム研究室のL.W.です。
一般な方々に対して、ブロックチェーンのメカニズムは複雑で、知名度はまだ高くないと言えるが、最近ビットコイン(BTC)、イーサ(ETH)の価格の急騰につれて、多くの方はビットコイン、イーサを初めの仮想通貨(暗号資産)及びこれらを支えるブロックチェーン技術を勉強し始めているかと思っています。
ブロックチェーン技術で作られたビットコインネットワーク、イーサリアムネットワークはグローバルの決済システム、金融システム(DeFi)となりつつあります。世界での人々はインターネットにアクセスできれば、検閲なしにこの共通のシステムで決済、金融などのサービスを享受できます。
だが、今の時点でビットコインネットワーク、イーサリアムネットワークを決して容易に利用できるわけではないです。妨げる最大の要因の1つはユーザー体験(UX)の悪さと言われています。DeFiの未来はいくら明るいと言われてもUXが改善されないと、美しきおとぎ話に過ぎないでしょう。
イーサリアムはDeFiと親和性があると多くの有識者が受け止めていて、去年からイーサリアムでのDeFiプロジェクトは雨後の筍のように出てきています。今年もこの勢いは衰えないでしょう。より多くの方をDeFiの参加に促進するために、UXの向上は一番重要です。
イーサリアムにまつわるUXの悪さというと、よく挙げられるのはガスメカニズムの複雑さとスマートコントラクトとのやり取りの複雑さでしょう。
UXを向上するためには、イーサリアム界隈で多くの有識者、チームは鋭意取り組んでいます。今回は取組の最前線について、共有させていただきたいです。
この前編でガスメカニズムの複雑さを改善するためのガスレストランザクションについて紹介します。
後編でスマートコントラクトとのやり取りの複雑さを改善するためのgelatoと言うトランザクションのオートメーション(自動化)のプロジェクトを紹介します。
さて、始めましょうか。
1.ガスレストランザクション(Gasless Transaction)
(前回のブログでガスメカニズムを触れていたので、まだ詳しくない方は参照していただければと思います。)
メタトランザクション(Meta Transactions)はNon-custodialでガスレストランザクション(Gasless Transaction、ガス要らない)を可能にする技術です。
前回の説明のように、リレー方式により、二つ種類のメタトランザクションに分けられます。Centralized Relays(通常、リレーサービスを自作する)とDecentralized Relays(gsn)です。いずれも第三者はユーザーの代わりに、ガスを支払い、トランザクションをイーサリアムにリレーする仕組みとなります。
これで、サービス利用のユーザーはガスがなくても容易にイーサリアムを触れることが始めますが、自分の持つERC20トークンでより多くのガス代金を補填するか、あるいは誰か(DAPPのプロバイダーか)により多くのガス代金を肩代わりしてもらうことですね。
1-1.ガス費用の比較
Gas Priceの急騰につれて、トランザクションにかかったガスを最小限に抑えるのは望まれています。
各トランザクションにかかったガスについて、Ropstenネットワークでテストしてみました。
0x1111111111111111111111111111111111111111というアドレス宛てにGYENトークンを1GYENを送金することにします。
ユーザーは直接にターゲットコントラクト(transfer)を呼び出す場合
以下の図から見ると、4万位のガスが掛かりました。
自らトランザクションを作ることには、ユーザーはETHを持ち、かつガスメカニズムに了解する必要があります。
Centralized RelayerによりtransferWithAuthorizationを呼び出すの場合
以下の図から見ると、7万位のガスが掛かりました。
自作のリレーサーバーで管理されているアカウントのSignerがユーザーのオフチェーンでの署名済みの送金メッセージをトランザクションに埋めて、ガスを出して、ユーザーの代わりに、ターゲットコントラクトとやり取りします。
自作のリレーサービスを立ち上げる時に、サーバー側のSignerのNonce管理、GasPrice管理、ユーザー期待通りにブロックに書き込まれるDealine管理、セキュリティ管理などの課題は浮き彫りとなります。
これらの課題について、後ほどこの記事でも触れます。
GSNを利用する場合
以下の図から見ると、15万位のガスが掛かりました。
GSN利用でかかったガスは直接にターゲットコントラクトを呼び出すことによりかかったガスの数倍に至ることが伺えますね。
1-2.Meta Transactionに関わるEIPの紹介
EIP-3009: Transfer With Authorization
承認(authorization)である署名情報を介して第三者にもトークンの転送を可能にさせるコントラクトインターフェースの提案です。
疑似コードは以下の通りです。
/** * @notice Execute a transfer with a signed authorization * @param from Payer's address (Authorizer) * @param to Payee's address * @param value Amount to be transferred * @param validAfter The time after which this is valid (unix time) * @param validBefore The time before which this is valid (unix time) * @param nonce Unique nonce * @param v v of the signature * @param r r of the signature * @param s s of the signature */ function transferWithAuthorization( address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s ) public whenNotPaused onlyNotProhibited(from) { _requireValidAuthorization(from, nonce, validAfter, validBefore); // オフチェーンでの署名されたメッセージをここで再生成する bytes memory data = abi.encode( TRANSFER_WITH_AUTHORIZATION_TYPEHASH, from, to, value, validAfter, validBefore, nonce ); // 確かにfromのプライベートキーで署名したか、メッセージデータは改竄されたかチェックする require( EIP712.recover(DOMAIN_SEPARATOR, v, r, s, data) == from, "transferWithAuthorization: invalid signature" ); // リプレイプロテクションを実行する(fromアカウントのnonceを利用済みということをマークする) _markAuthorizationAsUsed(from, nonce); // fromアドレス所持のトークンをtoアドレスに送金する _transfer(from, to, value); }
応用としては、クライアント(Payer)はプライベートキーでオフチェーンでfrom、to、value、validAfter、validBefore、nonceを送金メッセージとしてまとめて署名し、署名情報(v, r, s)が取れます。送金メッセージと署名情報をリレーサービスに送ります。
プログラミング知識が持てば、誰でもCentralizedリレーサービスを立ち上げます。リレーサーバー側でリレーのアカウントでtransferWithAuthorizationを呼び出します。これでユーザー(Payer)の期待の通りに、fromアドレス所持のトークンをtoアドレスに送金することが済ませます。
EIP-2771: Secure Protocol for Native Meta Transactions
信頼できるforwarder(スマートコントラクト )を通してメタトランザクションを受信できるコントラクトインターフェイスの提案です。GSNは正にこれをベースに作られた分散型リレーネットワークです。
以下のは利用のフローです。
- Transaction Signer – ガスリレーへのリクエストに署名して送信するエンティティ
- Gas Relay – ガスを支払うリレーヤーで、Transaction Signerからオフチェーンで署名されたリクエストを受け取り、TrustedForwarderにより有効性を判定させる。
- Trusted Forwarder – Recipientコントラクト(ターゲットコントラクト、GYEN送金する場合、GYENコントラクトと指す)に信頼されて、Recipientコントラクトに転送する前には、Transaction Signerからオフチェーンで署名されたリクエストでの署名とNonceをチャックするスマートコントラクトです。
- Recipient – Trusted Forwarderから転送されたmeta-transactionsを受け入れるターゲットのコントラクトです。
Trusted Forwarderの疑似コードは以下の通りです。
contract Forwarder is IForwarder { // Transaction Signerからオフチェーンで署名されたリクエストでの署名とNonceをチャックする。 // チェックで問題がなければ、Recipientコントラクトを呼び出す。 function execute( ForwardRequest memory req, bytes32 domainSeparator, bytes32 requestTypeHash, bytes calldata suffixData, bytes calldata sig ) external payable override returns (bool success, bytes memory ret) { _verifyNonce(req); _verifySig(req, domainSeparator, requestTypeHash, suffixData, sig); _updateNonce(req); // ターゲットコントラクトを呼び出す (success,ret) = req.to.call{gas : req.gas, value : req.value}(abi.encodePacked(req.data, req.from)); return (success,ret); } }
gsnチームはRecipientコントラクトのベースコントラクトを提供しました。EIP-2771を準拠するためには、ターゲットコントラクトをBaseRelayRecipientを継承してバージョンアップすることができます。
/** * A base contract to be inherited by any contract that want to receive relayed transactions * A subclass must use "_msgSender()" instead of "msg.sender" */ abstract contract BaseRelayRecipient is IRelayRecipient { /* * Forwarder singleton we accept calls from */ address public trustedForwarder; // Forwarderコントラクトを信頼された奴かチェックする。 function isTrustedForwarder(address forwarder) public override view returns(bool) { return forwarder == trustedForwarder; } /** * return the sender of this call. * if the call came through our trusted forwarder, return the original sender. * otherwise, return `msg.sender`. * should be used in the contract anywhere instead of msg.sender */ // trusted forwarderから呼び出される場合、_msgSender()はオリジナルsenderのアドレスを返却する。 // trusted forwarder以外のアカウントから呼び出される場合、_msgSender()はmsg.senderを返却する。 function _msgSender() internal override virtual view returns (address payable ret) { if (msg.data.length >= 24 && isTrustedForwarder(msg.sender)) { // At this point we know that the sender is a trusted forwarder, // so we trust that the last bytes of msg.data are the verified sender address. // extract sender address from the end of msg.data assembly { ret := shr(96,calldataload(sub(calldatasize(),20))) } } else { return msg.sender; } } ... ... }
EIP-2612: permit – 712-signed approvals
ERC-20の拡張で、secp256k1署名(オフチェーン署名)を介して承認(approve)を行うことを可能にする提案です。
疑似コードは以下の通りです。
contract Permit { // リプレイプロテクション用 mapping(address => uint256) private _permitNonces; /** * @notice Update allowance with a signed permit * @param owner Token owner's address (Authorizer) * @param spender Spender's address * @param value Amount of allowance * @param deadline Expiration time, seconds since the epoch * @param v v of the signature * @param r r of the signature * @param s s of the signature */ function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) public whenNotPaused { require(deadline >= now, "permit is expired"); // オフチェーンでの署名されたメッセージをここで再生成する bytes memory data = abi.encode( PERMIT_TYPEHASH, owner, spender, value, _permitNonces[owner]++, //リプレイプロテクションを実行する(ownerのNonceがカウントアップ) deadline ); // 確かにownerのプライベートキーで署名したか、メッセージデータは改竄されたかチェックする require( EIP712.recover(DOMAIN_SEPARATOR, v, r, s, data) == owner, "permit: invalid signature" ); // ownerはspenderにトークンの引き出しを許可する _approve(owner, spender, value); } }
permitは最大のメリットは承認(approve)およびpullオペレーションは、2つの連続するトランザクションではなく、1つのトランザクションで実行で済ませることです。
これについて、Uniswapを例にして、どのようにUXを向上させたか体験してみました。
まず、UniswapのUniswapV2PairコントラクトでのUniswap V2 (UNI-V2)トークンは既にEIP2612を準拠したことを確認できました。USDCーETHのLiquidity Poolの場合、ここで参照できます。
ケース❶: 対象のトークンはEIP2612を準拠したか判断できず、悪いUX例
UniswapでAdd Liquidityする場合、ペアーに参加するトークンにはEIP2612を準拠したトークンがあれば、EIP2612を準拠しなかったトークンもあるので、approveオペレーションとaddLiquidityオペレーションを2つの連続するトランザクションにするしかないです。これにより、addLiquidityトランザクションを実行する前にはユーザーはまずapproveトランザクションの完了を待つしかないです。そして、二つのトランザクションの間、何か発生すれば、リスクを秘める可能性もあります。
addLiquidityでUNIーV2トークンを受け取られました。
ケース❷: UNIーV2トークンが既にEIP2612を準拠した、良いUX例
UniswapでRemove Liquiditする場合、つまり上の操作で受け取ったUNIーV2トークンでGYENとUSDTトークンを取り戻す場合、approveオペレーションとremoveLiquidityオペレーションを2つの連続するトランザクションにすることではなく、一つのremoveLiquidityWithPermitトランザクションで済ませました。
ケース❶のAdd Liquidityオペレーションと比べて、ケース❷のUXが改善されましたね。では、ソースコードを覗いてみます。
function removeLiquidityWithPermit( address tokenA, address tokenB, uint liquidity, uint amountAMin, uint amountBMin, address to, uint deadline, bool approveMax, uint8 v, bytes32 r, bytes32 s ) external virtual override returns (uint amountA, uint amountB) { address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); uint value = approveMax ? uint(-1) : liquidity; // ここでEIP2612のpermitが利用されました。これで、わざわざapproveトランザクションを出す必要がないですね。 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s); (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline); }
これで、EIP2612のpermit機能はDeFiに向いていると言われています。まだまだ他の可能性が秘めているかと信じています。
1-3.ウォレットコントラクト(Wallet Contract)
上で紹介させていただいたEIP2612、EIP3009とEIP2771はいずれも去年(2020)に提出した提案となりますが、USDT、USDC、DAIを初めの多くのトークンは既に2020年前に発行されました。EIP-2612、EIP3009またはEIP2771を準拠するためには、ERC20トークンコントラクトを修正して、バージョンアップするしかないです。
ERC20トークンコントラクトのうち、バージョンアップできるトークンがあれば、できないトークンもあります。USDCは既に2020/8/20にてEIP3009を準拠するためにバージョンアップ済み、EIP2771の対応も検討中に見られます。
では、USDT、DAIのようなバージョンアップ不能のトークンには、ガスレストランザクションを実現できますか。
答えはできます。方法はウォレットコントラクトとなります。
EOA(Externally Owned Account)で直接にターゲットのERC20トークンをアクセスすることではなくて、このEOAと紐ついているスマートコントラクトウォレットを構築して、EOAでのトークンをウォレットコントラクトに預けることで、ウォレットコントラクトはEOAの代理としてERC20トークンコントラクトとやりとりさせます。ウォレットコントラクトと言うと、GnosisSafeはよく挙げられます。
ProxyFactory通してGnosis Safeウォレットコントラクトをデプロイするフローは以下の通りです。
ProxyFactoryコントラクトでCREATE2方法でProxyをデプロイするのは一般です。
/// @dev Allows to create new proxy contact using CREATE2 but it doesn't run the initializer. /// This method is only meant as an utility to be called from other methods /// @param _mastercopy Address of master copy. /// @param initializer Payload for message call sent to new proxy contract. /// @param saltNonce Nonce that will be used to generate the salt to calculate the address of the new proxy contract. function deployProxyWithNonce(address _mastercopy, bytes memory initializer, uint256 saltNonce) internal returns (GnosisSafeProxy proxy) { // If the initializer changes the proxy address should change too. Hashing the initializer data is cheaper than just concatinating it bytes32 salt = keccak256(abi.encodePacked(keccak256(initializer), saltNonce)); bytes memory deploymentData = abi.encodePacked(type(GnosisSafeProxy).creationCode, uint256(_mastercopy)); assembly { proxy := create2(0x0, add(0x20, deploymentData), mload(deploymentData), salt) } require(address(proxy) != address(0), "Create2 call failed"); }
ユーザはEOAでオフチェーンでメッセージを署名する。ターゲットコントラクトのアドレスtoと呼び出す内容のdataが正しく署名されれば、第三者の誰でもガスを支払って、Gnosis SafeウォレットコントラクトのexecTransactionを呼び出して、dataの内容を実行させることができます。
ユーザーのEOAのプライベートキーにアクセスせず、EOAはオフラインになっても、Gnosis SafeウォレットコントラクトはEOAを代理して、ターゲットコントラクトとやりとりする仕組みとなります。
例えば、dataの内容はtransferの場合、署名するEOAアドレスではなく、このEOAの代理のGnosisSafeアドレスから宛てのアドレスへトークンを送金することとなります。トランザクションのfromは誰でも構わないです。
疑似コードは以下の通りです。
contract GnosisSafe { /// @dev Allows to execute a Safe transaction confirmed by required number of owners and then pays the account that submitted the transaction. /// Note: The fees are always transfered, even if the user transaction fails. /// @param to Destination address of Safe transaction. /// @param value Ether value of Safe transaction. /// @param data Data payload of Safe transaction. /// @param operation Operation type of Safe transaction. /// @param safeTxGas Gas that should be used for the Safe transaction. /// @param baseGas Gas costs for that are indipendent of the transaction execution(e.g. base transaction fee, signature check, payment of the refund) /// @param gasPrice Gas price that should be used for the payment calculation. /// @param gasToken Token address (or 0 if ETH) that is used for the payment. /// @param refundReceiver Address of receiver of gas payment (or 0 if tx.origin). /// @param signatures Packed signature data ({bytes32 r}{bytes32 s}{uint8 v}) function execTransaction( address to, uint256 value, bytes calldata data, Enum.Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes calldata signatures ) external payable returns (bool success) { // オフチェーンでのメッセージを再生成する bytes memory txHashData = encodeTransactionData( to, value, data, operation, // Transaction info safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, // Payment info nonce ); // 署名情報をチェックする。 checkSignatures(txHash, txHashData, signatures, true); // Use scope here to limit variable lifetime and prevent `stack too deep` errors { // 署名情報が問題なければ、data内容を実行する success = execute(to, value, data, operation, gasPrice == 0 ? (gasleft() - 2500) : safeTxGas); gasUsed = gasUsed.sub(gasleft()); // We transfer the calculated tx costs to the tx.origin to avoid sending it to intermediate contracts that have made calls uint256 payment = 0; if (gasPrice > 0) { // ガス費用を精算する payment = handlePayment(gasUsed, baseGas, gasPrice, gasToken, refundReceiver); } if (success) emit ExecutionSuccess(txHash, payment); else emit ExecutionFailure(txHash, payment); } } }
ウォレットコントラクトでもガスレストランザクションを実現できますが、セットアップフェーズ(トークンをEOAからウォレットコントラクトへtransferあるいはapproveする)では必ずEOAからトランザクションを出す必要です。つまり、ETHはセットアップフェーズでは必須です。
ウォレットコントラクトではMultisig、トランザクションのバッチ処理などの機能も備えていますので、EIP2612、EIP3009とEIP2771とうまく結合できれば、面白い機能が沢山に生み出せるかと思っています。
2.自作リレーサービスの課題
弊社の日本円と連動(円ペッグ)したステーブルコイン「GYEN」(GMO Japanese YEN)は近く世に出せますが、RinkebyテストネットでGYENをEIP3009に準拠させて、ガスレスのwebウォレットを作ってみました。興味の持つ方は試していただければと思います。
手順は以下の通りです。(MetaMaskというブラウザウォレットが事前にインストール要)
- MetaMaskでRinkebyネットワークにしておく。
- このサイトで「Mint GYEN for test」をクリックして、GYENのテストトークンを手に入れる。(ETH要らないよ、gsnを利用しました。)
- GYEN Gasless Transfer Walletでガスレスの送金を楽しめる。
ソースコードはgithubに参照してください。
疑似コードは以下の通りにです。
async postbySingleserver(req, res, next) { let overrides = { // The maximum units of gas for the transaction to use gasLimit: 230000, // The price (in wei) per unit of gas gasPrice: ethers.utils.parseUnits('10', 'gwei'), // The nonce to use in the transaction nonce: nonce, }; // execute the transaction try{ const tx = await contract.transferWithAuthorization(from, to, value, validAfter, validBefore, nonce, v, r, s, overrides); // Waiting for the transaction to be mined const receipt = await tx.wait(); var re = { minedBlock: receipt.blockNumber, txHash: tx.hash } res.status(200); res.body = { 'status': 200, 'success': true, 'result': re } return next(null, req, res, next); }catch(err){ console.log("send error =>\n",err); res.status(500); res.body = { 'status': 500, 'success': false, 'result': err.reason } return next(null, req, res, next); } }
リレーサービスを安定的かつ経済的に稼働するためには、テスト通して、以下の課題は認識し始めました。
2-1.GasPrice
ユーザーの代わりに、サーバー側のSignerのETHでガスを支払う仕組みとなるので、GasPrice市場を常にモニタリングし、イーサリアムネットワークのメモリープール内のすべてのトランザクションに基づいて、競争力のあるGasPriceを選択しないといけないです。
トランザクションのGasPriceを過少的にセッティングすれば、トランザクション処理されるまでの時間が長くなります。一方、過大的にセッティングすれば、利益にならないでしょう。適切的にセッティングできれば何よりですが、この適切さの把握は決して容易ではないです。
2-2.Nonce
リレーサービスのロジックで、サーバー側のSignerのnonceを常にトラッキングし、ローカルのトランザクションキューを管理し、前のnonceのトランザクションが後ろの優先度の高いトランザクションのマイニングを妨げないようにnonceを謹んで取り扱う必要があります。ではないと、トランザクションの紛失となることもあります。
例えば、nonce1で署名したトランザクションのGasPriceは相場に合わない場合に、長い間に処理されないと、このトランザクションはMinerにより廃棄されて、後ろのnonce2、nonce3で署名したトランザクションらも処理されていなくなり、結局廃棄になります。
どのように廃棄されたトランザクションの再構築、再発送を行うか、工夫が欠かせないでしょう。
2-3.Deadline
ユーザーはあるdeadline前に(例えば、○ブロック数以内、現在のSignerのNonce+○以内など)トランザクションを処理させたいという要望は容易に答えられますか。
これはGasPrice相場、マイニングプール、イーサリアムの混雑具合などに掛かっています。
deadline前にトランザクションを処理させる保証が実現できれば、UXの向上に繋がります。
2-4.セキュリティ
リレーサーバー側のSignerはトランザクションの署名を繰り返し行うことで、プライベートキーはインターネットにさらされています。
ネットワークの構築には何か不備があれば、Signerの資金流失も不可能ではないでしょう。
ブロックチェーン界隈では、資金流出が多発していますね。いつか再発生してもおかしくないので、セキュリティに120パーセントの注意が必要です。
3.ITXの紹介
ITX(Infura Transactions)サービスはInfuraが世に出しているイーサリアムのトランザクション管理を簡素化させるサービスです。
ITXはトランザクション送信のすべてのエッジケース(GasPriceの見直し、滞っているトランザクションの再発送など)を処理し、トランザクションのマイニングを保証すると同時に、開発者がガス管理に関する複雑さに対処する必要をなくし、イーサリアムトランザクションの送信を簡略化させることを目指しています。
そして、ITX利用で、サービスプロバイダーはユーザーに代わってトランザクションのガス代金を支払うことも可能です。
現在はRinkebyネットワークでのテストに利用できるアルファリリースであり、2021年中に正式なリリースができるか注目を集めています。
3-1.ITXの前身のany.sender
2020/12/3にてInfuraの公式ブログで、any.senderを買収して、ITX(Infura Transactions)のアルファリリースを発表したというア記事を目にした方がいるかと思います。ITXの前身はany.senderであることが間違い無いです。
- Better UX – ユーザーは、GasPriceの設定や滞っているトランザクションの対処する必要はなくなる。 トランザクション管理は any.senderに任せる。
- Build faster – トランザクションの管理は any.senderに任せることで、開発者はDAPPの開発に集中できる。
- Non-custodial – any.senderはユーザーの事前承認されたトランザクションのみをリレーできます。 メタトランザクションのおかげで、dappのガスレスが実現可能になる。
これらの特色からすると、any.senderを活かせば、自作リレーサービスでの課題のNonce、GasPrice、Deadline、セキュリティーを一気にクリアできるかと思って、注目し始めました。
any.senderのリレーサービスで、面白いDEXサイトが作られました。
このサイト利用して、DAIトークンしか持たなくても、ガスレスでETHとスワップできます。any.senderのリレーヤーはスワップされたETHからガス代金とリレー手数料を差し引きます。
3-2.ITXを利用してみる
ITXの利用手順
ITXをプロジェクトに統合するときに実行する必要の手順が三つがあります。
① On-chain デポジットする
Depositerはon-chain deposit contractにETHをデポジットし、このデポジットはITXガスタンクとされる。Depositerはトランザクションをリレーする場合、秘密鍵でのオフチェーン署名でITXガスタンクからの支出をいつでも承認できます。
② ITXへリクエストをリレーする
ITXのrelay_sendTransactionというRPCを呼び出して、トランザクションリレーをリクエストする。ITXはまずDepositerのITXガスタンクに十分な残高があるかどうかを確認し、次に残高の一部をロックして、トランザクションをイーサリアムネットワークにリレーします。
③ トランザクションがマイニングされ、DepositerのITXガスタンクからガス代金が差し引かれる
トランザクションがマイニングされてブロックチェーンの一部になり次第、(gas price * gas used) + the ITX feeをDepositerのITXガスタンクからガス代金を差し引きます。
ITX API
ITX APIは、標準のイーサリアムクライアントインターフェイスのJSON-RPC拡張機能として実装されています。
① relay_getBalance
on-chain deposit contrにデポジットしたETH(ITXガスタンク)の残高の照会となります。
② relay_sendTransaction
Depositerの署名済みのトランザクションリレーのリクエストをITXサービスへ送信します。
③ Relay_getTransactionStatus
リレーされたトランザクションのステータスを取得します。
ITXサービスは、リレーリクエストを受け取り、それをイーサリアムトランザクションに詰め、トランザクションがマイニングされるまで徐々にGasPriceを引き上げます。 イーサリアムトランザクションハッシュは、毎回GasPriceが上がるたびに変更され、前のハッシュはそのステータスをトラッキングするための信頼性がなくなりますので、Ethereumトランザクションハッシュのリストを返します。
ITX のデモ
ITXを自作のデモのwebウォレットに統合したので、RinkebyテストネットでのEIP3009に準拠したGYENの送金をしてみましょう。
① まずはDepositerのITXガスタンクの残高を確認してみます。
最終のトランザクションのfromアドレス(ITX relayer address)は、常にITXサービス内部のウォレットアドレス(ITXシステムによって選択された)に設定されますので、ユーザーは指定できません。
② トランザクションのリクエストをITXリレーによりイーサリアムネットワークに出す
トランザクションはここで確認が出来ました。fromアドレスの0x7dcfcb9129a32ea1c9b6125b97495396f52f36d8はITXの内部管理しているアカウントとなります。
ITX のデモのソースコード
githubでソースコードを確認できます。
疑似コードで要約すると、以下のようとなります。
async postbyITX(req, res, next) { // Create the transaction relay request const tx = { // Address of the contract we want to call to: process.env.GYEN, // Encoded data payload representing the contract method call data: data, // An upper limit on the gas we're willing to spend gas: '100000' }; // 署名するメッセージのハッシュを取得する const relayTransactiondataHash = ethers.utils.keccak256( ethers.utils.defaultAbiCoder.encode( ['address', 'bytes', 'uint', 'uint'], [tx.to, tx.data, tx.gas, chainId] // Rinkeby chainId is 4 ) ); // Depositerはトランザクションのリクエストを署名することで、ITXガスタンクからのガス費用を負担することを承認すること。 const signature = await signer.signMessage(ethers.utils.arrayify(relayTransactiondataHash)) // トランザクションをイーサリアムネットワークにリレーする。(nonce、gaspriceを顧慮することなくなり、楽) relayTransactionHash = await itx.send('relay_sendTransaction', [tx, signature]); }
3-3.ITXの課題
まだ正式版ではないですが、デモ通して、見られた課題をまとめてみます。
① ITXはtransaction relay requestの中身をチェックしないまま直接にイーサリアムに送ることで、中身のエラーにより、トランザクションは失敗となる場合があります。ガスの浪費となるかと思っています。もちろん、relay_sendTransactionを呼び出す前には、開発者は自分のサービスで中身のチェックを行えますが、relay_sendTransactionで統一的に対応できればいいじゃないでしょうか。
② 開発者は自分のサービスでdepositerのプライベントキーでtransaction relay requestを署名する必要があることで、プライベントキーはインターネットにさらされていて、セキュリティ都合でプライベントキーが盗まれて、depositerの所持ETHあるいはトークンが流失となるリスクが秘めています。
③ ユーザーはあるdeadline前に(例えば、○ブロック数以内、現在のSignerのNonce+○以内など)トランザクションを処理させたいという要望は答えていなそうです。deadline前に処理されないと、ユーザーへの補償の対策も見られていないです。
④ ITXのリレー手数料は正式版でどうなりますか。自作リレーサービスの利用より経済性がなければ、ITXの採用を猶予する、または見送ることとなりかねないですね。
⑤ ITXは単一障害点の問題が抱えていますか。分散リレーサービスがどのように実現できるか見守っていきます。
⑥ 適切なGasPriceでトランザクションを処理させることを目指していますが、もしユーザーの希望するGasPriceと合わせることができればと思います。
(続きます)
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD