FABRKNT
Cross-Chain Bridges — CCIP から light client まで
Bridge を作る
レッスン 6 / 7·CONTENT22 分55 XP
コース
Cross-Chain Bridges — CCIP から light client まで
レッスンの役割
CONTENT
順序
6 / 7

レッスン5 — Reth 上の最小 bridge を作る(light-client 検証メッセージング)

問い

理論 + 本番コードを読んだ。次は最小の trust 最小化 bridge を実際に組む — Tempo が Ethereum light client を動かし、source イベントの inclusion proof を検証する Ethereum→Tempo フロー。multisig なし、guardian なし、fast-withdrawal LP なし。Bridge は 3 コンポーネントから成る — それぞれが何をして何を信頼するか?

原理(最小モデル)

  • 3 コンポーネント. L1 Bridge Contract(source、何も信頼しない)+ Relayer(permissionless、信頼不要)+ L2 Bridge Contract + Light client(Ethereum コンセンサスのみ信頼)。
  • Trust 仮定 1 つ. Ethereum の PoS が正しく動くこと、それ以外は何もない。
  • L1 Contract = イベント発火のみ. Lock + emit Event、relayer 依存なし、permissionless 観測可能。
  • Relayer = stateless 中継. Locked イベント観測 → inclusion proof 生成 → L2 contract に提出。誰でも動かせる、落ちたら他が引き継ぐ。
  • L2 Light client が 3 メソッド. updateSyncCommittee 期間ごと + addHeader block ごと + verifyInclusion 検証。
  • L2 Bridge contract. Light client.verifyInclusion → wrapped token mint。
  • コスト = ~$6-7 / bridge tx. ZK light client なら ~10 分の 1。
  • 3 落とし穴. Light client trusted setup(初期 checkpoint)+ Replay 防止(claimed mapping)+ Withdrawal 方向(Ethereum で Tempo light client、こちらが難しい)。

具体例

アーキテクチャ:

flowchart LR
    User["User on Ethereum"] -->|1. USDC lock + イベント発火| L1Contract["L1 Bridge Contract"]
    L1Contract -->|2. イベント観測| Relayer["Relayer<br/>(off-chain、誰でも)"]
    Relayer -->|3. Proof 提出| L2Contract["Tempo Bridge Contract"]
    L2Contract -->|4. Ethereum light client に対し検証| LightClient["Ethereum Light Client<br/>on Tempo"]
    LightClient -->|valid| L2Contract
    L2Contract -->|5. Wrapped USDC mint| User2["User on Tempo"]

L1 Contract(Solidity):

contract EthereumBridge {
    mapping(address => mapping(address => uint256)) public locked;

    event Locked(
        address indexed user,
        address indexed token,
        uint256 amount,
        bytes32 indexed destChainId
    );

    function lock(address token, uint256 amount, bytes32 destChainId) external {
        IERC20(token).transferFrom(msg.sender, address(this), amount);
        locked[msg.sender][token] += amount;
        emit Locked(msg.sender, token, amount, destChainId);
    }
}

Relayer(Rust):

use alloy_provider::{Provider, ProviderBuilder};
use alloy_primitives::Address;

#[tokio::main]
async fn main() -> eyre::Result<()> {
    let l1_provider = ProviderBuilder::new()
        .on_http("https://ethereum-rpc.url".parse()?);
    let l2_provider = ProviderBuilder::new()
        .on_http("https://tempo-rpc.url".parse()?);

    // L1 の latest finalized block 取得
    let block = l1_provider
        .get_block(BlockId::finalized())
        .full()
        .await?
        .expect("no finalized block");

    // 最近ブロックの Locked イベント検索
    let logs = l1_provider
        .get_logs(&Filter::new()
            .from_block(block.header.number - 100)
            .address(L1_BRIDGE)
            .event("Locked(address,address,uint256,bytes32)"))
        .await?;

    for log in logs {
        // Inclusion proof 構築
        let proof = build_inclusion_proof(&log, &block).await?;

        // L2 に提出
        let tx = l2_provider
            .send_transaction(TransactionRequest::default()
                .with_to(L2_BRIDGE)
                .with_input(encode_claim_call(&log, &proof)))
            .await?;

        let receipt = tx.get_receipt().await?;
        println!("Submitted claim for {:?}, tx: {:?}",
            log.transaction_hash,
            receipt.transaction_hash);
    }

    Ok(())
}

