-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat(cast): validate-auth #12634
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat(cast): validate-auth #12634
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,4 +25,5 @@ pub mod run; | |
| pub mod send; | ||
| pub mod storage; | ||
| pub mod txpool; | ||
| pub mod validate_auth; | ||
| pub mod wallet; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| use std::collections::HashMap; | ||
|
|
||
| use alloy_consensus::{Transaction, TxEnvelope}; | ||
| use alloy_eips::BlockId; | ||
| use alloy_network::{AnyTxEnvelope, TransactionResponse}; | ||
| use alloy_primitives::{Address, B256}; | ||
| use alloy_provider::Provider; | ||
| use foundry_cli::{ | ||
| opts::RpcOpts, | ||
| utils::{self, LoadConfig}, | ||
| }; | ||
|
|
||
| #[derive(Debug, clap::Parser)] | ||
| pub struct ValidateAuthArgs { | ||
| /// Transaction hash. | ||
| tx_hash: B256, | ||
|
|
||
| #[command(flatten)] | ||
| rpc: RpcOpts, | ||
| } | ||
|
|
||
| impl ValidateAuthArgs { | ||
| pub async fn run(self) -> eyre::Result<()> { | ||
| let config = self.rpc.load_config()?; | ||
| let provider = utils::get_provider(&config)?; | ||
|
|
||
| let tx = provider | ||
| .get_transaction_by_hash(self.tx_hash) | ||
| .await? | ||
| .ok_or_else(|| eyre::eyre!("tx not found: {:?}", self.tx_hash))?; | ||
|
|
||
| // Get block info for nonce calculation | ||
| let block_number = | ||
| tx.block_number.ok_or_else(|| eyre::eyre!("transaction is not yet mined"))?; | ||
| let tx_index = | ||
| tx.transaction_index.ok_or_else(|| eyre::eyre!("transaction index not available"))?; | ||
|
|
||
| // Fetch the block to get all transactions up to this one | ||
| let block = provider | ||
| .get_block_by_number(block_number.into()) | ||
| .full() | ||
| .await? | ||
| .ok_or_else(|| eyre::eyre!("block not found: {}", block_number))?; | ||
|
|
||
| // Build a map of address -> running nonce from txs in this block up to and including | ||
| // our tx | ||
| let mut running_nonces: HashMap<Address, u64> = HashMap::new(); | ||
|
Comment on lines
+45
to
+47
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need this?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i assume this is because naively getting the nonce at the block before this assumes there is only 1 transaction from the sender account in the current block, so we need to account for the fact that the account might send 1 or more txs before their eip7702 authorization which would bump the nonce. |
||
| for block_tx in block.transactions.txns().take((tx_index + 1) as usize) { | ||
| let from = block_tx.from(); | ||
| let nonce = block_tx.nonce(); | ||
| // Track the next expected nonce (current nonce + 1) | ||
| running_nonces.insert(from, nonce + 1); | ||
| } | ||
|
|
||
| let chain_id = provider.get_chain_id().await?; | ||
|
|
||
| // Extract authorization list from EIP-7702 transaction | ||
| let auth_list = match &*tx.inner.inner { | ||
| AnyTxEnvelope::Ethereum(TxEnvelope::Eip7702(signed_tx)) => { | ||
| signed_tx.tx().authorization_list.clone() | ||
| } | ||
| _ => { | ||
| eyre::bail!("transaction is not an EIP-7702 transaction"); | ||
| } | ||
| }; | ||
|
Comment on lines
+57
to
+65
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe we can use the |
||
|
|
||
| sh_println!("Transaction: {}", self.tx_hash)?; | ||
| sh_println!("Block: {} (tx index: {})", block_number, tx_index)?; | ||
| sh_println!()?; | ||
|
|
||
| if auth_list.is_empty() { | ||
| sh_println!("Authorization list is empty")?; | ||
| } else { | ||
| for (i, auth) in auth_list.iter().enumerate() { | ||
| let valid_chain = auth.chain_id == chain_id || auth.chain_id == 0; | ||
| sh_println!("Authorization #{}", i)?; | ||
| sh_println!(" Decoded:")?; | ||
| sh_println!(" Chain ID: {}", auth.chain_id,)?; | ||
| sh_println!(" Address: {}", auth.address)?; | ||
| sh_println!(" Nonce: {}", auth.nonce)?; | ||
| sh_println!(" r: {}", auth.r())?; | ||
| sh_println!(" s: {}", auth.s())?; | ||
| sh_println!(" v: {}", auth.y_parity())?; | ||
|
|
||
| match auth.recover_authority() { | ||
| Ok(authority) => { | ||
| sh_println!(" Recovered Authority: {}", authority)?; | ||
|
|
||
| sh_println!(" Validation Status:")?; | ||
| sh_println!( | ||
| " Chain: {}", | ||
| if valid_chain { | ||
| "VALID".to_string() | ||
| } else { | ||
| format!("INVALID (expected: 0 or {chain_id})") | ||
| } | ||
| )?; | ||
|
|
||
| // Get the expected nonce at time of tx execution | ||
| let expected_nonce = if let Some(&nonce) = running_nonces.get(&authority) { | ||
| nonce | ||
| } else { | ||
| // Fetch nonce at block - 1 (state before this block) | ||
| let prev_block = BlockId::number(block_number - 1); | ||
| provider.get_transaction_count(authority).block_id(prev_block).await? | ||
| }; | ||
|
|
||
| let valid_nonce = auth.nonce == expected_nonce; | ||
| if valid_nonce { | ||
| sh_println!(" Nonce: VALID")?; | ||
| } else { | ||
| sh_println!( | ||
| " Nonce: INVALID (expected: {}, got: {})", | ||
| expected_nonce, | ||
| auth.nonce | ||
| )?; | ||
| } | ||
|
|
||
| // If authorization was valid, update running nonce for subsequent auths | ||
| if valid_chain && valid_nonce { | ||
| running_nonces.insert(authority, expected_nonce + 1); | ||
| } | ||
|
|
||
| // Check if the authority's code was set to the delegated address | ||
| let code = provider.get_code_at(authority).await?; | ||
| if code.is_empty() { | ||
| sh_println!(" Code Status: No delegation (account has no code)")?; | ||
| } else if code.len() == 23 && code[0..3] == [0xef, 0x01, 0x00] { | ||
| // EIP-7702 delegation designator: 0xef0100 followed by 20-byte | ||
| // address | ||
| let delegated_to = Address::from_slice(&code[3..23]); | ||
| if delegated_to == auth.address { | ||
| sh_println!( | ||
| " Code Status: ACTIVE (delegated to {})", | ||
| delegated_to | ||
| )?; | ||
| } else { | ||
| sh_println!( | ||
| " Code Status: SUPERSEDED (currently delegated to {})", | ||
| delegated_to | ||
| )?; | ||
| } | ||
| } else { | ||
| sh_println!( | ||
| " Code Status: Account has contract code (not a delegation)" | ||
| )?; | ||
| } | ||
| } | ||
| Err(e) => { | ||
| sh_println!(" Authority: UNKNOWN")?; | ||
| sh_println!(" Signature: INVALID ({})", e)?; | ||
| } | ||
| } | ||
| sh_println!()?; | ||
| } | ||
| } | ||
| Ok(()) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ use crate::cmd::{ | |
| creation_code::CreationCodeArgs, da_estimate::DAEstimateArgs, erc20::Erc20Subcommand, | ||
| estimate::EstimateArgs, find_block::FindBlockArgs, interface::InterfaceArgs, logs::LogsArgs, | ||
| mktx::MakeTxArgs, rpc::RpcArgs, run::RunArgs, send::SendTxArgs, storage::StorageArgs, | ||
| txpool::TxPoolSubcommands, wallet::WalletSubcommands, | ||
| txpool::TxPoolSubcommands, validate_auth::ValidateAuthArgs, wallet::WalletSubcommands, | ||
| }; | ||
| use alloy_ens::NameOrAddress; | ||
| use alloy_primitives::{Address, B256, Selector, U256}; | ||
|
|
@@ -1119,6 +1119,10 @@ pub enum CastSubcommand { | |
| #[command(visible_aliases = &["decode-auth"])] | ||
| RecoverAuthority { auth: String }, | ||
|
|
||
| /// Validate EIP-7702 authorizations in a transaction and print validity status. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is being validated here? this info is also already included in cast tx (only displays the recovered auth if valid)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think the I think it shows the decoded data and recovered authority from the signature, but it doesn't show the the validity. I had to refer to etherscan for instance when I couldn't figure out why my eoa was not getting delegated |
||
| #[command(name = "validate-auth", visible_aliases = &["va", "validate-auths"])] | ||
| ValidateAuth(ValidateAuthArgs), | ||
|
|
||
| /// Extracts function selectors and arguments from bytecode | ||
| #[command(visible_alias = "sel")] | ||
| Selectors { | ||
|
|
||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this needs some docs, what this actually does