arb_precompiles/
arbfilteredtxmanager.rs

1use alloy_evm::precompiles::{DynPrecompile, PrecompileInput};
2use alloy_primitives::{Address, B256, U256};
3use revm::precompile::{PrecompileError, PrecompileId, PrecompileOutput, PrecompileResult};
4
5use crate::storage_slot::{
6    derive_subspace_key, map_slot_b256, ARBOS_STATE_ADDRESS, FILTERED_TX_STATE_ADDRESS,
7    ROOT_STORAGE_KEY, TRANSACTION_FILTERER_SUBSPACE,
8};
9
10/// ArbFilteredTransactionsManager precompile address (0x74).
11pub const ARBFILTEREDTXMANAGER_ADDRESS: Address = Address::new([
12    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
13    0x00, 0x00, 0x00, 0x74,
14]);
15
16// Function selectors.
17const ADD_FILTERED_TX: [u8; 4] = [0xbf, 0xc1, 0xd5, 0x0e];
18const DELETE_FILTERED_TX: [u8; 4] = [0x0b, 0x23, 0x48, 0x5a];
19const IS_TX_FILTERED: [u8; 4] = [0x37, 0x94, 0x6f, 0x6a];
20
21const SLOAD_GAS: u64 = 800;
22const SSTORE_GAS: u64 = 20_000;
23const COPY_GAS: u64 = 3;
24
25/// Sentinel value stored for filtered tx hashes.
26const PRESENT_VALUE: U256 = U256::from_limbs([1, 0, 0, 0]);
27
28pub fn create_arbfilteredtxmanager_precompile() -> DynPrecompile {
29    DynPrecompile::new_stateful(PrecompileId::custom("arbfilteredtxmanager"), handler)
30}
31
32fn handler(mut input: PrecompileInput<'_>) -> PrecompileResult {
33    // ArbFilteredTransactionsManager requires ArbOS >= 60 (TransactionFiltering).
34    if let Some(result) = crate::check_precompile_version(
35        arb_chainspec::arbos_version::ARBOS_VERSION_TRANSACTION_FILTERING,
36    ) {
37        return result;
38    }
39
40    let data = input.data;
41    if data.len() < 4 {
42        return Err(PrecompileError::other("input too short"));
43    }
44
45    let selector: [u8; 4] = [data[0], data[1], data[2], data[3]];
46
47    let result = match selector {
48        ADD_FILTERED_TX => handle_add_filtered_tx(&mut input),
49        DELETE_FILTERED_TX => handle_delete_filtered_tx(&mut input),
50        IS_TX_FILTERED => handle_is_tx_filtered(&mut input),
51        _ => Err(PrecompileError::other("unknown selector")),
52    };
53    crate::gas_check(input.gas, result)
54}
55
56// ── helpers ──────────────────────────────────────────────────────────
57
58fn load_accounts(input: &mut PrecompileInput<'_>) -> Result<(), PrecompileError> {
59    input
60        .internals_mut()
61        .load_account(ARBOS_STATE_ADDRESS)
62        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
63    input
64        .internals_mut()
65        .load_account(FILTERED_TX_STATE_ADDRESS)
66        .map_err(|e| PrecompileError::other(format!("load_account: {e:?}")))?;
67    Ok(())
68}
69
70fn sload_arbos(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
71    let val = input
72        .internals_mut()
73        .sload(ARBOS_STATE_ADDRESS, slot)
74        .map_err(|_| PrecompileError::other("sload failed"))?;
75    Ok(val.data)
76}
77
78fn sload_filtered(input: &mut PrecompileInput<'_>, slot: U256) -> Result<U256, PrecompileError> {
79    let val = input
80        .internals_mut()
81        .sload(FILTERED_TX_STATE_ADDRESS, slot)
82        .map_err(|_| PrecompileError::other("sload failed"))?;
83    Ok(val.data)
84}
85
86fn sstore_filtered(
87    input: &mut PrecompileInput<'_>,
88    slot: U256,
89    value: U256,
90) -> Result<(), PrecompileError> {
91    input
92        .internals_mut()
93        .sstore(FILTERED_TX_STATE_ADDRESS, slot, value)
94        .map_err(|_| PrecompileError::other("sstore failed"))?;
95    Ok(())
96}
97
98/// Compute the storage slot for a tx hash in the filtered transactions account.
99/// The filtered tx storage uses an empty storageKey, so: map_slot_b256(&[], &tx_hash).
100fn filtered_tx_slot(tx_hash: &B256) -> U256 {
101    map_slot_b256(&[], tx_hash)
102}
103
104/// Check if caller is a transaction filterer via the TransactionFilterers address set.
105fn is_transaction_filterer(
106    input: &mut PrecompileInput<'_>,
107    addr: Address,
108) -> Result<bool, PrecompileError> {
109    // TransactionFilterers is at subspace [11] in ArbOS state.
110    // byAddress sub-storage is at [0] within the address set.
111    let filterer_key = derive_subspace_key(ROOT_STORAGE_KEY, TRANSACTION_FILTERER_SUBSPACE);
112    let by_address_key = derive_subspace_key(filterer_key.as_slice(), &[0]);
113    let addr_hash = B256::left_padding_from(addr.as_slice());
114    let slot = map_slot_b256(by_address_key.as_slice(), &addr_hash);
115    let val = sload_arbos(input, slot)?;
116    Ok(val != U256::ZERO)
117}
118
119/// Check if a transaction hash is in the filtered transactions list.
120fn handle_is_tx_filtered(input: &mut PrecompileInput<'_>) -> PrecompileResult {
121    let data = input.data;
122    if data.len() < 36 {
123        return Err(PrecompileError::other("input too short"));
124    }
125
126    let gas_limit = input.gas;
127    let tx_hash = B256::from_slice(&data[4..36]);
128    load_accounts(input)?;
129
130    let slot = filtered_tx_slot(&tx_hash);
131    let value = sload_filtered(input, slot)?;
132    let is_filtered = if value == PRESENT_VALUE {
133        U256::from(1u64)
134    } else {
135        U256::ZERO
136    };
137
138    Ok(PrecompileOutput::new(
139        (SLOAD_GAS + COPY_GAS).min(gas_limit),
140        is_filtered.to_be_bytes::<32>().to_vec().into(),
141    ))
142}
143
144/// Add a transaction hash to the filtered transactions list.
145fn handle_add_filtered_tx(input: &mut PrecompileInput<'_>) -> PrecompileResult {
146    let data = input.data;
147    if data.len() < 36 {
148        return Err(PrecompileError::other("input too short"));
149    }
150
151    let gas_limit = input.gas;
152    let tx_hash = B256::from_slice(&data[4..36]);
153    let caller = input.caller;
154    load_accounts(input)?;
155
156    if !is_transaction_filterer(input, caller)? {
157        return Err(PrecompileError::other(
158            "caller is not a transaction filterer",
159        ));
160    }
161
162    let slot = filtered_tx_slot(&tx_hash);
163    sstore_filtered(input, slot, PRESENT_VALUE)?;
164
165    let gas_used = 2 * SLOAD_GAS + SSTORE_GAS + COPY_GAS;
166    Ok(PrecompileOutput::new(
167        gas_used.min(gas_limit),
168        vec![].into(),
169    ))
170}
171
172/// Delete a transaction hash from the filtered transactions list.
173fn handle_delete_filtered_tx(input: &mut PrecompileInput<'_>) -> PrecompileResult {
174    let data = input.data;
175    if data.len() < 36 {
176        return Err(PrecompileError::other("input too short"));
177    }
178
179    let gas_limit = input.gas;
180    let tx_hash = B256::from_slice(&data[4..36]);
181    let caller = input.caller;
182    load_accounts(input)?;
183
184    if !is_transaction_filterer(input, caller)? {
185        return Err(PrecompileError::other(
186            "caller is not a transaction filterer",
187        ));
188    }
189
190    let slot = filtered_tx_slot(&tx_hash);
191    sstore_filtered(input, slot, U256::ZERO)?;
192
193    let gas_used = 2 * SLOAD_GAS + SSTORE_GAS + COPY_GAS;
194    Ok(PrecompileOutput::new(
195        gas_used.min(gas_limit),
196        vec![].into(),
197    ))
198}