レッスン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 失敗:
- AWS KMS — 秘密鍵が AWS の外に出ない、署名は async ネットワーク
- Ledger — 鍵はデバイス、async USB + 人間押下
- マルチチェーン — OP の
OpTypedTransactionは EthereumTypedTransactionではない - 多種署名 — 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 つ:
- 署名操作の大部分は tx 署名ではない(EIP-191 + EIP-712 のほうが dapp で一般的)
- Signer 実装はチェーン横断で再利用可能(LocalSigner は Ethereum か Optimism か気にしない)
- 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 でチェーン認識必要 → Signer を N: 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 トレイト(
Signerchain-agnostic async /TxSigner<Sig>chain-aware tx 署名 /SignerSyncsync 並列ミラー)= 素朴 sign の 4 失敗(AWS / Ledger / マルチチェーン / 多種署名)を解決する 3 軸抽象化。 Signerのsign_messageデフォルト実装で EIP-191 を共通化 + オーバーライド可能、TxSigner別トレイトで chain-agnostic 再利用 + 多態性。ProviderBuilder.wallet(signer)がWalletFiller(signer)を FillProvider に積む糖衣、署名層が nonce / gas / chain-id filler と同じ機構で動く。