ブロックチェーンにおけるレイヤー1とレイヤー2間の資産送金手法の実践

この記事は[NTTコムウェア AdventCalendar 2024](https://qiita.com/advent-calendar/2024/nttcomware) 16日目の記事です。

# はじめに

NTTコムウェアの管です！
システム設計から開発・運用まで幅広い経験を持つ、キャリア7年目のフルスタックエンジニアです。最新技術に強い関心を持ち、現在はWeb3.0技術検証ワーキンググループのリーダーを務めています。

## 記事の対象者

本記事は、Web3領域の基本知識を持つ以上の方を対象としています。

## きっかけ

私は最近、ブロックチェーンのレイヤー1とレイヤー2間の相互運用性について興味を持ち、様々な検証を行ってきました。この記事では、その過程で得た送金処理に関する学習ノウハウを共有したいと思います。

---

## 対象者

本記事は、ブロックチェーンの基礎知識とスマートコントラクト開発の経験をお持ちの方向けの内容となっています。初心者の方には難しい内容を含む場合があります。


::: note info

ブロックチェーンのスケーラビリティ問題の解決には、レイヤー1（L1）とレイヤー2（L2）の活用が不可欠です。本記事では、Ethereum（L1）とOptimism（L2）間での資産送信について解説します。

:::
---
---

## 1. レイヤー1とレイヤー2の概要

### レイヤー1（L1）とは

Ethereumメインネットなどの基盤となるブロックチェーンのことを指します。L1は優れたセキュリティと分散性を備えていますが、トランザクション手数料（Gas代）が高額で、処理速度にも制限があります。

### レイヤー2（L2）とは

L2は、L1のスケーラビリティを向上させるために開発されたソリューションです。Optimistic RollupやZK Rollupなどがその代表例です。L2では、トランザクションをL2上で処理し、必要な情報のみをL1に送信することで効率化を図ります。

---

## 2. 必要なツールと準備

- **Node.js**: 開発環境を整えるために使用
- **[Remix](**https://remix.ethereum.org/**):** コントラクト開発・デプロイに利用
    - **Hardhat**を使っても構いません
- **Metamask**: テストネットの資産管理に利用
- **ブリッジ**: レイヤー1とレイヤー2間の資産を転送するための利用
  ```shell
    - ETH Sepolia messenger - L1 0x58Cc85b8D04EA49cC6DBd3CbFFd00B4B8D6cb3ef
    - OP Sepolia messenger - L2 0x4200000000000000000000000000000000000007
  ```
## 3. 事前準備:

::: note info

OP SepoliaのETHを取得することが難しい場合、テストネットのブリッジ機能利用がおすすめです。
https://docs.optimism.io/builders/tools/build/faucets#bridge-from-sepolia
:::

1. Ethereumテストネット（Sepolia）でL1のETHを取得
2. Optimismテストネット（Op Sepolia）でL2のETHを取得
3. 上記のネットワークをMetamaskに追加

---

## 4. Solidityで受送金コントラクトを実装

以下に、シンプルなETH送信用のスマートコントラクトを実装し
まず、 `ICrossDomainMessenger`というインターフェースを定義します。

```solidity
interface ICrossDomainMessenger {
    function xDomainMessageSender() external view returns (address);

    function sendMessage(
        address target,
        bytes calldata message,
        uint32 gasLimit
    ) external payable;
}
```

::: note info
「xDomainMessageSender」は、実際には「通信ブリッジ」そのものではなく、「通信ブリッジによって転送されたメッセージのオリジナル送信者」を取得するための機能（関数）です。
つまり、「sendMessage」だけだと元送信者がわからないので、元送信者を知りたい場合、「xDomainMessageSender」が必要です。
:::

次、以下のように送信用なコントラクトを作成します。

```solidity
contract Wallet {

    address public immutable MESSENGER;

    constructor(address messenger) {
        MESSENGER = messenger;
    }

    receive() external payable {}
    function send(address remote_wallet) external payable {
        ICrossDomainMessenger(MESSENGER).sendMessage{value: msg.value}(
            remote_wallet,
            "",
            200000
        );
    }

    function get_balance() external view returns (uint256) {
        return address(this).balance;
    }

    function withdraw() external {
        (bool ok, ) = msg.sender.call{value: address(this).balance}("");
        require(ok, "Withdraw failed");
    }
}
```

- `send`は、送信処理を行う関数です。
- `receive`はブリッジコントラクトのアドレスを指定します。
- `get_balance`は、ウォレットが保有する仮想通貨を確認する関数です。
- `withdraw`は、送信処理を行う関数です。
---
## 5. Remixを用いて、テストネットにデプロイ

:::note warn
※ ネットワークを切り替えた後、Remixの反映に時間がかかる場合があるため、切り替えが完了したことを確認してからデプロイすることをお勧めします。
:::

### 1. Sepolia、Op Sepoliaテストネットにデプロイ
単純な流れは以下の感じです。
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/f7fc5f35-a9a4-b1ad-f918-ad92f3e4432c.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/4c3400a9-6205-6b99-1f59-3c8fb38a3512.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/35fe81ed-d167-387f-e09a-5e62688b6c72.png)|
|---|---|---|
|Step1 スマートコントラクトをビルド|Step2 Sepoliaテストネットにデプロイ|Step3 Op Sepoliaテストネットにデプロイ|

出典：Ethereum Foundation, Remix-Ethereum IDE, https://remix.ethereum.org/ （参照：2024/12/07）


:::note info
ポイント
`Step2` デプロイの際に、独自のMetaMask walletを接続して、テストネットをSepoliaに設定してください。
`Step2` Deploy MESSENGERは「ETH Sepolia messenger」の情報を記入して、「transact」してください。
`Step3` デプロイの際に、独自のMetaMask walletを接続して、テストネットをOp Sepoliaに設定してください。
`Step3` Deploy MESSENGERは「Op Sepolia messenger」の情報を記入して、「transact」してください。
:::


## 6. テストネットでの送金試験
テスト前に、各レイヤーのコントラクトの状況を確認します。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/f251ff7e-bca7-b1e8-961f-5dc317cc2f8e.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/05ae6c7b-9500-fd5c-0b00-bba3e1357007.png)|
|---|---|
|Sepoliaのコントラクトの通貨が 1 wei|Op Sepoliaのコントラクトの通貨が 0 wei|

