レッスン8 — 実 Signer トレイト + PrivateKeySigner / AwsSigner / WalletFiller を読む
問い
3 トレイト分割 + WalletFiller 橋渡しを組み立てた。実ソースで全境界 + PrivateKeySigner のキャッシュ戦略 + AwsSigner の recovery-id 復元 + SignableTransaction 接着剤 + WalletFiller の FillProvider 組み込み — どこにあるか?
原理(最小モデル)
Sig = Signatureデフォルト付きジェネリックパラメータ. ECDSA secp256k1 がデフォルト、BLS / ed25519 / 耐量子で別実装可能。Sigが関連型ではなくジェネリックパラメータの理由. 同じ署名者が複数SigでSigner実装可能(Signer<Signature>+Signer<RawBytes>両方)。auto_impl(&mut, Box, Arc)の 3 種.Providerの 5 種より狭い。&なし =set_chain_id(&mut self)変更メソッド +Rcなし =Signer: Send + SyncでRc<T>は!Send + !Sync。PrivateKeySignerの構造.SigningKey+ キャッシュ済みaddress+ per-signerchain_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 注目点:
- AWS は DER エンコード署名を返す → alloy の (r, s, v) タプルに変換
- Recovery ID は AWS が返さない → v=0 と v=1 を試してアドレスマッチで復元
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 は 両可能性を試して復元 必要。PrivateKeySigner は k256 の 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で複数回トレイト実装可能 →PrivateKeySignerがSigner<Signature>(標準 ECDSA)とSigner<RawBytes>(生バイト出力)両方を実装可能。関連型なら実装ごとにSigひとつ確定 = 「複数 Sig で同じ署名者」が表現不可能。AwsSignerで recovery-id を自前復元する理由: AWS KMS は(r, s)のみ返す(DER エンコード)→v(リカバリバイト)は API 仕様外。両可能性(v=0、v=1)を試してアドレスを再現するほう を選ぶ → AWS 呼び出し 1 回 + アドレス導出 2 通り。PrivateKeySignerはk256::sign_recoverableで v が署名の一部として返る → 追加コストなし。address()キャッシュが必要な理由: すべての tx で複数回呼ばれる(追跡 / ロギング / 署名適格性 / コールフレーム構築)→ ネットワーク呼び出しだったら tx ごとに AWS 往復レイテンシ(数百 ms)→ 全 tx が誰の鍵で署名したか知るためだけに遅延。構築時キャッシュ = 一度きりセットアップ + ほぼゼロ per-call。
合格基準
Sigがジェネリックパラメータである理由を即答できる。auto_impl(&mut, Box, Arc)の 3 種と省略 2 種の理由を言える。PrivateKeySignerのaddressキャッシュ理由を 1 文で説明できる。AwsSignerの recovery-id 復元手順を言える。SignableTransaction接着剤の役割を 1 文で説明できる。WalletFillerがTxFiller<N>で nonce filler と同機構と理解している。
まとめ(3行)
Signer<Sig = Signature>ジェネリック +auto_impl(&mut, Box, Arc)(&なし = 変更メソッド、Rcなし = Send/Sync)+Send + SyncでArc<S>パターン成立。PrivateKeySignerはaddress構築時キャッシュ + Signer/SignerSync 両方実装、AwsSignerは recovery-id を v=0/v=1 試行で復元 + async のみ実装。SignableTransaction接着剤でTxSignerがチェーン非依存、WalletFillerが nonce / gas / chain-id と同じTxFiller<N>で FillProvider に積層、.wallet(signer)ユーザー API はその糖衣。