L2 Light client(Solidity):

contract EthereumLightClient {
    struct Header {
        bytes32 blockRoot;
        uint64 slot;
        bytes32 stateRoot;
        bytes32 receiptsRoot;
    }

    mapping(uint64 => Header) public headers;
    bytes32 public currentSyncCommitteeHash;

    function updateSyncCommittee(
        SyncCommitteeUpdate calldata update
    ) external {
        // 現 committee の 2/3+ で署名されたかを検証
        verifyCommitteeSignature(update);
        // 次期間用に現 committee を更新
        currentSyncCommitteeHash = computeCommitteeHash(update.newCommittee);
    }

    function addHeader(
        Header calldata header,
        bytes calldata signatures
    ) external {
        // 現 committee の 2/3+ で署名されたかを検証
        verifyHeaderSignature(header, signatures, currentSyncCommitteeHash);
        headers[header.slot] = header;
    }

    function verifyInclusion(
        uint64 slot,
        bytes32 leaf,
        bytes calldata proof
    ) external view returns (bool) {
        return MerkleProof.verify(headers[slot].receiptsRoot, leaf, proof);
    }
}

L2 Bridge contract(Solidity):

contract TempoBridge {
    EthereumLightClient public lightClient;

    function claim(
        uint64 slot,
        bytes32 eventHash,
        bytes calldata merkleProof,
        address user,
        address token,
        uint256 amount
    ) external {
        // イベントが light client 経由で Ethereum の block N にあったかを検証
        require(
            lightClient.verifyInclusion(slot, eventHash, merkleProof),
            "invalid proof"
        );
        // Tempo 上で wrapped USDC を mint
        IERC20(wrappedToken[token]).mint(user, amount);
    }
}

コスト内訳:

操作Chain 上のコストいつ発生
L1 lock80k gas ($2)User tx ごと
Light client updateSyncCommittee50k gas ($1.50)27 時間ごと(sync committee 期間)
Light client addHeader20k gas ($0.60)Ethereum block ごと(~12s)
L2 claim150k gas ($4.50)User tx ごと

ユーザコスト: bridge tx あたり ~$6-7(ガス価格依存)。

ZK light client 版なら addHeader を epoch あたり 1 件の定数コスト proof 検証に置換 → 総コスト ~10 分の 1

Replay 防止:

mapping(bytes32 => bool) public claimed;
require(!claimed[eventHash], "already claimed");
claimed[eventHash] = true;

Withdrawal 方向(Tempo → Ethereum)の追加要件:

  • Tempo bridge が Withdrawn イベント発火
  • Ethereum 上で Tempo の light client(こちらが難しい方)
  • L1 bridge が Tempo light client に対する proof 受け入れ

Tempo(Reth ベース BFT)の場合、Ethereum 上の Tempo light client は Ethereum のそれより単純(validator セット有界 + BFT 署名で済む、~30 validator なら header ~5k gas)。

失敗例(誤解)

「Relayer を信頼する必要がある」— 間違い。Relayer は stateless 中継、誰でも動かせる。proof が valid なら採用、invalid なら reject。落ちたら別の誰かが引き継ぐ。信頼すべきは Ethereum のコンセンサスのみ

「Light client trusted setup = 永続的信頼」— 半分間違い。trusted setup は setup 時のみ(初期 checkpoint = Tempo チーム / DAO 信頼)。継続的信頼ではない、IBC も新 client で DAO ガバナンス採用。

「Withdrawal も同じく簡単」— 間違い。Ethereum で Tempo の light client が必要 = こちらが難しい方。ただし Tempo BFT は有界 validator + BFT 署名で Ethereum PoS(512 sync committee)より単純 = ~30 validator で header ~5k gas。

