FABRKNT
Inside Alloy — Rust Ethereum ライブラリを読む
Alloyの内側
レッスン 10 / 15·CONTENT10 分25 XP
コース
Inside Alloy — Rust Ethereum ライブラリを読む
レッスンの役割
CONTENT
順序
10 / 15

レッスン7 — Signer トレイトをステップごとに組み立てる

問い

MEV サーチャーは AWS KMS で署名(秘密鍵が AWS の外に出ない)、トレジャリーは Ledger で署名(ハードウェアウォレット、毎回ボタン押下)、テストは生 secp256k1 バイトで署名。同じ alloy アプリケーションコードが 3 つすべてを駆動する — 何が可能にするか?

原理(最小モデル)

  • 素朴 sign 関数の 4 失敗. AWS KMS(async ネットワーク)+ Ledger(async USB + 人間押下)+ マルチチェーン(OP OpTypedTransaction ≠ Ethereum)+ 多種署名(EIP-191 / EIP-712 / 生ハッシュ)。
  • 3 軸抽象化. 署名者の所在(プロセス内 / クラウド / ハードウェア)+ なにに署名するか(ハッシュ / メッセージ / tx)+ async か sync か。
  • Signer = チェーン非依存のハッシュ/メッセージ署名. async、sign_hash のみ必須、sign_message は EIP-191 プレフィックス付きのデフォルト実装。
  • TxSigner<Sig> = 別トレイト. Tx 署名はチェーン認識(SignableTransaction 経由)、Signer は chain-agnostic に保つ。
  • SignerSync = sync 並列ミラー. プロセス内署名者は両方実装、ネットワーク束縛は async のみ。
  • WalletFiller で Provider に接続. ProviderBuilder.wallet(signer) の糖衣、FillProvider チェーンに署名層を積む。
  • デフォルト実装 + オーバーライド. sign_message のデフォルトは EIP-191、AWS KMS が独自プレフィクシングする場合は AwsSigner 側でオーバーライド。

具体例

最終形:

#[async_trait]
pub trait Signer<Sig = Signature> {
    async fn sign_hash(&self, hash: &B256) -> Result<Sig>;
    async fn sign_message(&self, message: &[u8]) -> Result<Sig> { /* デフォルト: ハッシュ化して sign_hash */ }
    fn address(&self) -> Address;
    fn chain_id(&self) -> Option<ChainId>;
    fn set_chain_id(&mut self, chain_id: Option<ChainId>);
}

#[async_trait]
pub trait TxSigner<Sig> {
    fn address(&self) -> Address;
    async fn sign_transaction(&self, tx: &mut dyn SignableTransaction<Sig>) -> Result<Sig>;
}

pub trait SignerSync<Sig = Signature> { /* Signer の sync ミラー */ }

素朴 sign 関数:

fn sign_tx(privkey: B256, mut tx: TypedTransaction) -> Result<TxEnvelope> {
    let hash = tx.signature_hash();
    let sig = secp256k1_sign(privkey, hash)?;
    Ok(tx.with_signature(sig))
}

4 失敗:

  1. AWS KMS — 秘密鍵が AWS の外に出ない、署名は async ネットワーク
  2. Ledger — 鍵はデバイス、async USB + 人間押下
  3. マルチチェーン — OP の OpTypedTransaction は Ethereum TypedTransaction ではない
  4. 多種署名 — EIP-191(personal_sign)、EIP-712(typed data)、生ハッシュ

Step 1(async + 1 メソッド):

#[async_trait]
trait Signer {
    async fn sign_hash(&self, hash: B256) -> Result<Signature>;
}

struct LocalSigner { privkey: B256 }
impl Signer for LocalSigner { /* プロセス内 */ }

struct AwsSigner { client: AwsKmsClient, key_id: String }
impl Signer for AwsSigner { /* ネットワーク呼び出し */ }

struct LedgerSigner { dev: LedgerDevice }
impl Signer for LedgerSigner { /* USB 呼び出し */ }

Step 2(sign_message デフォルト実装):

#[async_trait]
trait Signer {
    async fn sign_hash(&self, hash: &B256) -> Result<Signature>;

    async fn sign_message(&self, message: &[u8]) -> Result<Signature> {
        let prefixed = eip191_hash(message);  // "\x19Ethereum Signed Message:\n" + len + msg
        self.sign_hash(&prefixed).await
    }
}

すべての Signer が sign_message をコストなしで得る + AWS KMS のような独自プレフィクシング実装はオーバーライド可能。

Step 3(TxSigner を別トレイトに):

選択肢 A: Signer をネットワーク上ジェネリック化 Signer<N: Network> 選択肢 B: Tx 署名を別トレイトに、Signer は chain-agnostic

alloy は 選択肢 B:

#[async_trait]
pub trait TxSigner<Sig> {
    fn address(&self) -> Address;
    async fn sign_transaction(&self, tx: &mut dyn SignableTransaction<Sig>) -> Result<Sig>;
}

