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

レッスン8 — 実 Signer トレイト + PrivateKeySigner / AwsSigner / WalletFiller を読む

問い

3 トレイト分割 + WalletFiller 橋渡しを組み立てた。実ソースで全境界 + PrivateKeySigner のキャッシュ戦略 + AwsSigner の recovery-id 復元 + SignableTransaction 接着剤 + WalletFiller の FillProvider 組み込み — どこにあるか?

原理(最小モデル)

  • Sig = Signature デフォルト付きジェネリックパラメータ. ECDSA secp256k1 がデフォルト、BLS / ed25519 / 耐量子で別実装可能。
  • Sig が関連型ではなくジェネリックパラメータの理由. 同じ署名者が複数 SigSigner 実装可能(Signer<Signature> + Signer<RawBytes> 両方)。
  • auto_impl(&mut, Box, Arc) の 3 種. Provider の 5 種より狭い。& なし = set_chain_id(&mut self) 変更メソッド + Rc なし = Signer: Send + SyncRc<T>!Send + !Sync
  • PrivateKeySigner の構造. SigningKey + キャッシュ済み address + per-signer chain_id。5 コンストラクタ(random / from_bytes / from_str / from_signing_key / random_with)。
  • AwsSigner の recovery-id 復元. AWS KMS は (r, s) のみ返す → v は v=0 と v=1 を試してキャッシュ済み address を再現する方を選ぶ。
  • address() キャッシュが必要な理由. すべての tx で複数回呼ばれる、ネットワーク呼び出しだったら tx ごとに往復レイテンシ。
  • SignableTransaction 接着剤. 各チェーン UnsignedTx が実装、TxSigner はチェーン非依存で動く(signature_hash() 呼ぶだけ)。
  • WalletFiller = TxFiller<N>. nonce / gas / chain-id filler と同じ TxFiller<N> トレイト、合成可能。

具体例

トレイトヘッダ:

#[async_trait]
#[auto_impl(&mut, Box, Arc)]
pub trait Signer<Sig = Signature>: Send + Sync {
    async fn sign_hash(&self, hash: &B256) -> Result<Sig>;

    async fn sign_message(&self, message: &[u8]) -> Result<Sig> {
        self.sign_hash(&eip191_hash_message(message)).await
    }

    fn address(&self) -> Address;
    fn chain_id(&self) -> Option<ChainId>;
    fn set_chain_id(&mut self, chain_id: Option<ChainId>);
}

