signet_types/
magic_sig.rs

1use alloy::primitives::{Address, Signature, B256};
2use signet_zenith::MINTER_ADDRESS;
3
4/// A sentinel value to identify the magic signature. This is encoded in the
5/// S value, and renders the S value invalid for Ethereum-based chains
6/// supporting EIP-2.
7///
8/// [EIP-2]: https://eips.ethereum.org/EIPS/eip-2
9pub(crate) const MAGIC_SIG_SENTINEL: [u8; 4] = [0xff, 0xee, 0xdd, 0xcc];
10
11/// Enum of flags
12#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
13#[repr(u8)]
14pub(crate) enum Flags {
15    /// Flag used to identify Enters
16    Enter = 0x01,
17    /// Flag used to identify EnterTokens
18    EnterToken = 0x02,
19    /// Flag used to identify Transacts
20    Transact = 0x03,
21}
22
23impl From<Flags> for u8 {
24    fn from(flag: Flags) -> u8 {
25        flag as u8
26    }
27}
28
29impl TryFrom<u8> for Flags {
30    type Error = ();
31
32    fn try_from(value: u8) -> Result<Self, Self::Error> {
33        match value {
34            0x01 => Ok(Self::Enter),
35            0x02 => Ok(Self::EnterToken),
36            0x03 => Ok(Self::Transact),
37            _ => Err(()),
38        }
39    }
40}
41
42/// Type flag used to identify the Signet event that cauesd the rollup
43/// consensus to create this magic signature.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
45#[non_exhaustive]
46pub enum MagicSigInfo {
47    /// An enter event.
48    Enter,
49    /// An enter token event.
50    EnterToken,
51    /// A transact event.
52    Transact {
53        /// The address of the sender.
54        sender: Address,
55    },
56}
57
58impl MagicSigInfo {
59    /// Get the flag for the magic signature info.
60    const fn flag(&self) -> u8 {
61        match self {
62            Self::Enter => Flags::Enter as u8,
63            Self::EnterToken => Flags::EnterToken as u8,
64            Self::Transact { .. } => Flags::Transact as u8,
65        }
66    }
67
68    /// Write the magic signature info into a buffer.
69    pub fn write_into_s(&self, buf: &mut [u8]) {
70        debug_assert_eq!(buf.len(), 32);
71
72        buf[8] = self.flag();
73        if let Self::Transact { sender } = self {
74            buf[12..32].copy_from_slice(sender.as_slice());
75        }
76    }
77
78    /// Read the magic signature info from a signature S value.
79    pub fn read_from_s(s: impl AsRef<[u8]>) -> Option<Self> {
80        let s = s.as_ref();
81        if s.len() < 32 {
82            return None;
83        }
84        let flag = s[8].try_into().ok()?;
85        match flag {
86            Flags::Enter => Some(Self::Enter),
87            Flags::EnterToken => Some(Self::EnterToken),
88            Flags::Transact => Some(Self::Transact { sender: Address::from_slice(&s[12..]) }),
89        }
90    }
91
92    /// Get the sender from the magic signature info. For enter and enter token
93    /// events, this is the [`MINTER_ADDRESS`]. For transact events, this is the
94    /// sender.
95    pub const fn sender(&self) -> Address {
96        match self {
97            Self::Transact { sender } => *sender,
98            _ => MINTER_ADDRESS,
99        }
100    }
101}
102
103/// A magic signature, containing information about the host-chain event
104/// which caused the rollup transaction to occur.
105///
106/// This is used to "recover" the sender of a transaction that is not actually
107/// signed by the sender. This is used for enter events, enter token events,
108/// and transact events.
109///
110///  Magic signatures are used for Signet system events to allow the system to
111/// "recover" the sender of a transaction that is not actually signed by the
112/// sender. This is used for enter events, enter token events, and transact
113/// events. These signatures contain the sender, a sentinel, and a type flag,
114/// and are used by the RPC and other systems to determine the sender of the
115/// "transaction".
116///
117/// The magic signature format is as follows:
118/// - odd_y_parity: RESERVED (false)
119/// - r: 32-byte txid of the transaction that emitted the event
120/// - s:
121///   - `[0..4]`: A 4-byte sentinel value (0xffeeddcc).
122///   - `[4..8]`: A 4-bytes BE u32 containing the index of the event in the
123///     transaction's log array.
124///   - `[8..9]`: A 1-byte flag (0x01 for enter, 0x02 for enter token, 0x03
125///     for transact).
126///   - `[9..12]`: A 3-byte RESERVED region.
127///   - `[12..32]`: For transact events, (flag byte 3) the sender's address.
128///     RESERVED otherwise.
129///
130/// Because Ethereum-like chains enforce low-S signatures, the S value of the
131/// magic signature is invalid for Ethereum-based chains supporting [EIP-2].
132/// This means that the magic signature is never a valid signature for any
133/// relevant Ethereum-like chain.
134///
135/// [EIP-2]: https://eips.ethereum.org/EIPS/eip-2
136#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
137pub struct MagicSig {
138    /// The type of event, with any additional information.
139    pub ty: MagicSigInfo,
140    /// The transaction ID of the host chain transaction that emitted the event.
141    pub txid: B256,
142    /// The index of the event in the transaction's log array.
143    pub event_idx: usize,
144}
145
146impl MagicSig {
147    /// Try to [`MagicSig`] from a signature.
148    pub fn try_from_signature(sig: &Signature) -> Option<Self> {
149        let s = sig.s();
150        let s_bytes: [u8; 32] = s.to_be_bytes();
151        if !s_bytes.starts_with(&MAGIC_SIG_SENTINEL) {
152            return None;
153        }
154
155        let ty = MagicSigInfo::read_from_s(s_bytes)?;
156        let txid = sig.r().to_le_bytes().into();
157
158        let mut buf = [0u8; 4];
159        buf.copy_from_slice(&s_bytes[4..8]);
160        let event_idx = u32::from_be_bytes(buf) as usize;
161
162        Some(Self { ty, txid, event_idx })
163    }
164
165    /// Create a new [`MagicSig`] for an enter event.
166    pub const fn enter(txid: B256, event_idx: usize) -> Self {
167        Self { ty: MagicSigInfo::Enter, txid, event_idx }
168    }
169
170    /// Create a new [`MagicSig`] for an enter token event.
171    pub const fn enter_token(txid: B256, event_idx: usize) -> Self {
172        Self { ty: MagicSigInfo::EnterToken, txid, event_idx }
173    }
174
175    /// Create a new [`MagicSig`] for a transact event.
176    pub const fn transact(txid: B256, event_idx: usize, sender: Address) -> Self {
177        Self { ty: MagicSigInfo::Transact { sender }, txid, event_idx }
178    }
179
180    /// Get the sender of the magic signature.
181    pub const fn sender(&self) -> Address {
182        self.ty.sender()
183    }
184}
185
186impl From<MagicSig> for Signature {
187    fn from(value: MagicSig) -> Self {
188        let mut buf = [0u8; 64];
189        buf[..32].copy_from_slice(value.txid.as_ref());
190        buf[32..36].copy_from_slice(&MAGIC_SIG_SENTINEL);
191        buf[36..40].copy_from_slice(&(value.event_idx as u32).to_be_bytes());
192        value.ty.write_into_s(&mut buf[32..]);
193
194        Signature::from_bytes_and_parity(&buf, false)
195    }
196}
197
198#[cfg(test)]
199mod test {
200
201    use super::*;
202
203    // Enter token transaction, from Pecorino.
204    // Corresponding to tx hash 0xb67ee8b269385dee5e4b454a3fe64a75e1073144cf1b71836e00e35b38e8ee5a
205    const ENTER_TOKEN_TX: &str = include_str!("../../../tests/artifacts/enter_tx.json");
206
207    #[test]
208    fn test_enter_roundtrip() {
209        let txid = B256::repeat_byte(0xcc);
210
211        let msig = MagicSig::enter_token(txid, 333);
212        let sig: Signature = msig.into();
213
214        assert_eq!(MagicSig::try_from_signature(&sig), Some(msig))
215    }
216
217    #[test]
218    fn test_enter_token_roundtrip() {
219        let txid = B256::repeat_byte(0xcc);
220
221        let msig = MagicSig::enter_token(txid, 3821);
222        let sig: Signature = msig.into();
223
224        assert_eq!(MagicSig::try_from_signature(&sig), Some(msig))
225    }
226
227    #[test]
228    fn test_transact_roundtrip() {
229        let txid = B256::repeat_byte(0xcc);
230        let sender = Address::repeat_byte(0x12);
231
232        let msig = MagicSig::transact(txid, u32::MAX as usize, sender);
233        let sig: Signature = msig.into();
234
235        assert_eq!(MagicSig::try_from_signature(&sig), Some(msig))
236    }
237
238    #[test]
239    fn test_tx_decode_sig() {
240        let tx: alloy::consensus::TxEnvelope = serde_json::from_str(ENTER_TOKEN_TX).unwrap();
241        let sig = tx.signature();
242        let msig = MagicSig::try_from_signature(sig).unwrap();
243        // The sender should be equivalent to the minter address.
244        // Only on transact events the sender is the actual initiator of the tx on L1.
245        assert_eq!(msig.sender(), MINTER_ADDRESS)
246    }
247}