🛑 予測。 Bridge は 3 コンポーネント = L1 contract + Relayer + L2 contract + Light client(L2 内)。それぞれが何をして何を信頼するか?(答え: ① L1 Bridge Contract = lock + Locked イベント発火、自身が source of truth、何も信頼しない。② Relayer = Ethereum 観測 → inclusion proof 生成 → L2 提出、permissionless、信頼不要(proof が valid なら誰でも採用される)。③ L2 Bridge Contract = light client.verifyInclusion → wrapped token mint、light client 経由で Ethereum コンセンサスのみ信頼。④ L2 Light client = updateSyncCommittee + addHeader + verifyInclusion、Ethereum sync committee 署名のみ信頼システム全体の trust 仮定 = Ethereum の PoS が正しく動くこと、それ以外何もない。)

ステップで組み立てる

Step 1: 4 コンポーネントの責務

L1 Bridge / Relayer / L2 Bridge / L2 Light client。各々の trust モデル即答。

Step 2: L1 Contract = lock + emit

最も単純、relayer 依存なし、誰でも観測可能。

Step 3: Relayer = stateless 中継

Locked 観測 → inclusion proof → L2 提出。誰でも動かせる + 落ちたら引き継ぎ可能。

Step 4: L2 Light client = 3 メソッド

updateSyncCommittee(期間ごと)+ addHeader(block ごと)+ verifyInclusion(クエリ)。

Step 5: L2 Bridge = verify → mint

Light client.verifyInclusion 結果のみで mint 判断。他の trust 仮定なし

Step 6: 3 落とし穴を意識

Trusted setup(初期 checkpoint のみ)+ Replay 防止(claimed mapping)+ Withdrawal 方向(Ethereum で Tempo light client、有界 BFT で安価)。

Step 7: コスト計算

~$6-7 / bridge tx、ZK light client で ~10 分の 1。

答え合わせ

  • 全 trust 仮定の正体: Ethereum の PoS が正しく動くこと。それ以外なし。L1 contract = source of truth(自身)、Relayer = permissionless(誰でも、proof valid なら)、L2 Bridge = light client 経由で Ethereum コンセンサス、Light client = sync committee 署名。4 段階チェーンで全部が Ethereum コンセンサスに帰着
  • 3 ユースケースの相当物: ① OP Standard Bridge = optimistic 7 日遅延なしで実現する版(OP は fraud proof window 必要、これは light client で即時 finality)、② ZK rollup が本番化したときの目標(light client が ZK proof で置換)、③ Espresso 共有 sequencer が今日やっていること(共有 light client が複数 rollup の bridge)。「trustless bridge」の標準解
  • Withdrawal 方向の難しさと解: Ethereum で Tempo light client = Tempo BFT 検証を Solidity で実装、ただし 有界 validator + BFT 署名で Ethereum PoS(512 sync committee)より単純 = ~30 validator なら header ~5k gas(Ethereum sync committee の 1/10)。逆向きは構造的に難しいが Tempo BFT の単純さが救う

合格基準

  • 4 コンポーネント(L1 / Relayer / L2 / Light client)を即答できる。
  • Trust 仮定 1 つ(Ethereum PoS)を即答できる。
  • L1 lock + L2 Light client 3 メソッドを Solidity で書ける。
  • Relayer の Rust ループを書ける。
  • 3 落とし穴(trusted setup / replay / withdrawal)を即答できる。

まとめ(3行)

  • 最小 bridge = 3 コンポーネント(L1 lock + Relayer + L2 claim + Light client)、trust 仮定 1 つ(Ethereum PoS)、コスト ~$6-7 / tx。
  • Solidity 4 contract(EthereumBridge + EthereumLightClient + TempoBridge + Replay mapping)+ Rust Relayer = trustless bridge の標準解、OP Standard / ZK rollup / 共有 sequencer の参照実装。
  • Withdrawal 方向は Ethereum 上で Tempo light client 必要だが、有界 BFT で Ethereum PoS より単純(~30 validator で header ~5k gas)= 構造的に難しいが Tempo の単純さが救う。