理由 3 つ:

  1. 署名操作の大部分は tx 署名ではない(EIP-191 + EIP-712 のほうが dapp で一般的)
  2. Signer 実装はチェーン横断で再利用可能(LocalSigner は Ethereum か Optimism か気にしない)
  3. Tx 署名は自然に多態的(1 Signer が TxSigner<Ethereum>TxSigner<Optimism> を別実装)

Step 4(SignerSync 分離):

pub trait SignerSync<Sig = Signature> {
    fn sign_hash_sync(&self, hash: &B256) -> Result<Sig>;
    fn sign_message_sync(&self, message: &[u8]) -> Result<Sig> { /* デフォルト */ }
    fn chain_id_sync(&self) -> Option<ChainId>;
}

LocalSigner は両方実装、AwsSigner / LedgerSigner は async のみ。

Step 5(WalletFiller で Provider 接続):

pub struct WalletFiller<W> {
    wallet: W,
}

impl<W: TxSigner<...>, N: Network> Filler<N> for WalletFiller<W> {
    async fn fill(&self, tx: &mut N::TransactionRequest) -> Result<()> {
        // 1. リクエストから unsigned tx を解決
        // 2. self.wallet.sign_transaction(...)
        // 3. 署名を取り付け
    }
}

ユーザーコード:

let signer = PrivateKeySigner::random();
let provider = ProviderBuilder::new()
    .wallet(signer)               // WalletFiller(signer) をインストール
    .with_recommended_fillers()
    .on_http(url);

provider.send_transaction(tx).await?;  // tx は WalletFiller により署名される

失敗例(誤解)

Signer だけで tx 署名も賄える」— 間違い。Tx は SignableTransaction でチェーン認識必要 → SignerN: Network でジェネリック化すると全 signer signature がふくらむ + チェーン横断再利用不可。TxSigner を別トレイトに

「sync 版を作る必要なし、async で統一」— 間違い。プロセス内署名者で async = future オーバーヘッド + ホットループで損。SignerSync でプロセス内が両方実装、ネットワーク束縛は async のみ。

sign_message を関数(トレイトメソッドではない)にする」— 間違い。AWS KMS が独自プレフィクシングを実装したい → デフォルト実装ならオーバーライド可能、トレイト外関数では不可能。「共通挙動 + 実装単位カスタマイズ」がデフォルト実装の正しい用途

ステップで組み立てる

Step 1: 素朴 sign 関数の 4 失敗

AWS KMS + Ledger + マルチチェーン + 多種署名。

Step 2: 3 軸抽象化

所在 + 何に署名 + async/sync。

Step 3: Signer を async + chain-agnostic に

sign_hash のみ必須、sign_message デフォルト実装。

Step 4: TxSigner 別トレイトの 3 理由

dapp 利用パターン + chain-agnostic 再利用 + 多態性。

Step 5: SignerSync で sync 並列

LocalSigner 両方実装、AWS / Ledger は async のみ。

Step 6: WalletFiller で Provider 統合

ProviderBuilder.wallet(signer) が糖衣、FillProvider チェーンに署名層。

答え合わせ

  • Signer が async である理由: AWS KMS(ネットワーク呼び出し)+ Ledger(USB + 人間押下)の 2 つが async 必須。プロセス内でも async 統一で 1 トレイトに集約、sync 専用は SignerSync で並列に。
  • TxSigner が別トレイトである 3 理由: ① 署名操作の大部分は tx 署名ではない(EIP-191 + EIP-712 が dapp で一般的)→ Signer signature ふくらませない、② Signer 実装はチェーン横断再利用可能(LocalSigner は Ethereum / Optimism 両対応)、③ Tx 署名は自然に多態的(1 Signer が TxSigner<Ethereum> + TxSigner<Optimism> を別コードパスで実装可能)。
  • WalletFiller の役割: Signer / TxSignerどう署名するか を抽象化、WalletFiller はリクエストフローの正しいタイミング(unsigned tx 構築後)で署名を呼び出す Filler 機構。ProviderBuilder.wallet(signer) ユーザー API は内部で WalletFiller(signer) を FillProvider に積む糖衣。

合格基準

  • 素朴 sign 関数の 4 失敗を即答できる。
  • 3 軸抽象化(所在 + 対象 + async/sync)を言える。
  • Signer / TxSigner / SignerSync の 3 トレイトを役割で言える。
  • TxSigner 別トレイトの 3 理由を言える。
  • WalletFiller の役割(Filler 機構経由で Provider 統合)を 1 文で説明できる。

まとめ(3行)

  • 3 トレイト(Signer chain-agnostic async / TxSigner<Sig> chain-aware tx 署名 / SignerSync sync 並列ミラー)= 素朴 sign の 4 失敗(AWS / Ledger / マルチチェーン / 多種署名)を解決する 3 軸抽象化。
  • Signersign_message デフォルト実装で EIP-191 を共通化 + オーバーライド可能、TxSigner 別トレイトで chain-agnostic 再利用 + 多態性。
  • ProviderBuilder.wallet(signer)WalletFiller(signer) を FillProvider に積む糖衣、署名層が nonce / gas / chain-id filler と同じ機構で動く。