レッスン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期間ごと +addHeaderblock ごと +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 lock | User tx ごと | |
Light client updateSyncCommittee | 27 時間ごと(sync committee 期間) | |
Light client addHeader | Ethereum block ごと(~12s) | |
L2 claim | 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 + L2claim+ 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 の単純さが救う。