出典：Ethereum Foundation, Remix-Ethereum IDE, https://remix.ethereum.org/ （参照：2024/12/07）

### 1. L1からL2に送金

SepoliaネットのスマートコントラクトからOP Sepoliaへ送金を行います。
まず、送信する金額として1 Weiを入力します。
次に、スマートコントラクトのSend関数に「OP Sepoliaコントラクトのアドレス」を入力し、「send」ボタンを押します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/8069f291-2ff8-35dc-38da-ff891857c325.png)

<details><summary>ログ</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/a7681b98-d116-03b0-ac0c-0175d9919c5d.png)

</details>

出典：Ethereum Foundation, Remix-Ethereum IDE, https://remix.ethereum.org/ （参照：2024/12/07）

#### 送金結果確認
以下の比較見ると、Op Sepoliaのコントラクトの通貨が1 weiが増やしましたね。
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/05ae6c7b-9500-fd5c-0b00-bba3e1357007.png)|⇒|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/d6e4d2b1-a9f5-0464-39b1-a7a92e25068b.png)|
|---|---|---|
|送金前の状態|⇒|送金後の状態|

出典：Ethereum Foundation, Remix-Ethereum IDE, https://remix.ethereum.org/ （参照：2024/12/07）

### 1. L2からL1に送金

最初のスマートコントラクトの操作手順は「L1からL2に送金」と同様のため、ここでは説明を省略します。
L2からL1への送金はRollup技術を使用し、L1側での承認（検証）期間が必要となります。通常、この処理はL2側のバッチ処理が定期的に実行しますが、テストネットを使用する今回は手動での処理が必要です。
以下で具体的な処理手順を説明します。

まず、処理用のJavaScriptを作成します。

処理用なJavascriptを作成します。
```javascript:batch.js
require('dotenv').config({ path: '.env' })

const optimism = require("@eth-optimism/sdk")
const ethers = require("ethers")

const PRIVATE_KEY = process.env.PRIVATE_KEY
const L2_TX = process.env.L2_TX

const L1_RPC = "https://rpc.ankr.com/eth_sepolia"
const L2_RPC = "https://sepolia.optimism.io"
// 11155111 for Sepolia
const L1_CHAIN_ID = 11155111
// 11155420 for OP Sepolia
const L2_CHAIN_ID = 11155420

// send L2 -> L1
async function main() {
    // Create RPC providers and wallet
    const l1_provider = new ethers.providers.StaticJsonRpcProvider(L1_RPC)
    const l2_provider = new ethers.providers.StaticJsonRpcProvider(L2_RPC)
    const l1_wallet = new ethers.Wallet(PRIVATE_KEY, l1_provider)
    const l2_wallet = new ethers.Wallet(PRIVATE_KEY, l2_provider)

    // Create CrossChainMessenger instance
    const messenger = new optimism.CrossChainMessenger({
        l1ChainId: L1_CHAIN_ID,
        l2ChainId: L2_CHAIN_ID,
        l1SignerOrProvider: l1_wallet,
        l2SignerOrProvider: l2_wallet,
    })

    // Wait until message is ready to prove
    console.log("Wait for message status...")
    await messenger.waitForMessageStatus(
        L2_TX,
        optimism.MessageStatus.READY_TO_PROVE
    )

    // Prove the message on L1
    console.log("Prove message on L1...")
    await messenger.proveMessage(L2_TX)

    console.log("Wait for message status...")
    await messenger.waitForMessageStatus(
        L2_TX,
        optimism.MessageStatus.IN_CHALLENGE_PERIOD
    )

    // Wait until the message is ready for relay
    console.log("Wait for message status...")
    await messenger.waitForMessageStatus(
        L2_TX,
        optimism.MessageStatus.READY_FOR_RELAY
    )

}

main()
```

