maili_consensus/deposit/
mod.rs

1//! Deposit Transaction type.
2
3mod envelope;
4pub use envelope::DepositTxEnvelope;
5
6mod source;
7pub use source::{
8    DepositContextDepositSource, DepositSourceDomain, DepositSourceDomainIdentifier,
9    L1InfoDepositSource, UpgradeDepositSource, UserDepositSource,
10};
11
12use alloc::vec::Vec;
13use alloy_consensus::{Sealable, Transaction, Typed2718};
14use alloy_eips::{
15    eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718},
16    eip2930::AccessList,
17};
18use alloy_primitives::{
19    keccak256, Address, Bytes, ChainId, PrimitiveSignature as Signature, TxHash, TxKind, B256, U256,
20};
21use alloy_rlp::{
22    Buf, BufMut, Decodable, Encodable, Error as DecodeError, Header, EMPTY_STRING_CODE,
23};
24use core::mem;
25
26/// Identifier for an Optimism deposit transaction
27pub const DEPOSIT_TX_TYPE_ID: u8 = 126; // 0x7E
28
29/// A trait representing a deposit transaction with specific attributes.
30pub trait DepositTransaction: Transaction {
31    /// Returns the hash that uniquely identifies the source of the deposit.
32    ///
33    /// # Returns
34    /// An `Option<B256>` containing the source hash if available.
35    fn source_hash(&self) -> Option<B256>;
36
37    /// Returns the optional mint value of the deposit transaction.
38    ///
39    /// # Returns
40    /// An `Option<u128>` representing the ETH value to mint on L2, if any.
41    fn mint(&self) -> Option<u128>;
42
43    /// Indicates whether the transaction is exempt from the L2 gas limit.
44    ///
45    /// # Returns
46    /// A `bool` indicating if the transaction is a system transaction.
47    fn is_system_transaction(&self) -> bool;
48}
49
50/// Deposit transactions, also known as deposits are initiated on L1, and executed on L2.
51#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
52#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
55pub struct TxDeposit {
56    /// Hash that uniquely identifies the source of the deposit.
57    pub source_hash: B256,
58    /// The address of the sender account.
59    pub from: Address,
60    /// The address of the recipient account, or the null (zero-length) address if the deposited
61    /// transaction is a contract creation.
62    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "TxKind::is_create"))]
63    pub to: TxKind,
64    /// The ETH value to mint on L2.
65    #[cfg_attr(feature = "serde", serde(default, with = "alloy_serde::quantity::opt"))]
66    pub mint: Option<u128>,
67    ///  The ETH value to send to the recipient account.
68    pub value: U256,
69    /// The gas limit for the L2 transaction.
70    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity", rename = "gas"))]
71    pub gas_limit: u64,
72    /// Field indicating if this transaction is exempt from the L2 gas limit.
73    #[cfg_attr(
74        feature = "serde",
75        serde(
76            default,
77            with = "alloy_serde::quantity",
78            rename = "isSystemTx",
79            skip_serializing_if = "core::ops::Not::not"
80        )
81    )]
82    pub is_system_transaction: bool,
83    /// Input has two uses depending if transaction is Create or Call (if `to` field is None or
84    /// Some).
85    pub input: Bytes,
86}
87
88impl DepositTransaction for TxDeposit {
89    #[inline]
90    fn source_hash(&self) -> Option<B256> {
91        Some(self.source_hash)
92    }
93
94    #[inline]
95    fn mint(&self) -> Option<u128> {
96        self.mint
97    }
98
99    #[inline]
100    fn is_system_transaction(&self) -> bool {
101        self.is_system_transaction
102    }
103}
104
105impl TxDeposit {
106    /// Decodes the inner [TxDeposit] fields from RLP bytes.
107    ///
108    /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following
109    /// RLP fields in the following order:
110    ///
111    /// - `source_hash`
112    /// - `from`
113    /// - `to`
114    /// - `mint`
115    /// - `value`
116    /// - `gas_limit`
117    /// - `is_system_transaction`
118    /// - `input`
119    pub fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
120        Ok(Self {
121            source_hash: Decodable::decode(buf)?,
122            from: Decodable::decode(buf)?,
123            to: Decodable::decode(buf)?,
124            mint: if *buf.first().ok_or(DecodeError::InputTooShort)? == EMPTY_STRING_CODE {
125                buf.advance(1);
126                None
127            } else {
128                Some(Decodable::decode(buf)?)
129            },
130            value: Decodable::decode(buf)?,
131            gas_limit: Decodable::decode(buf)?,
132            is_system_transaction: Decodable::decode(buf)?,
133            input: Decodable::decode(buf)?,
134        })
135    }
136
137    /// Decodes the transaction from RLP bytes.
138    pub fn rlp_decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
139        let header = Header::decode(buf)?;
140        if !header.list {
141            return Err(alloy_rlp::Error::UnexpectedString);
142        }
143        let remaining = buf.len();
144
145        if header.payload_length > remaining {
146            return Err(alloy_rlp::Error::InputTooShort);
147        }
148
149        let this = Self::rlp_decode_fields(buf)?;
150
151        if buf.len() + header.payload_length != remaining {
152            return Err(alloy_rlp::Error::UnexpectedLength);
153        }
154
155        Ok(this)
156    }
157
158    /// Outputs the length of the transaction's fields, without a RLP header or length of the
159    /// eip155 fields.
160    pub(crate) fn rlp_encoded_fields_length(&self) -> usize {
161        self.source_hash.length()
162            + self.from.length()
163            + self.to.length()
164            + self.mint.map_or(1, |mint| mint.length())
165            + self.value.length()
166            + self.gas_limit.length()
167            + self.is_system_transaction.length()
168            + self.input.0.length()
169    }
170
171    /// Encodes only the transaction's fields into the desired buffer, without a RLP header.
172    /// <https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/deposits.md#the-deposited-transaction-type>
173    pub(crate) fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
174        self.source_hash.encode(out);
175        self.from.encode(out);
176        self.to.encode(out);
177        if let Some(mint) = self.mint {
178            mint.encode(out);
179        } else {
180            out.put_u8(EMPTY_STRING_CODE);
181        }
182        self.value.encode(out);
183        self.gas_limit.encode(out);
184        self.is_system_transaction.encode(out);
185        self.input.encode(out);
186    }
187
188    /// Calculates a heuristic for the in-memory size of the [TxDeposit] transaction.
189    #[inline]
190    pub fn size(&self) -> usize {
191        mem::size_of::<B256>() + // source_hash
192        mem::size_of::<Address>() + // from
193        self.to.size() + // to
194        mem::size_of::<Option<u128>>() + // mint
195        mem::size_of::<U256>() + // value
196        mem::size_of::<u128>() + // gas_limit
197        mem::size_of::<bool>() + // is_system_transaction
198        self.input.len() // input
199    }
200
201    /// Create an rlp header for the transaction.
202    fn rlp_header(&self) -> Header {
203        Header { list: true, payload_length: self.rlp_encoded_fields_length() }
204    }
205
206    /// RLP encodes the transaction.
207    pub fn rlp_encode(&self, out: &mut dyn BufMut) {
208        self.rlp_header().encode(out);
209        self.rlp_encode_fields(out);
210    }
211
212    /// Get the length of the transaction when RLP encoded.
213    pub fn rlp_encoded_length(&self) -> usize {
214        self.rlp_header().length_with_payload()
215    }
216
217    /// Get the length of the transaction when EIP-2718 encoded. This is the
218    /// 1 byte type flag + the length of the RLP encoded transaction.
219    pub fn eip2718_encoded_length(&self) -> usize {
220        self.rlp_encoded_length() + 1
221    }
222
223    fn network_header(&self) -> Header {
224        Header { list: false, payload_length: self.eip2718_encoded_length() }
225    }
226
227    /// Get the length of the transaction when network encoded. This is the
228    /// EIP-2718 encoded length with an outer RLP header.
229    pub fn network_encoded_length(&self) -> usize {
230        self.network_header().length_with_payload()
231    }
232
233    /// Network encode the transaction with the given signature.
234    pub fn network_encode(&self, out: &mut dyn BufMut) {
235        self.network_header().encode(out);
236        self.encode_2718(out);
237    }
238
239    /// Calculate the transaction hash.
240    pub fn tx_hash(&self) -> TxHash {
241        let mut buf = Vec::with_capacity(self.eip2718_encoded_length());
242        self.encode_2718(&mut buf);
243        keccak256(&buf)
244    }
245
246    /// Returns the signature for the optimism deposit transactions, which don't include a
247    /// signature.
248    pub fn signature() -> Signature {
249        Signature::new(U256::ZERO, U256::ZERO, false)
250    }
251}
252
253impl Typed2718 for TxDeposit {
254    fn ty(&self) -> u8 {
255        DEPOSIT_TX_TYPE_ID
256    }
257}
258
259impl Transaction for TxDeposit {
260    fn chain_id(&self) -> Option<ChainId> {
261        None
262    }
263
264    fn nonce(&self) -> u64 {
265        0u64
266    }
267
268    fn gas_limit(&self) -> u64 {
269        self.gas_limit
270    }
271
272    fn gas_price(&self) -> Option<u128> {
273        None
274    }
275
276    fn max_fee_per_gas(&self) -> u128 {
277        0
278    }
279
280    fn max_priority_fee_per_gas(&self) -> Option<u128> {
281        None
282    }
283
284    fn max_fee_per_blob_gas(&self) -> Option<u128> {
285        None
286    }
287
288    fn priority_fee_or_price(&self) -> u128 {
289        0
290    }
291
292    fn effective_gas_price(&self, _: Option<u64>) -> u128 {
293        0
294    }
295
296    fn is_dynamic_fee(&self) -> bool {
297        false
298    }
299
300    fn kind(&self) -> TxKind {
301        self.to
302    }
303
304    fn is_create(&self) -> bool {
305        self.to.is_create()
306    }
307
308    fn value(&self) -> U256 {
309        self.value
310    }
311
312    fn input(&self) -> &Bytes {
313        &self.input
314    }
315
316    fn access_list(&self) -> Option<&AccessList> {
317        None
318    }
319
320    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
321        None
322    }
323
324    fn authorization_list(&self) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> {
325        None
326    }
327}
328
329impl Encodable2718 for TxDeposit {
330    fn type_flag(&self) -> Option<u8> {
331        Some(DEPOSIT_TX_TYPE_ID)
332    }
333
334    fn encode_2718_len(&self) -> usize {
335        self.eip2718_encoded_length()
336    }
337
338    fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
339        out.put_u8(DEPOSIT_TX_TYPE_ID);
340        self.rlp_encode(out);
341    }
342}
343
344impl Decodable2718 for TxDeposit {
345    fn typed_decode(ty: u8, data: &mut &[u8]) -> Eip2718Result<Self> {
346        if ty != DEPOSIT_TX_TYPE_ID {
347            return Err(Eip2718Error::UnexpectedType(ty));
348        }
349        let tx = Self::decode(data)?;
350        Ok(tx)
351    }
352
353    fn fallback_decode(data: &mut &[u8]) -> Eip2718Result<Self> {
354        let tx = Self::decode(data)?;
355        Ok(tx)
356    }
357}
358
359impl alloy_rlp::Encodable for TxDeposit {
360    fn encode(&self, out: &mut dyn BufMut) {
361        Header { list: true, payload_length: self.rlp_encoded_fields_length() }.encode(out);
362        self.rlp_encode_fields(out);
363    }
364
365    fn length(&self) -> usize {
366        let payload_length = self.rlp_encoded_fields_length();
367        Header { list: true, payload_length }.length() + payload_length
368    }
369}
370
371impl alloy_rlp::Decodable for TxDeposit {
372    fn decode(data: &mut &[u8]) -> alloy_rlp::Result<Self> {
373        Self::rlp_decode(data)
374    }
375}
376
377impl Sealable for TxDeposit {
378    fn hash_slow(&self) -> B256 {
379        self.tx_hash()
380    }
381}
382
383/// Deposit transactions don't have a signature, however, we include an empty signature in the
384/// response for better compatibility.
385///
386/// This function can be used as `serialize_with` serde attribute for the [`TxDeposit`] and will
387/// flatten [`TxDeposit::signature`] into response.
388#[cfg(feature = "serde")]
389pub fn serde_deposit_tx_rpc<T: serde::Serialize, S: serde::Serializer>(
390    value: &T,
391    serializer: S,
392) -> Result<S::Ok, S::Error> {
393    use serde::Serialize;
394
395    #[derive(Serialize)]
396    struct SerdeHelper<'a, T> {
397        #[serde(flatten)]
398        value: &'a T,
399        #[serde(flatten)]
400        signature: Signature,
401    }
402
403    SerdeHelper { value, signature: TxDeposit::signature() }.serialize(serializer)
404}
405
406/// Bincode-compatible [`TxDeposit`] serde implementation.
407#[cfg(feature = "serde-bincode-compat")]
408pub mod serde_bincode_compat {
409    use alloc::borrow::Cow;
410    use alloy_primitives::{Address, Bytes, TxKind, B256, U256};
411    use serde::{Deserialize, Deserializer, Serialize, Serializer};
412    use serde_with::{DeserializeAs, SerializeAs};
413
414    /// Bincode-compatible [`super::TxDeposit`] serde implementation.
415    ///
416    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
417    /// ```rust
418    /// use op_alloy_consensus::{serde_bincode_compat, TxDeposit};
419    /// use serde::{Deserialize, Serialize};
420    /// use serde_with::serde_as;
421    ///
422    /// #[serde_as]
423    /// #[derive(Serialize, Deserialize)]
424    /// struct Data {
425    ///     #[serde_as(as = "serde_bincode_compat::TxDeposit")]
426    ///     transaction: TxDeposit,
427    /// }
428    /// ```
429    #[derive(Debug, Serialize, Deserialize)]
430    pub struct TxDeposit<'a> {
431        source_hash: B256,
432        from: Address,
433        #[serde(default)]
434        to: TxKind,
435        #[serde(default)]
436        mint: Option<u128>,
437        value: U256,
438        gas_limit: u64,
439        is_system_transaction: bool,
440        input: Cow<'a, Bytes>,
441    }
442
443    impl<'a> From<&'a super::TxDeposit> for TxDeposit<'a> {
444        fn from(value: &'a super::TxDeposit) -> Self {
445            Self {
446                source_hash: value.source_hash,
447                from: value.from,
448                to: value.to,
449                mint: value.mint,
450                value: value.value,
451                gas_limit: value.gas_limit,
452                is_system_transaction: value.is_system_transaction,
453                input: Cow::Borrowed(&value.input),
454            }
455        }
456    }
457
458    impl<'a> From<TxDeposit<'a>> for super::TxDeposit {
459        fn from(value: TxDeposit<'a>) -> Self {
460            Self {
461                source_hash: value.source_hash,
462                from: value.from,
463                to: value.to,
464                mint: value.mint,
465                value: value.value,
466                gas_limit: value.gas_limit,
467                is_system_transaction: value.is_system_transaction,
468                input: value.input.into_owned(),
469            }
470        }
471    }
472
473    impl SerializeAs<super::TxDeposit> for TxDeposit<'_> {
474        fn serialize_as<S>(source: &super::TxDeposit, serializer: S) -> Result<S::Ok, S::Error>
475        where
476            S: Serializer,
477        {
478            TxDeposit::from(source).serialize(serializer)
479        }
480    }
481
482    impl<'de> DeserializeAs<'de, super::TxDeposit> for TxDeposit<'de> {
483        fn deserialize_as<D>(deserializer: D) -> Result<super::TxDeposit, D::Error>
484        where
485            D: Deserializer<'de>,
486        {
487            TxDeposit::deserialize(deserializer).map(Into::into)
488        }
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495    use alloy_primitives::hex;
496    use alloy_rlp::BytesMut;
497
498    #[test]
499    fn test_deposit_transaction_trait() {
500        let tx = TxDeposit {
501            source_hash: B256::with_last_byte(42),
502            from: Address::default(),
503            to: TxKind::default(),
504            mint: Some(100),
505            value: U256::from(1000),
506            gas_limit: 50000,
507            is_system_transaction: true,
508            input: Bytes::default(),
509        };
510
511        assert_eq!(tx.source_hash(), Some(B256::with_last_byte(42)));
512        assert_eq!(tx.mint(), Some(100));
513        assert!(tx.is_system_transaction());
514    }
515
516    #[test]
517    fn test_deposit_transaction_without_mint() {
518        let tx = TxDeposit {
519            source_hash: B256::default(),
520            from: Address::default(),
521            to: TxKind::default(),
522            mint: None,
523            value: U256::default(),
524            gas_limit: 50000,
525            is_system_transaction: false,
526            input: Bytes::default(),
527        };
528
529        assert_eq!(tx.source_hash(), Some(B256::default()));
530        assert_eq!(tx.mint(), None);
531        assert!(!tx.is_system_transaction());
532    }
533
534    #[test]
535    fn test_deposit_transaction_to_contract() {
536        let contract_address = Address::with_last_byte(0xFF);
537        let tx = TxDeposit {
538            source_hash: B256::default(),
539            from: Address::default(),
540            to: TxKind::Call(contract_address),
541            mint: Some(200),
542            value: U256::from(500),
543            gas_limit: 100000,
544            is_system_transaction: false,
545            input: Bytes::from_static(&[1, 2, 3]),
546        };
547
548        assert_eq!(tx.source_hash(), Some(B256::default()));
549        assert_eq!(tx.mint(), Some(200));
550        assert!(!tx.is_system_transaction());
551        assert_eq!(tx.kind(), TxKind::Call(contract_address));
552    }
553
554    #[test]
555    fn test_rlp_roundtrip() {
556        let bytes = Bytes::from_static(&hex!("7ef9015aa044bae9d41b8380d781187b426c6fe43df5fb2fb57bd4466ef6a701e1f01e015694deaddeaddeaddeaddeaddeaddeaddeaddead000194420000000000000000000000000000000000001580808408f0d18001b90104015d8eb900000000000000000000000000000000000000000000000000000000008057650000000000000000000000000000000000000000000000000000000063d96d10000000000000000000000000000000000000000000000000000000000009f35273d89754a1e0387b89520d989d3be9c37c1f32495a88faf1ea05c61121ab0d1900000000000000000000000000000000000000000000000000000000000000010000000000000000000000002d679b567db6187c0c8323fa982cfb88b74dbcc7000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240"));
557        let tx_a = TxDeposit::decode(&mut bytes[1..].as_ref()).unwrap();
558        let mut buf_a = BytesMut::default();
559        tx_a.encode(&mut buf_a);
560        assert_eq!(&buf_a[..], &bytes[1..]);
561    }
562
563    #[test]
564    fn test_encode_decode_fields() {
565        let original = TxDeposit {
566            source_hash: B256::default(),
567            from: Address::default(),
568            to: TxKind::default(),
569            mint: Some(100),
570            value: U256::default(),
571            gas_limit: 50000,
572            is_system_transaction: true,
573            input: Bytes::default(),
574        };
575
576        let mut buffer = BytesMut::new();
577        original.rlp_encode_fields(&mut buffer);
578        let decoded = TxDeposit::rlp_decode_fields(&mut &buffer[..]).expect("Failed to decode");
579
580        assert_eq!(original, decoded);
581    }
582
583    #[test]
584    fn test_encode_with_and_without_header() {
585        let tx_deposit = TxDeposit {
586            source_hash: B256::default(),
587            from: Address::default(),
588            to: TxKind::default(),
589            mint: Some(100),
590            value: U256::default(),
591            gas_limit: 50000,
592            is_system_transaction: true,
593            input: Bytes::default(),
594        };
595
596        let mut buffer_with_header = BytesMut::new();
597        tx_deposit.encode(&mut buffer_with_header);
598
599        let mut buffer_without_header = BytesMut::new();
600        tx_deposit.rlp_encode_fields(&mut buffer_without_header);
601
602        assert!(buffer_with_header.len() > buffer_without_header.len());
603    }
604
605    #[test]
606    fn test_payload_length() {
607        let tx_deposit = TxDeposit {
608            source_hash: B256::default(),
609            from: Address::default(),
610            to: TxKind::default(),
611            mint: Some(100),
612            value: U256::default(),
613            gas_limit: 50000,
614            is_system_transaction: true,
615            input: Bytes::default(),
616        };
617
618        assert!(tx_deposit.size() > tx_deposit.rlp_encoded_fields_length());
619    }
620
621    #[test]
622    fn test_encode_inner_with_and_without_header() {
623        let tx_deposit = TxDeposit {
624            source_hash: B256::default(),
625            from: Address::default(),
626            to: TxKind::default(),
627            mint: Some(100),
628            value: U256::default(),
629            gas_limit: 50000,
630            is_system_transaction: true,
631            input: Bytes::default(),
632        };
633
634        let mut buffer_with_header = BytesMut::new();
635        tx_deposit.network_encode(&mut buffer_with_header);
636
637        let mut buffer_without_header = BytesMut::new();
638        tx_deposit.encode_2718(&mut buffer_without_header);
639
640        assert!(buffer_with_header.len() > buffer_without_header.len());
641    }
642
643    #[test]
644    fn test_payload_length_header() {
645        let tx_deposit = TxDeposit {
646            source_hash: B256::default(),
647            from: Address::default(),
648            to: TxKind::default(),
649            mint: Some(100),
650            value: U256::default(),
651            gas_limit: 50000,
652            is_system_transaction: true,
653            input: Bytes::default(),
654        };
655
656        let total_len = tx_deposit.network_encoded_length();
657        let len_without_header = tx_deposit.eip2718_encoded_length();
658
659        assert!(total_len > len_without_header);
660    }
661
662    #[cfg(feature = "serde-bincode-compat")]
663    #[test]
664    fn test_tx_deposit_bincode_roundtrip() {
665        use arbitrary::Arbitrary;
666        use rand::Rng;
667
668        #[serde_with::serde_as]
669        #[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
670        struct Data {
671            #[serde_as(as = "serde_bincode_compat::TxDeposit")]
672            transaction: TxDeposit,
673        }
674
675        let mut bytes = [0u8; 1024];
676        rand::thread_rng().fill(bytes.as_mut_slice());
677        let data = Data {
678            transaction: TxDeposit::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap(),
679        };
680
681        let encoded = bincode::serialize(&data).unwrap();
682        let decoded: Data = bincode::deserialize(&encoded).unwrap();
683        assert_eq!(decoded, data);
684    }
685}