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

レッスン9 — ドリル: FillProvider チェーン経由でエンドツーエンドの署名済 tx

問い

読むだけではリハーサル、手を動かすことで記憶。実際の署名者を ProviderBuilder に配線、Anvil に対して署名済み tx を送信、FillProvider チェーンが nonce / gas / chain-id / 署名をスタック順に処理するのを観察。Provider + Network + Signer 3 チェーンの総決算 — どう組み合わさるか?

原理(最小モデル)

  • PrivateKeySignersign_hash 直接. k256::sign_recoverable(r, s, v) 全部得られる、追加 recovery 不要。
  • 4 Filler のスタック順序. NonceFiller → GasFiller → ChainIdFiller → WalletFiller。「fill → sign」順序は譲れない(署名ハッシュ計算前に nonce/gas/chain_id 揃う必要)。
  • with_recommended_fillers() + .wallet(signer). nonce + gas + chain-id 自動 + 署名層追加。
  • 3 チェーン総合. Provider チェーン(ProviderBuilder)+ Network チェーン(TransactionBuilder.with_to / .with_value)+ Signer チェーン(.wallet())が 1 実行可能プログラム。
  • Filler 外すと失敗. NonceFiller なしだと "missing nonce"、GasFiller なしだと "missing gas" 等。各 filler がギャップを 1 つずつ埋める証拠。
  • タワー型の型レベル具現化. FillProvider<JoinFill<JoinFill<JoinFill<JoinFill<Identity, GasFiller>, NonceFiller>, ChainIdFiller>, WalletFiller<EthereumWallet>>, RootProvider, Ethereum> がスタック順序を型でエンコード。

具体例

セットアップ:

# ターミナル 1: Anvil(10 アカウントを prefund)
anvil

# ターミナル 2: プロジェクト
cargo new alloy-signer-drill --bin
cd alloy-signer-drill

Cargo.toml:

[dependencies]
alloy = { version = "0.x", features = ["full", "provider-http", "signer-local"] }
tokio = { version = "1", features = ["full"] }
eyre = "0.6"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

ドリル 1(ハッシュ直接署名):

use alloy::primitives::{B256, keccak256};
use alloy::signers::{Signer, local::PrivateKeySigner};

#[tokio::main]
async fn main() -> eyre::Result<()> {
    tracing_subscriber::fmt().with_env_filter("info").init();

    let signer = PrivateKeySigner::random();
    let signer_addr = signer.address();
    println!("signer address: {signer_addr}");

    let message = b"hello, alloy";
    let hash = keccak256(message);
    let sig = signer.sign_hash(&hash).await?;

    let recovered = sig.recover_address_from_prehash(&hash)?;
    assert_eq!(recovered, signer_addr);
    println!("recovered: {recovered}  (matches: {})", recovered == signer_addr);

    Ok(())
}

ドリル 2(FillProvider + 実 tx 送信):

use alloy::providers::{Provider, ProviderBuilder};
use alloy::primitives::{Address, U256, address};

// ドリル 2: FillProvider 経由で実際に署名済みの tx を送る
let funded_pk = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
let funded_signer: PrivateKeySigner = funded_pk.parse()?;
let funded_addr = funded_signer.address();
println!("funded sender: {funded_addr}");

let provider = ProviderBuilder::new()
    .with_recommended_fillers()  // nonce + gas + chain_id
    .wallet(funded_signer)        // 署名
    .on_http("http://localhost:8545".parse()?);

// 新規アドレスに 1 ETH 送信
let recipient: Address = address!("000000000000000000000000000000000000beef");
let value = U256::from(1_000_000_000_000_000_000u128);  // 1 ETH

let pending = provider
    .send_transaction(
        alloy::rpc::types::TransactionRequest::default()
            .with_to(recipient)
            .with_value(value)
    )
    .await?;
let receipt = pending.get_receipt().await?;
println!("tx hash: {:?}", receipt.transaction_hash);
println!("status: {:?}", receipt.status());

let recipient_balance = provider.get_balance(recipient).await?;
println!("recipient balance: {recipient_balance}");

期待出力:

signer address: 0x... (ランダム)
recovered: 0x... (matches: true)
funded sender: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
tx hash: 0x...
status: true
recipient balance: 1000000000000000000

4 Filler の処理表:

Filler埋めたフィールド処理
NonceFillernonceeth_getTransactionCount(from, "pending") 呼ぶ
GasFillergas / gasPrice / maxFeePerGas + maxPriorityFeePerGaseth_estimateGas + eth_gasPrice(or eth_feeHistory)呼ぶ
ChainIdFillerchainIdeth_chainId を 1 度呼んでキャッシュ
WalletFillersignatureTxEnvelope 化)SignableTransaction 構築 + signer.sign_transaction() + 署名取り付け