3 注目点:

  • Sig = Signature デフォルト: ECDSA secp256k1 デフォルト、BLS / ed25519 / 耐量子で別実装可能
  • auto_impl(&mut, Box, Arc) の 3 種: & なし(set_chain_id が変更メソッド)+ Rc なし(Signer: Send + Sync だが Rc は !Send + !Sync
  • Send + Sync スーパートレイト: Arc<S> でタスク間共有

PrivateKeySigner:

pub struct PrivateKeySigner {
    signer: SigningKey,
    address: Address,        // 構築時にキャッシュ
    chain_id: Option<ChainId>,
}

impl PrivateKeySigner {
    pub fn random() -> Self { /* OsRng → SigningKey */ }
    pub fn random_with(rng: &mut impl CryptoRng) -> Self { /* tests */ }
    pub fn from_bytes(bytes: &B256) -> Result<Self> { /* k256 鍵をパース */ }
    pub fn from_str(s: &str) -> Result<Self> { /* hex → from_bytes */ }
    pub fn from_signing_key(signer: SigningKey) -> Self { /* 直接 */ }
}

address を構築時キャッシュ理由: 公開鍵からの導出は非自明(非圧縮 pubkey を keccak → 最後 20 バイト)→ 1 度だけ計算。

Signer + SignerSync の両方実装:

fn high_throughput_path<S: SignerSync>(signer: &S) { /* sync、future オーバーヘッドなし */ }
fn cloud_compatible_path<S: Signer>(signer: &S) { /* async、AWS でも動く */ }

sync パスが実作業、async は async ブロックで sync 呼び出すだけ。async は sync の上に安価に合成可能、逆は不可

AwsSigner:

pub struct AwsSigner {
    client: Client,
    key_id: String,
    address: Address,       // キャッシュ済
    chain_id: Option<ChainId>,
}

#[async_trait]
impl Signer for AwsSigner {
    async fn sign_hash(&self, hash: &B256) -> Result<Signature> {
        let resp = self.client
            .sign()
            .key_id(&self.key_id)
            .message(Blob::new(hash.to_vec()))
            .message_type(MessageType::Digest)
            .send()
            .await?;
        let sig = der_to_alloy(&resp.signature.as_ref())?;
        let recid = recover_recid(hash, &sig, &self.address)?;
        Ok(Signature { /* ... */ })
    }
}

// AwsSigner は SignerSync を impl しない — AWS KMS 経由には sync パスがない

3 注目点:

  1. AWS は DER エンコード署名を返す → alloy の (r, s, v) タプルに変換
  2. Recovery ID は AWS が返さない → v=0 と v=1 を試してアドレスマッチで復元
  3. SignerSync 実装なし → ネットワーク越しなので sync 不可能

SignableTransaction 接着剤:

pub trait SignableTransaction<Sig> {
    fn set_chain_id(&mut self, chain_id: ChainId);
    fn set_chain_id_checked(&mut self, chain_id: ChainId) -> bool;
    fn encode_for_signing(&self, out: &mut dyn BufMut);
    fn signature_hash(&self) -> B256;
    fn into_signed(self, signature: Sig) -> Signed<Self, Sig>
    where
        Self: Sized;
}

各チェーン UnsignedTx(Ethereum TypedTransaction / Optimism OpTypedTransaction)が実装。TxSigner はチェーン非依存で動く:

async fn sign_transaction(&self, tx: &mut dyn SignableTransaction<Signature>) -> Result<Signature> {
    let hash = tx.signature_hash();
    self.sign_hash(&hash).await
}

WalletFiller:

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

impl<W, N: Network> TxFiller<N> for WalletFiller<W>
where
    W: NetworkWallet<N>,
{
    type Fillable = Sendable<N::TxEnvelope>;

    async fn fill(&self, fillable: Self::Fillable, tx: &mut SendableTx<N>) -> TransportResult<...> {
        let envelope = self.wallet.sign_request(/* fillable からの unsigned tx */).await?;
        tx.envelope = Some(envelope);
        Ok(...)
    }
}

.wallet(signer) 糖衣:

impl<P, N> ProviderBuilder<P, N>
where
    P: ProviderLayer<...>,
    N: Network,
{
    pub fn wallet<W: NetworkWallet<N>>(self, wallet: W) -> ProviderBuilder<...> {
        self.layer(WalletFiller::new(wallet))
    }
}

.wallet(signer) = .layer(WalletFiller::new(signer)) 糖衣、nonce / gas / chain-id filler と同じ合成機構共有

失敗例(誤解)

address() を毎回ネットワーク呼び出し」— 致命的。すべての tx で複数回呼ばれる(追跡 + ロギング + 適格性チェック + コールフレーム構築)→ tx ごとに AWS 往復レイテンシ。構築時キャッシュで一度きりセットアップ + ほぼゼロの per-call

Rc<Signer> で thread-local 共有」— 間違いSigner: Send + Sync 要求 → Rc<T>!Send + !Sync(参照カウントが atomic でない)→ コンパイル拒否。Arc<Signer> のみ

AwsSigner で recovery-id 不要」— 間違い。AWS は (r, s) のみ返す、v両可能性を試して復元 必要。PrivateKeySignerk256 の sign-recoverable で直接得られる、追加コストなし。

ステップで組み立てる

Step 1: Sig = Signature デフォルト + ジェネリック

ECDSA デフォルト、BLS / ed25519 で別実装可能、ジェネリックパラメータで複数 Sig 実装可能。

Step 2: auto_impl(&mut, Box, Arc) の 3 種理由

& なし = 変更メソッド、Rc なし = Send/Sync 制約。

Step 3: PrivateKeySigner キャッシュ戦略

構築時 address キャッシュ、5 コンストラクタ。

Step 4: AwsSigner の recovery-id 復元

AWS (r, s) のみ → v=0/v=1 試してアドレスマッチで復元、SignerSync 実装なし。

Step 5: SignableTransaction 接着剤

各 UnsignedTx 実装、TxSigner はチェーン非依存。

Step 6: WalletFiller = TxFiller<N>

nonce / gas / chain-id と同機構、.wallet(signer) 糖衣。

答え合わせ

  • Sig が関連型ではなくジェネリックパラメータの理由: 同じ署名者が 異なる Sig で複数回トレイト実装可能PrivateKeySignerSigner<Signature>(標準 ECDSA)と Signer<RawBytes>(生バイト出力)両方を実装可能。関連型なら実装ごとに Sig ひとつ確定 = 「複数 Sig で同じ署名者」が表現不可能。
  • AwsSigner で recovery-id を自前復元する理由: AWS KMS は (r, s) のみ返す(DER エンコード)→ v(リカバリバイト)は API 仕様外。両可能性(v=0、v=1)を試してアドレスを再現するほう を選ぶ → AWS 呼び出し 1 回 + アドレス導出 2 通り。PrivateKeySignerk256::sign_recoverable で v が署名の一部として返る → 追加コストなし。
  • address() キャッシュが必要な理由: すべての tx で複数回呼ばれる(追跡 / ロギング / 署名適格性 / コールフレーム構築)→ ネットワーク呼び出しだったら tx ごとに AWS 往復レイテンシ(数百 ms)→ 全 tx が誰の鍵で署名したか知るためだけに遅延。構築時キャッシュ = 一度きりセットアップ + ほぼゼロ per-call

合格基準

  • Sig がジェネリックパラメータである理由を即答できる。
  • auto_impl(&mut, Box, Arc) の 3 種と省略 2 種の理由を言える。
  • PrivateKeySigneraddress キャッシュ理由を 1 文で説明できる。
  • AwsSigner の recovery-id 復元手順を言える。
  • SignableTransaction 接着剤の役割を 1 文で説明できる。
  • WalletFillerTxFiller<N> で nonce filler と同機構と理解している。

まとめ(3行)

  • Signer<Sig = Signature> ジェネリック + auto_impl(&mut, Box, Arc)& なし = 変更メソッド、Rc なし = Send/Sync)+ Send + SyncArc<S> パターン成立。
  • PrivateKeySigneraddress 構築時キャッシュ + Signer/SignerSync 両方実装、AwsSigner は recovery-id を v=0/v=1 試行で復元 + async のみ実装。
  • SignableTransaction 接着剤で TxSigner がチェーン非依存、WalletFiller が nonce / gas / chain-id と同じ TxFiller<N> で FillProvider に積層、.wallet(signer) ユーザー API はその糖衣。