Symbol Bridge 概要
NEM/SymbolとEthereum間でトークンを相互変換できる双方向ブリッジシステムです。
3つの動作モード
1. Wrapモード(ラップ)
- XEMやXYMを他のネットワークのwXYMなどに変換
- 1:1の固定レートで交換
- 逆操作(アンラップ)も可能
2. Stakeモード(ステーキング)
- XEMやXYMをsXYMなどに変換
- ステーキング報酬を得られる
- 変動レート(報酬により増える)
- アンステーク時、元本+報酬を受け取れる
3. Swapモード(スワップ)
- XEMやXYMをETHに交換
- オンライン価格情報を使用
- 一方向のみ(逆操作不可)
仕組み
- ユーザーがブリッジのアカウントにトークンを送金
- メッセージに送付先アドレスを記載
- ブリッジが自動的に反対側のネットワークでトークンを送信
4つのコアワークフロー(定期実行スクリプト)
- 残高変更のダウンロード - ブリッジアカウントの残高更新を取得
- リクエストの検出 - wrap/unwrapリクエストを検証・保存
- 支払い実行 - 指定アドレスへトークンを送信
- 完了確認 - トランザクションの確定を監視
要するに、異なるブロックチェーン間でトークンを安全に移動させる自動化システムです!
環境設定
Linux 環境を想定
前提条件
ブリッジはPythonで書かれているため、Pythonが必要です。
Python3、pip、OpenSSLの開発ライブラリをインストール
$ sudo apt-get update
$ sudo apt-get install python3 python3-pip libssl-dev
仮想環境の作成 ( オプション )
プロジェクト専用のPython環境を作成(推奨)
- venvモジュールをインストール
- ブリッジディレクトリに移動
- 仮想環境を作成
- 仮想環境を有効化(. venv/bin/activateコマンド)
$ sudo apt-get install python3-venv
$ git clone https://github.com/symbol/product.git
$ cd product/bridge
$ python3 -m venv venv
$ . venv/bin/activate
インストール
$ python3 -m pip install -r requirements.txt
手動操作
ブリッジの実行
以下のスクリプトをsymbol-product-directory/bridgeから、記載された順序で定期的に実行する必要があります:
$ . ./venv/bin/activate
$ python3 -m workflows.download_balance_changes --config configuration.ini
$ python3 -m workflows.download_wrap_requests --config configuration.ini
$ python3 -m workflows.download_wrap_requests --config configuration.ini --unwrap
$ python3 -m workflows.send_payouts --config configuration.ini
$ python3 -m workflows.send_payouts --config configuration.ini --unwrap
$ python3 -m workflows.check_finalized_transactions --config configuration.ini
$ python3 -m workflows.check_finalized_transactions --config configuration.ini --unwrap
Swapモード使用時の注意
ネイティブXYMをネイティブETHに交換するSwapモードで動作する場合は、--unwrapパラメータを含むコマンドをすべてスキップしてください。このモードでは、ブリッジは一方向のみで動作します。
設定ファイル
すべてのコマンドには、--configパラメータで指定する設定ファイルが必要です。このファイルに必要な内容は、Configurationセクションで説明されています。
実行頻度
スクリプトの実行頻度が高いほど、ブリッジリクエストの処理が速くなりますが、ハードウェアの使用量も増加します。サポートされているネットワークの平均ブロック時間を考慮すると、15秒ごとにスクリプトを実行するのが良いバランスです。
APIの公開
ブリッジは、外部システムがステータスを照会できるHTTP APIを公開しています。例えば、このAPIは特定のアカウントの保留中のwrapおよびunwrapリクエストのリストを返したり、過去のエラーの詳細を提供したりできます。このAPIのOpenAPI仕様は近日中に提供される予定です。
APIの起動
APIはFlaskを使用して開発されています。公開するには、以下を実行してください:
$ . ./venv/bin/activate
$ export FLASK_RUN_PORT=5000
$ export BRIDGE_API_SETTINGS=/path/to/api_configuration.ini
$ FLASK_APP=bridge.api:create_app FLASK_ENV=development PYTHONPATH=../. python3 -m flask run
API設定ファイル
APIには独自の設定ファイル(上記コマンドのapi_configuration.ini)が必要です:
CONFIG_PATH="<path_to_bridge_configuration.ini>"
説明
- ポート設定: FLASK_RUN_PORT=5000でAPIのポート番号を指定
- 設定ファイルパス: BRIDGE_API_SETTINGS環境変数でAPI設定ファイルの場所を指定
- 開発モード: FLASK_ENV=developmentで開発環境として実行
- 設定ファイルの内容: メインのブリッジ設定ファイル(configuration.ini)へのパスを指定
これにより、ブリッジの状態をHTTP経由で照会できるAPIサーバーが起動します。
Dockerを使用した実行
依存関係を手動でインストールしてスクリプトを実行する代わりに、Dockerを使用することもできます。
Dockerイメージのビルド
$ docker build -t symbolplatform/bridge:1.1 -f Dockerfile --network host ..
ブリッジの実行
$ docker run -d -it --name xym_wxym_bridge --restart always -v $(pwd):/data symbolplatform/bridge:1.1 <command>
<command>の指定
<command>は以下のいずれかの値でなければなりません:
- wrappedflow: wrapとunwrapフローを実行します。WrapモードとStakeモードで使用します。
- nativeflow: --unwrap操作をスキップします。Swapモードで使用します。
- api: APIサーバーを起動します。APIポートを公開するために-p 5000:5000も追加する必要があります。実際にブリッジを起動する他の2つのコマンドのいずれかと併用して実行する必要があります。
注意事項
$(pwd):/data: ホストの現在の作業ディレクトリをコンテナ内の/dataディレクトリにマウントします。
デフォルトでは、ブリッジは/data/config/configuration.iniで設定ファイルを探します。このパスは、BRIDGE_CONFIG_PATH環境変数を設定することで上書きできます。例:
$ docker run -d -it --name xym_wxym_bridge --restart always -v $(pwd):/data -e BRIDGE_CONFIG_PATH="/data/configuration/custom_path_to_config.ini" symbolplatform/bridge:1.1 <command>
実行例
# Wrapモード/Stakeモードの場合
$ docker run -d -it --name xym_wxym_bridge --restart always -v $(pwd):/data symbolplatform/bridge:1.1 wrappedflow
# APIサーバーの起動
$ docker run -d -it --name xym_wxym_api --restart always -v $(pwd):/data -p 5000:5000 symbolplatform/bridge:1.1 api
ワークフロー
残高変更のダウンロード(Download Balance Changes)
このワークフローは、最後に処理されたブロックから最新の確定ブロックまでの、すべてのネイティブチェーンのブロックを取得します。各ブロックは解析され、ブリッジのネイティブアカウントへの残高変更は、以下の情報とともにblock_changesデータベースに保存されます:
- 残高変更が発生したブロック高
- 通貨(またはモザイクID)
- 変更額(正または負)
これらの記録は、ブリッジアカウント内のネイティブトークンの量を常に追跡するために使用され、これによりステーキングトークンの交換レートが決定されます。
初期セットアップでは、balanceChangeScanStartHeight設定プロパティを使用して、ブリッジアカウントが存在する前に作成されたブロックをスキップします。
最後に、次回の実行で既に処理済みのブロックを再処理しないように、最後の確定(処理済み)ブロックの高さにダミーエントリが追加されます。このエントリは空の通貨と金額を持ちます。
Wrapリクエストのダウンロード(Download Wrap Requests)
このワークフローは2つの方向で動作します:
- wrap(デフォルト): ネイティブチェーン上のリクエストトランザクションを検出し、wrap_requestデータベースに保存します。
- unwrap: ターゲットチェーン上のリクエストトランザクションを検出し、unwrap_requestデータベースに保存します。
これを行うために、スクリプトは最後に記録されたリクエスト(またはエラー)の後から最新の確定ブロックまで、関連するブリッジアカウントに送信されたすべてのトランザクションを取得します。
有効なリクエストの条件
有効なリクエストは、以下の条件を満たすトランザクションです:
- ネイティブトークン(wrap方向)またはラップされたトークン(unwrap方向)を転送する。他のトークンは無視されます。
- 交換されたトークンを配信する宛先アドレスを含む:
- NEMとSymbolのトランザクションの場合:アドレスは暗号化されていないメッセージフィールドで提供する必要があります。
- **Ethereumトランザクション(unwrapのみ)**の場合:アドレスはトランザクションのinputデータに16進数値として提供する必要があります。
ethers.jsを使用したEthereum unwrapの例:
const provider = new ethers.JsonRpcProvider(RPC_URL);
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
const ERC20_ABI = [
'function transfer(address to, uint amount) public returns (bool)',
];
const token = new ethers.Contract(TOKEN_CONTRACT_ADDRESS, ERC20_ABI, wallet);
const amountInWei = ethers.parseUnits(AMOUNT.toString(), TOKEN_DECIMALS);
const recipientAddressHexadecimal = // base32のSymbol/NEMアドレスを16進数に変換
// transfer関数をエンコード
const iface = new ethers.Interface(ERC20_ABI);
const transferData = iface.encodeFunctionData('transfer', [BRIDGE_ADDRESS, amountInWei]);
// transferデータ + Symbol/NEMアドレスを結合
const fullData = transferData + recipientAddressHexadecimal;
const tx = await wallet.sendTransaction({
to: TOKEN_CONTRACT_ADDRESS,
data: fullData,
gasLimit: // ガス見積もりを計算,
gasPrice: // ガス価格を計算
});
マルチシグとアグリゲートトランザクション
マルチシグ(NEM)およびアグリゲート(Symbol)転送がサポートされています。アグリゲートには、有効なリクエストである複数の内部転送トランザクションを含めることができます。これらを区別するために、ワークフローはアグリゲートトランザクションと内部トランザクションインデックスで構成されるタプルを一意の識別子として使用します。
リクエストの保存
有効なリクエストはUNPROCESSEDステータスでwrap_requestテーブルに保存され、無効なリクエストはエラーとして記録されます(関連するトークンは寄付とみなされます)。すべてのリクエストが処理された後、すべての有効なリクエストの高さが収集され、それぞれのタイムスタンプが取得されます。これらのタイムスタンプはblock_metadataテーブルに保存されます。
支払いの送信(Send Payouts)
このワークフローも2つの方向で動作します:
- wrap(デフォルト): wrap_requestデータベースからリクエストを処理します。
- unwrap: unwrap_requestデータベースからリクエストを処理します。
ワークフローの流れ
- 該当するデータベースから、すべてのUNPROCESSED(未処理)リクエストを取得
- リクエストのブロック高での変換レートを計算(下記参照)
- 支払い額からネットワーク手数料を差し引く
- 受取人にトークンを送信
- リクエストのステータスをSENT(送信済み)またはFAILED(失敗)に更新
変換レートの計算
変換レートはglobal.mode設定によって異なります。
1. stakeモード
変換レートは以下の式で計算されます:
変換レート = (total_staked - total_unstaked) / native_balance
各変数の意味:
- total_staked: ブリッジが稼働開始以降に生成したステーキングトークンの総量。ブリッジが内部で追跡。
- total_unstaked: ブリッジが稼働開始以降にネイティブトークンに戻したステーキングトークンの総量。ブリッジが内部で追跡。
- native_balance: ネイティブネットワーク上のブリッジアカウントの現在残高。
ブリッジのネイティブアカウントに報酬が発生しない場合、その残高はstakeとunstake操作の結果としてのみ変化します。その場合、native_balanceは常にtotal_staked - total_unstakedに等しく、変換係数は1のままです。この条件下では、stakeモードはwrapモードとまったく同じように動作します。
例
- ネイティブ残高:12,000
- ステーキングトークン総量:10,000
- アンステークトークン総量:2,000
これは、8,000のステーキングトークンが残っており、その所有者は12,000のネイティブトークンを引き出す権利があることを意味します。
8,000のステーキングトークンとブリッジが保有する12,000のネイティブトークンの差は、ハーベスティング収入、寄付、無効な転送などの累積報酬を表しています。
これらの値から変換レートは以下のように計算されます:
(10,000 - 2,000) / 12,000 = 2/3 ≈ 67%
したがって:
- 1,200ネイティブトークンを預ける人は800ステーキングトークンを受け取り、変換レートは変わりません。なぜなら(10,800 - 2,000) / 13,200もやはり2/3だからです。
- 800ステーキングトークンを預ける人は1,200ネイティブトークンを受け取り、変換レートは変わりません。なぜなら(10,000 - 2,800) / 10,800もやはり2/3だからです。
- 4,000ネイティブトークンのハーベスティング報酬が発生すると、変換レートは(10,000 - 2,000) / 16,000 = 50%に調整されます。
これで、各ステーキングトークンは以前よりも多くのネイティブトークンの価値を持つようになります。なぜなら、所有者は新たに発生した報酬の比例配分を受け取る権利があるからです。
同時に、預けられたネイティブトークンは、より少ないステーキングトークンになります。なぜなら、ネイティブ残高の増加により、各ステーキングトークンがより大きな価値のシェアを表すようになるからです。
2. wrapモード
変換レートは1:1で固定されています。1つのラップされたネットワークトークンは、常に1つのネイティブネットワークトークンに等しくなります。
3. swapモード
変換は、オンライン価格プロバイダーからの為替レートを使用して動的に決定されます。
注意事項
wrapモードとstakeモードでは、手数料控除を計算するために、両チェーン上のネイティブトークン間の変換レートが依然として価格オラクルから取得されます。
確定トランザクションのチェック(Check Finalized Transactions)
このワークフローは、未確認の支払いトランザクションを監視し、ネットワーク上で確定されると、そのステータスを更新します。wrapとunwrapの両方向で動作し、それぞれのリクエストデータベースを処理します。
簡略化されたトランザクションフローと手数料の取り扱い
Symbol(XYM)をネイティブチェーン、Ethereum(wXYM)をラップチェーンとしたstakeフローの例:
1. 預け入れ(Deposit)
- AliceはXYMをwXYMに変換するために、ネイティブネットワーク(Symbol)上のブリッジアカウントに100 XYMを送信します。
- 彼女はSymbolネットワークのトランザクション手数料を支払います。
- ブリッジはこのトランザクションを開始せず、手数料を支払いません。
2. ラップトークンの支払い(Wrapped token payment)
- send_payoutsスクリプト(wrap方向)がAliceの預け入れを検出します。
- 要求されたアカウントに (δ × 100 - μ) wXYMを送信するEthereumトランザクションが作成されます。
- δ はwXYM:XYMの変換レートです。
- μ はEthereumトランザクション手数料です。
- これはETHで支払われますが、wXYMから差し引かれます。
- 為替レートリスクがあるため、価格オラクルが使用されます。
3. 償還(Redemption)
- AliceはwXYMをXYMに戻すために、ラップネットワーク(Ethereum)上のブリッジアカウントに99 wXYMを送信します。
- 彼女はEthereumネットワークの手数料を支払います。
- ブリッジはこのトランザクションを開始せず、手数料を支払いません。
4. ネイティブトークンの支払い(Native token payment)
- send_payoutsスクリプト(unwrap方向)がAliceの償還リクエストを検出します。
- 要求されたSymbolアカウントに (1/δ × 99 - μ) XYMを送信するSymbolトランザクションが作成されます。
- δ はwXYM:XYMの変換レートです。
- μ はネイティブ(Symbol)ネットワークのトランザクション手数料です。
- これはXYMで支払われ、XYMから差し引かれます。
- 為替レートリスクはありません。
まとめ
このワークフローにより:
- ユーザーは預け入れ時と償還時に各ネットワークの手数料を負担
- ブリッジが送信するトークンからブリッジのトランザクション手数料(μ)が差し引かれる
- Ethereumの手数料はETHで支払うがwXYMで控除されるため、価格オラクルが必要
設定(Configuration)
ブリッジは5つのセクションを持つINI設定ファイルを使用します:machine、global、native_network、wrapped_network、price_oracle
machine セクション
- logFilename: ブリッジの運用ログが書き込まれるファイルパス。
- databaseDirectory: ブリッジの内部データベース(リクエストや支払い状態の追跡など)を保存するディレクトリ。
- logBackupCount: 保持するバックアップログファイルの最大数。ログファイルが指定されたサイズ(maxLogSize)を超えるとローテーションされます。バックアップファイル数がこの制限を超えると、古いバックアップは削除されます。
- maxLogSize: ログファイルがローテーションされるまでの最大サイズ(バイト単位)。このサイズに達すると、現在のログがアーカイブされ、新しいログファイルが開始されます。
global セクション
- mode: ブリッジの動作モードを指定(stake、wrap、またはswap)。
native_network と wrapped_network セクションに共通のプロパティ
これらのプロパティはnative_networkとwrapped_networkセクションで共有されており、設定ファイルに2回記述できます。
- blockchain: ブロックチェーンの種類(symbol、nem、ethereum)。Ethereumは[wrapped_network]でのみ利用可能。
- network: ネットワーク環境(testnet、mainnet)。
- endpoint: ネットワークのREST APIノードエンドポイント。
- bridgeAddress: wrapまたはunwrap操作をトリガーするためにトークンを送信すべきアドレス。
- signerPrivateKey: 支払いトランザクションに署名するための秘密鍵。
- signerPublicKey: signerPrivateKeyに対応する公開鍵。
- mosaicId: トークン識別子のフォーマットはブロックチェーンによって異なります:
- NEM: {namespace name}:{mosaic name}
- Symbol: {hex mosaic id}
- Ethereum: ラップトークンのERC-20コントラクトアドレス。ネイティブトークン(ETH)の変換(swapモード)の場合は空にする必要があります。
- explorerEndpoint: ブロックエクスプローラーのURL。
- finalizationLookahead: 概念的な確定を進めるブロック数(デフォルト:0)。例えば、NEMの確定には360ブロックかかります。このプロパティを350に設定すると、10ブロック後に確定したと見なされます。
- percentageConversionFee: すべてのwrapおよびunwrap操作の一定割合が手数料としてブリッジに保持されます(デフォルト:0)。0から1の間の数値である必要があります。
- unconfirmedWaitTimeSeconds: ブリッジがネイティブネットワーク上でトランザクションが確認されるまで待機する時間(秒単位)(デフォルト:60)。
- transactionFeeMultiplier: Symbolネットワークでトランザクション手数料を計算するために使用される乗数。
- maxTransferAmount: このネットワーク上の単一転送操作で許可されるトークンの最大量。
native_network セクション
ネイティブネットワークのプロパティ。
- balanceChangeScanStartHeight: ブリッジが残高変更(bridgeAddressへの預け入れ)のスキャンを開始するブロック高。これは最適化です:最高のパフォーマンスを得るには、ブリッジアドレスが作成される前のブロックに設定してください。
- rosettaEndpoint: (NEMのみ)NEM rosettaエンドポイント。NEMはAPIとRosettaリクエストを異なるポート経由で提供するため、これが必要です。
wrapped_network セクション
ラップネットワークのプロパティ。
Ethereumはラップネットワークとしてのみサポートされているため、すべてのEthereum固有のプロパティはwrapped_networkセクションに属します:
- chainId: EthereumネットワークのチェーンID(数値)。
- isFinalizationSupported: Ethereumネットワークが確定をサポートしているかどうかを示します(デフォルト:false)。
- gasMultiple: 推定ガスリミットの乗数(デフォルト:1.15)。
- gasPriceMultiple: 推定ガス価格(レガシートランザクション)またはベース手数料の乗数(デフォルト:1.2)。
- priorityFeeMultiple: EIP-1559トランザクションの優先手数料(チップ)の乗数(デフォルト:1.05)。
- feeHistoryBlocksCount: EIP-1559ガス価格推定のために手数料履歴を取得する際に考慮する過去のブロック数(デフォルト:10)。
price_oracle セクション
- url: 価格オラクルAPI(例:CoinGecko、CoinMarketCap)のURL。クロスチェーンの価値検索が必要な変換時にリアルタイムのトークン価格を取得するために使用されます。
- accessToken: 価格オラクルAPIのアクセストークン。
設定例
[machine]
databaseDirectory = /home/ubuntu/product/bridge/xym_wxym_bridge/storage/db
logFilename = /home/ubuntu/product/bridge/xym_wxym_bridge/storage/log.log
logBackupCount = 30
maxLogSize = 26214400
[global]
mode = stake
[native_network]
blockchain = symbol
network = testnet
endpoint = https://201-sai-dual.symboltest.net:3001
bridgeAddress = TBQAAMLT4R6TPIZVWERYURELILHHMCERDWZ4FCQ
explorerEndpoint = https://testnet.symbol.fyi
mosaicId = 72C0212E67A08BCE
signerPrivateKey = <削除済み>
signerPublicKey = BA3E3AD78C1B57604345845F7D8466F3270754B98203D8C72C75994292123AF5
balanceChangeScanStartHeight = 2645824
transactionFeeMultiplier = 100
rosettaEndpoint = http://ocracoke.nemtest.net:4000
[wrapped_network]
blockchain = ethereum
network = testnet
endpoint = https://erigon.symboltest.net:8545
bridgeAddress = 0x9B5b717FEC711af80050986D1306D5c8Fb9FA953
mosaicId = 0x5E8343A455F03109B737B6D8b410e4ECCE998cdA
signerPrivateKey = <削除済み>
signerPublicKey = 044838021ee42c74bcf411ab008ce01bc89356a61cd6dddc78dfcce9cc97bfe66c6380cc7c6228d7d449d8e28125387d4447e9ba7df6708dccfb6f21cdfeaa2eda
chainId = 3151908
isFinalizationSupported = False
explorerEndpoint = https://otterscan.symboltest.net
[price_oracle]
url=https://api.coingecko.com
この設定ファイルをconfiguration.iniとして保存し、ブリッジの実行時に--configパラメータで指定します。