環境変数ファイルは以下の感じで
```shell
PRIVATE_KEY="自分のウォレット秘密鍵"
L2_TX="トランザクションのhash値"
```
実施コマンド
```shell
node batch.js
```
実行ログ
```shell
Message Status: 2
Wait for message status...
Message Status: 3
Wait for message status...
Message Status: 3
Wait for message status...
Message Status: 4
Wait for message status...
```
上記のステータス遷移から、トランザクションはチャレンジ期間に入っています。7日間待つとStatus: 5になります。

```javascript:finalize.js
require('dotenv').config({ path: '.env' })

const optimism = require("@eth-optimism/sdk")
const ethers = require("ethers")

const PRIVATE_KEY = process.env.PRIVATE_KEY
const L2_TX = process.env.L2_TX

const L1_RPC = "https://rpc.ankr.com/eth_sepolia"
const L2_RPC = "https://sepolia.optimism.io"
// 11155111 for Sepolia
const L1_CHAIN_ID = 11155111
// 11155420 for OP Sepolia
const L2_CHAIN_ID = 11155420

// send L2 -> L1
async function main() {
    // Create RPC providers and wallet
    const l1_provider = new ethers.providers.StaticJsonRpcProvider(L1_RPC)
    const l2_provider = new ethers.providers.StaticJsonRpcProvider(L2_RPC)
    const l1_wallet = new ethers.Wallet(PRIVATE_KEY, l1_provider)
    const l2_wallet = new ethers.Wallet(PRIVATE_KEY, l2_provider)

    // Create CrossChainMessenger instance
    const messenger = new optimism.CrossChainMessenger({
        l1ChainId: L1_CHAIN_ID,
        l2ChainId: L2_CHAIN_ID,
        l1SignerOrProvider: l1_wallet,
        l2SignerOrProvider: l2_wallet,
    })

    // Get messages from L2 transaction　status

    const receipt = await l2_provider.getTransactionReceipt(L2_TX);

    const messages = await messenger.getMessagesByTransaction(receipt)

    for (const message of messages) {
        const status = await messenger.getMessageStatus(message);
        console.log(`Message Status: ${status}`);
    }

    // Relay the message on L1
    console.log("Finalize...")
    await messenger.finalizeMessage(L2_TX)

    // Get messages from L2 transaction　status
    const messages_finalizeMessage = await messenger.getMessagesByTransaction(receipt)

    for (const message of messages_finalizeMessage) {
        const status = await messenger.getMessageStatus(message);
        console.log(`Message Status: ${status}`);
    }

    // Wait until the message is relayed
    console.log("Wait for message status...")
    await messenger.waitForMessageStatus(L2_TX, optimism.MessageStatus.RELAYED)
}

main()
```
上記のfinalize.jsを実行することで、ステータスが5から6へと遷移します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3626247/107e5cc9-5f5b-e4e7-e851-768f1024429e.png)
上記のブロック状態を確認するScanサイドは以下です。

https://optimism-sepolia.blockscout.com/

ちなみに、数字との関係は以下の感じです。
```shell
Optimism Sepolia L1 to L2 Testnet STATUS CODES
- UNCONFIRMED_L1_TO_L2_MESSAGE (0): The message is an L1 to L2 message and has not been processed by L2.
- FAILED_L1_TO_L2_MESSAGE (1): The message is an L1 to L2 message, and the transaction to execute the message failed.
- STATE_ROOT_NOT_PUBLISHED (2): The message is an L2 to L1 message, and no state root has been published yet.
- READY_TO_PROVE (3): The message is ready to be proved on L1 to initiate the challenge period.
- IN_CHALLENGE_PERIOD (4): The message is a proved L2 to L1 message and is undergoing the challenge period.
- READY_FOR_RELAY (5): The message is ready to be relayed.
- RELAYED (6): The message has been relayed.
```

---

## 7. まとめ

今回はブロックチェーンのレイヤー1（L1）とレイヤー2（L2）間での資産送金について検証を行い、その具体的な手順と得られた知見を共有しました。今後の課題として、Op Sepoliaテストネットでの長い処理時間に対応するため、独自のテストネット環境を構築して検証を進めていく予定です。

※本稿に記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。