alloy_provider/ext/mev/
with_auth.rs

1use crate::{ext::FLASHBOTS_SIGNATURE_HEADER, ProviderCall};
2use alloy_json_rpc::{RpcRecv, RpcSend};
3use alloy_primitives::{hex, hex::FromHexError, keccak256, Address, Signature, SignatureError};
4use alloy_rpc_client::RpcCall;
5use alloy_signer::Signer;
6use alloy_transport::{TransportErrorKind, TransportResult};
7use http::{HeaderMap, HeaderName, HeaderValue};
8use std::future::IntoFuture;
9
10/// Error returned by [`verify_flashbots_signature`].
11#[derive(Debug, thiserror::Error)]
12pub enum FlashbotsSignatureError {
13    /// Invalid signature format, expected `address:signature`.
14    #[error("invalid signature format, expected `address:signature`")]
15    InvalidFormat,
16    /// Invalid address.
17    #[error("invalid address")]
18    InvalidAddress(#[from] FromHexError),
19    /// Invalid signature.
20    #[error("invalid signature")]
21    InvalidSignature(#[from] SignatureError),
22    /// Signature mismatch.
23    #[error("signature mismatch: expected {expected}, actual {actual}")]
24    SignatureMismatch {
25        /// Expected address from the signature header.
26        expected: Address,
27        /// Actual address recovered from the signature.
28        actual: Address,
29    },
30}
31
32/// A builder for MEV RPC calls that allow optional Flashbots authentication.
33pub struct MevBuilder<Params, Resp, Output = Resp, Map = fn(Resp) -> Output>
34where
35    Params: RpcSend,
36    Resp: RpcRecv,
37    Map: Fn(Resp) -> Output,
38{
39    inner: RpcCall<Params, Resp, Output, Map>,
40    signer: Option<Box<dyn Signer + Send + Sync>>,
41}
42
43impl<Params, Resp, Output, Map> std::fmt::Debug for MevBuilder<Params, Resp, Output, Map>
44where
45    Params: RpcSend + std::fmt::Debug,
46    Resp: RpcRecv + std::fmt::Debug,
47    Output: std::fmt::Debug,
48    Map: Fn(Resp) -> Output + Clone + std::fmt::Debug,
49{
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        f.debug_struct("MevBuilder").field("inner", &self.inner).finish()
52    }
53}
54
55impl<Params, Resp, Output, Map> MevBuilder<Params, Resp, Output, Map>
56where
57    Params: RpcSend,
58    Resp: RpcRecv,
59    Map: Fn(Resp) -> Output + Clone,
60{
61    /// Create a new [`MevBuilder`] from a [`RpcCall`].
62    pub const fn new_rpc(inner: RpcCall<Params, Resp, Output, Map>) -> Self {
63        Self { inner, signer: None }
64    }
65}
66
67impl<Params, Resp, Output, Map> From<RpcCall<Params, Resp, Output, Map>>
68    for MevBuilder<Params, Resp, Output, Map>
69where
70    Params: RpcSend,
71    Resp: RpcRecv,
72    Map: Fn(Resp) -> Output + Clone,
73{
74    fn from(inner: RpcCall<Params, Resp, Output, Map>) -> Self {
75        Self::new_rpc(inner)
76    }
77}
78
79impl<Params, Resp, Output, Map> MevBuilder<Params, Resp, Output, Map>
80where
81    Params: RpcSend,
82    Resp: RpcRecv,
83    Map: Fn(Resp) -> Output,
84{
85    /// Enables Flashbots authentication using the provided signer.
86    ///
87    /// The signer is used to generate the `X-Flashbots-Signature` header, which will be included
88    /// in the request if the transport supports HTTP headers.
89    pub fn with_auth<S: Signer + Send + Sync + 'static>(mut self, signer: S) -> Self {
90        self.signer = Some(Box::new(signer));
91        self
92    }
93}
94
95impl<Params, Resp, Output, Map> IntoFuture for MevBuilder<Params, Resp, Output, Map>
96where
97    Params: RpcSend + 'static,
98    Resp: RpcRecv,
99    Output: 'static,
100    Map: Fn(Resp) -> Output + Send + 'static,
101{
102    type Output = TransportResult<Output>;
103    type IntoFuture = ProviderCall<Params, Resp, Output, Map>;
104
105    fn into_future(self) -> Self::IntoFuture {
106        if let Some(signer) = self.signer {
107            let fut = async move {
108                // Generate the Flashbots signature for the request body
109                let body = serde_json::to_string(&self.inner.request())
110                    .map_err(TransportErrorKind::custom)?;
111                let signature = sign_flashbots_payload(body, &signer)
112                    .await
113                    .map_err(TransportErrorKind::custom)?;
114
115                // Add the Flashbots signature to the request headers
116                let headers = HeaderMap::from_iter([(
117                    HeaderName::from_static(FLASHBOTS_SIGNATURE_HEADER),
118                    HeaderValue::from_str(signature.as_str())
119                        .map_err(TransportErrorKind::custom)?,
120                )]);
121
122                // Patch the existing RPC call with the new headers
123                let rpc_call = self.inner.map_meta(|mut meta| {
124                    meta.extensions_mut().get_or_insert_default::<HeaderMap>().extend(headers);
125                    meta
126                });
127
128                rpc_call.await
129            };
130            return ProviderCall::BoxedFuture(Box::pin(fut));
131        }
132        ProviderCall::RpcCall(self.inner)
133    }
134}
135
136/// Uses the provided signer to generate a Flashbots `X-Flashbots-Signature` header value in the
137/// format `{address}:{signature_hex}`.
138///
139/// See [Flashbots docs](https://docs.flashbots.net/flashbots-auction/advanced/rpc-endpoint#authentication) for more information.
140///
141/// # Example
142///
143/// ```
144/// use alloy_provider::ext::sign_flashbots_payload;
145/// use alloy_signer_local::PrivateKeySigner;
146///
147/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
148/// let signer: PrivateKeySigner =
149///     "0x0000000000000000000000000000000000000000000000000000000000123456".parse()?;
150/// let body = "sign this message".to_string();
151/// let signature = sign_flashbots_payload(body, &signer).await?;
152/// assert!(signature.starts_with("0xd5F5175D014F28c85F7D67A111C2c9335D7CD771:0x"));
153/// # Ok(())
154/// # }
155/// ```
156pub async fn sign_flashbots_payload<S: Signer + Send + Sync>(
157    body: String,
158    signer: &S,
159) -> Result<String, alloy_signer::Error> {
160    let message_hash = keccak256(body.as_bytes()).to_string();
161    let signature = signer.sign_message(message_hash.as_bytes()).await?;
162
163    // Normalized recovery byte (0/1) following the canonical signature encoding
164    let mut sig_bytes = [0u8; 65];
165    sig_bytes[..32].copy_from_slice(&signature.r().to_be_bytes::<32>());
166    sig_bytes[32..64].copy_from_slice(&signature.s().to_be_bytes::<32>());
167    sig_bytes[64] = signature.v() as u8;
168    Ok(format!("{}:{}", signer.address(), hex::encode_prefixed(sig_bytes)))
169}
170
171/// Verifies a Flashbots signature and returns the recovered signer address.
172///
173/// The signature format is `{address}:{signature_hex}` as produced by
174/// [`sign_flashbots_payload`]. Both normalized (v=0/1) and EIP-155 (v=27/28)
175/// recovery bytes are supported.
176///
177/// See [Flashbots docs](https://docs.flashbots.net/flashbots-auction/advanced/rpc-endpoint#authentication) for more information.
178///
179/// # Example
180///
181/// ```
182/// use alloy_provider::ext::verify_flashbots_signature;
183///
184/// let signature_header = "0xd5F5175D014F28c85F7D67A111C2c9335D7CD771:0x983dc7c520db0d287faff3cd0aef81d5a7f4ffd3473440d3f705da16299724271f660b6fe367f455b205bc014eff3e20defd011f92000f94d39365ca0bc7867200";
185/// let body = b"sign this message";
186/// let address = verify_flashbots_signature(signature_header, body).unwrap();
187/// assert_eq!(address.to_string(), "0xd5F5175D014F28c85F7D67A111C2c9335D7CD771");
188/// ```
189pub fn verify_flashbots_signature(
190    signature_header: &str,
191    body: &[u8],
192) -> Result<Address, FlashbotsSignatureError> {
193    let (address_str, sig_str) =
194        signature_header.split_once(':').ok_or(FlashbotsSignatureError::InvalidFormat)?;
195
196    let expected = address_str.parse::<Address>()?;
197    let signature = sig_str.parse::<Signature>()?;
198
199    let message_hash = keccak256(body).to_string();
200    let actual = signature.recover_address_from_msg(message_hash.as_bytes())?;
201
202    if actual != expected {
203        return Err(FlashbotsSignatureError::SignatureMismatch { expected, actual });
204    }
205
206    Ok(actual)
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use alloy_primitives::{address, b256};
213    use alloy_signer_local::PrivateKeySigner;
214
215    const TEST_BODY: &str = "sign this message";
216    const TEST_SIGNATURE: &str = "0xd5F5175D014F28c85F7D67A111C2c9335D7CD771:0x983dc7c520db0d287faff3cd0aef81d5a7f4ffd3473440d3f705da16299724271f660b6fe367f455b205bc014eff3e20defd011f92000f94d39365ca0bc7867200";
217
218    #[tokio::test]
219    async fn test_sign_flashbots_payload() {
220        let signer = PrivateKeySigner::from_bytes(&b256!(
221            "0x0000000000000000000000000000000000000000000000000000000000123456"
222        ))
223        .unwrap();
224        let signature = sign_flashbots_payload(TEST_BODY.to_string(), &signer).await.unwrap();
225        assert_eq!(signature, TEST_SIGNATURE);
226    }
227
228    #[tokio::test]
229    async fn test_verify_flashbots_signature_roundtrip() {
230        let signer = PrivateKeySigner::from_bytes(&b256!(
231            "0x0000000000000000000000000000000000000000000000000000000000123456"
232        ))
233        .unwrap();
234
235        let signature = sign_flashbots_payload(TEST_BODY.to_string(), &signer).await.unwrap();
236        let recovered = verify_flashbots_signature(&signature, TEST_BODY.as_bytes()).unwrap();
237        assert_eq!(recovered, signer.address());
238    }
239
240    #[test]
241    fn test_verify_flashbots_signature_v0() {
242        // TEST_SIGNATURE uses v=0 (ends with "00")
243        let recovered = verify_flashbots_signature(TEST_SIGNATURE, TEST_BODY.as_bytes()).unwrap();
244        assert_eq!(recovered, address!("0xd5F5175D014F28c85F7D67A111C2c9335D7CD771"));
245    }
246
247    #[test]
248    fn test_verify_flashbots_signature_v27() {
249        // Replace last byte: v=0 (00) -> v=27 (1b)
250        let signature_v27 = format!("{}1b", &TEST_SIGNATURE[..TEST_SIGNATURE.len() - 2]);
251        let recovered = verify_flashbots_signature(&signature_v27, TEST_BODY.as_bytes()).unwrap();
252        assert_eq!(recovered, address!("0xd5F5175D014F28c85F7D67A111C2c9335D7CD771"));
253    }
254
255    #[test]
256    fn test_verify_flashbots_signature_invalid_format() {
257        let result = verify_flashbots_signature("invalid", b"body");
258        assert!(matches!(result, Err(FlashbotsSignatureError::InvalidFormat)));
259    }
260
261    #[test]
262    fn test_verify_flashbots_signature_invalid_address() {
263        let result = verify_flashbots_signature("notanaddress:0x1234", b"body");
264        assert!(matches!(result, Err(FlashbotsSignatureError::InvalidAddress(_))));
265    }
266
267    #[test]
268    fn test_verify_flashbots_signature_invalid_signature() {
269        let result = verify_flashbots_signature(
270            "0xd5F5175D014F28c85F7D67A111C2c9335D7CD771:0xinvalid",
271            b"body",
272        );
273        assert!(matches!(result, Err(FlashbotsSignatureError::InvalidSignature(_))));
274    }
275
276    #[test]
277    fn test_verify_flashbots_signature_mismatch_wrong_address() {
278        let wrong_address = Address::repeat_byte(0x01);
279        let sig_part = TEST_SIGNATURE.split_once(':').unwrap().1;
280        let mismatched = format!("{wrong_address}:{sig_part}");
281        let result = verify_flashbots_signature(&mismatched, TEST_BODY.as_bytes());
282        assert!(matches!(result, Err(FlashbotsSignatureError::SignatureMismatch { .. })));
283    }
284
285    #[test]
286    fn test_verify_flashbots_signature_mismatch_wrong_body() {
287        let result = verify_flashbots_signature(TEST_SIGNATURE, b"wrong body");
288        assert!(matches!(result, Err(FlashbotsSignatureError::SignatureMismatch { .. })));
289    }
290}