順序が重要: WalletFiller は他の filler の に走る(署名ハッシュ計算前に nonce/gas/chain_id 揃う必要)。

Filler 外して失敗観察:

let provider_no_fillers = ProviderBuilder::new()
    .wallet(funded_signer.clone())  // wallet のみ
    .on_http("http://localhost:8545".parse()?);

let result = provider_no_fillers
    .send_transaction(
        TransactionRequest::default()
            .with_to(recipient)
            .with_value(U256::from(1_000_000_000_000_000_000u128))
    )
    .await;
println!("no-filler result: {:?}", result);

エラー: "missing nonce" or "missing gas" or "missing maxFeePerGas"。filler が実際に作業している証拠

最終 Provider 型(任意観察):

FillProvider<JoinFill<JoinFill<JoinFill<JoinFill<Identity, GasFiller>, NonceFiller>, ChainIdFiller>, WalletFiller<EthereumWallet>>, RootProvider, Ethereum>

左から右に読む: FillProvider が内側プロバイダラップ、JoinFill<...> チェーンが 4 filler スタック、Wallet 最外で送信時最後適用。型レベルでスタック順エンコード

失敗例(誤解)

「WalletFiller を NonceFiller 前に置いてもよい」— 致命的。署名ハッシュは nonce / gas / chain_id を含む → これらが埋まる前に署名すると無効署名。fill → sign 順序譲れない

with_recommended_fillers() だけで wallet 不要」— 間違い。署名がない → TxEnvelope 化できない → broadcast 不可能。両方必要。

「Anvil の prefund 秘密鍵は本番で使える」— 絶対 NG。Anvil の dev 鍵は 公開済み、本番で使うと即盗難。Anvil dev 鍵はテストのみ

ステップで組み立てる

Step 1: PrivateKeySigner::sign_hash を直接

k256::sign_recoverable(r, s, v) 取得、recover_address_from_prehash で検証。

Step 2: ProviderBuilder.with_recommended_fillers().wallet(signer)

4 filler スタック + 署名層追加。

Step 3: 実 tx 送信

TransactionRequest::default().with_to(recipient).with_value(value) + send_transaction().get_receipt()

Step 4: 4 Filler 処理表

NonceFiller / GasFiller / ChainIdFiller / WalletFiller の RPC 呼び出しと埋めるフィールド。

Step 5: Filler 外して失敗観察

NonceFiller なしで "missing nonce"、各 filler のギャップ埋め確認。

Step 6: 最終 Provider 型のタワー観察

FillProvider<JoinFill<JoinFill<...>>> がスタック順を型でエンコード。

答え合わせ

  • PrivateKeySigner で recovery-id 不要、AwsSigner で必要な理由: PrivateKeySigner = k256::sign_recoverable(r, s, v) 全部返す → 追加処理なし。AwsSigner = AWS KMS が (r, s) のみ → v は v=0/v=1 試して キャッシュ済み address を再現するほう を選ぶ。クラウド署名のコスト。
  • 4 Filler の実行順と RPC 呼び出し: NonceFiller → eth_getTransactionCount、GasFiller → eth_estimateGas + eth_gasPrice(or eth_feeHistory)、ChainIdFiller → eth_chainId(キャッシュ)、WalletFiller → signer.sign_transactionfill → sign 順序が譲れない
  • WalletFiller が filler チェーン最後の理由: 署名ハッシュ計算は nonce / gas / chain_id を含む → これらが埋まる前に署名すると 無効署名。NonceFiller より前に走らせると署名 = 古い nonce → ネットワークが reject。fill → sign 順序、譲れない

合格基準

  • PrivateKeySigner.sign_hash(r, s, v) 取得を即答できる。
  • 4 Filler の処理表(埋めるフィールド + RPC 呼び出し)を書ける。
  • WalletFiller が最後である理由を 1 文で説明できる。
  • Filler 外したときの "missing nonce" / "missing gas" 失敗パターンを言える。
  • 最終 Provider 型のタワー(FillProvider<JoinFill<JoinFill<...>>>)が型レベルでスタック順エンコードと理解している。

まとめ(3行)

  • 3 チェーン総決算 = Provider(ProviderBuilder)+ Network(TransactionBuilder.with_to / .with_value)+ Signer(.wallet())が 1 実行可能プログラム。
  • 4 Filler スタック(NonceFiller → GasFiller → ChainIdFiller → WalletFiller)= fill → sign 順序譲れない、署名ハッシュ計算前に nonce/gas/chain_id 揃う必要。
  • 最終 Provider 型タワーが型レベルでスタック順エンコード、Filler 外すと "missing X" で fail = 各 filler のギャップ埋めが実証される、dapp / MEV ボット / インデクサが本番投入する標準パターン。