tempo_primitives/transaction/
key_authorization.rs1use super::SignatureType;
2use crate::transaction::PrimitiveSignature;
3use alloc::vec::Vec;
4use alloy_consensus::crypto::RecoveryError;
5use alloy_primitives::{Address, B256, U256, keccak256};
6use alloy_rlp::Encodable;
7
8#[derive(Clone, Debug, PartialEq, Eq, Hash, alloy_rlp::RlpEncodable, alloy_rlp::RlpDecodable)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
15#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
16#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))]
17#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact, rlp))]
18pub struct TokenLimit {
19 pub token: Address,
21
22 pub limit: U256,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq, Hash, alloy_rlp::RlpEncodable, alloy_rlp::RlpDecodable)]
36#[rlp(trailing)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
39#[cfg_attr(test, reth_codecs::add_arbitrary_tests(rlp))]
40pub struct KeyAuthorization {
41 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
44 pub chain_id: u64,
45
46 pub key_type: SignatureType,
48
49 pub key_id: Address,
51
52 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity::opt"))]
56 pub expiry: Option<u64>,
57
58 pub limits: Option<Vec<TokenLimit>>,
63}
64
65impl KeyAuthorization {
66 pub fn signature_hash(&self) -> B256 {
68 let mut buf = Vec::new();
69 self.encode(&mut buf);
70 keccak256(&buf)
71 }
72
73 pub fn has_unlimited_spending(&self) -> bool {
75 self.limits.is_none()
76 }
77
78 pub fn never_expires(&self) -> bool {
80 self.expiry.is_none()
81 }
82
83 pub fn into_signed(self, signature: PrimitiveSignature) -> SignedKeyAuthorization {
85 SignedKeyAuthorization {
86 authorization: self,
87 signature,
88 }
89 }
90
91 pub fn validate_chain_id(
96 &self,
97 expected_chain_id: u64,
98 is_t1c: bool,
99 ) -> Result<(), KeyAuthorizationChainIdError> {
100 if is_t1c {
101 if self.chain_id != expected_chain_id {
102 return Err(KeyAuthorizationChainIdError {
103 expected: expected_chain_id,
104 got: self.chain_id,
105 });
106 }
107 } else if self.chain_id != 0 && self.chain_id != expected_chain_id {
108 return Err(KeyAuthorizationChainIdError {
109 expected: expected_chain_id,
110 got: self.chain_id,
111 });
112 }
113 Ok(())
114 }
115
116 pub fn size(&self) -> usize {
118 size_of::<Self>()
119 + self
120 .limits
121 .as_ref()
122 .map_or(0, |limits| limits.capacity() * size_of::<TokenLimit>())
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub struct KeyAuthorizationChainIdError {
129 pub expected: u64,
131 pub got: u64,
133}
134
135#[derive(
137 Clone,
138 Debug,
139 PartialEq,
140 Eq,
141 Hash,
142 alloy_rlp::RlpEncodable,
143 alloy_rlp::RlpDecodable,
144 derive_more::Deref,
145)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
147#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
148#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
149#[rlp(trailing)]
150#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact, rlp))]
151pub struct SignedKeyAuthorization {
152 #[cfg_attr(feature = "serde", serde(flatten))]
154 #[deref]
155 pub authorization: KeyAuthorization,
156
157 pub signature: PrimitiveSignature,
159}
160
161impl SignedKeyAuthorization {
162 pub fn recover_signer(&self) -> Result<Address, RecoveryError> {
164 self.signature
165 .recover_signer(&self.authorization.signature_hash())
166 }
167
168 pub fn size(&self) -> usize {
170 self.authorization.size() + self.signature.size()
171 }
172}
173
174#[cfg(feature = "reth-codec")]
175impl reth_codecs::Compact for SignedKeyAuthorization {
176 fn to_compact<B>(&self, buf: &mut B) -> usize
177 where
178 B: alloy_rlp::BufMut + AsMut<[u8]>,
179 {
180 self.encode(buf);
182 self.length()
183 }
184
185 fn from_compact(mut buf: &[u8], _len: usize) -> (Self, &[u8]) {
186 let item = alloy_rlp::Decodable::decode(&mut buf)
187 .expect("Failed to decode KeyAuthorization from compact");
188 (item, buf)
189 }
190}
191
192#[cfg(any(test, feature = "arbitrary"))]
193impl<'a> arbitrary::Arbitrary<'a> for KeyAuthorization {
194 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
195 Ok(Self {
196 chain_id: u.arbitrary()?,
197 key_type: u.arbitrary()?,
198 key_id: u.arbitrary()?,
199 expiry: u.arbitrary::<Option<u64>>()?.filter(|v| *v != 0),
201 limits: u.arbitrary()?,
202 })
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use crate::transaction::{
210 TempoSignature,
211 tt_authorization::tests::{generate_secp256k1_keypair, sign_hash},
212 };
213
214 fn make_auth(expiry: Option<u64>, limits: Option<Vec<TokenLimit>>) -> KeyAuthorization {
215 KeyAuthorization {
216 chain_id: 1,
217 key_type: SignatureType::Secp256k1,
218 key_id: Address::random(),
219 expiry,
220 limits,
221 }
222 }
223
224 #[test]
225 fn test_signature_hash_and_recover_signer() {
226 let (signing_key, expected_address) = generate_secp256k1_keypair();
227
228 let auth = make_auth(Some(1000), None);
229
230 let hash1 = auth.signature_hash();
232 let hash2 = auth.signature_hash();
233 assert_eq!(hash1, hash2, "signature_hash should be deterministic");
234 assert_ne!(hash1, B256::ZERO);
235
236 let auth2 = make_auth(Some(2000), None);
238 assert_ne!(auth.signature_hash(), auth2.signature_hash());
239
240 let signature = sign_hash(&signing_key, &auth.signature_hash());
242 let inner_sig = match signature {
243 TempoSignature::Primitive(p) => p,
244 _ => panic!("Expected primitive signature"),
245 };
246 let signed = auth.clone().into_signed(inner_sig);
247
248 let recovered = signed.recover_signer();
250 assert!(recovered.is_ok());
251 assert_eq!(recovered.unwrap(), expected_address);
252
253 let wrong_sig = sign_hash(&signing_key, &B256::random());
255 let wrong_inner = match wrong_sig {
256 TempoSignature::Primitive(p) => p,
257 _ => panic!("Expected primitive signature"),
258 };
259 let bad_signed = auth.into_signed(wrong_inner);
260 let bad_recovered = bad_signed.recover_signer();
261 assert!(bad_recovered.is_ok());
262 assert_ne!(bad_recovered.unwrap(), expected_address);
263 }
264
265 #[test]
266 fn test_spending_expiry_and_size() {
267 assert!(make_auth(None, None).has_unlimited_spending());
269 assert!(!make_auth(None, Some(vec![])).has_unlimited_spending());
270 assert!(
271 !make_auth(
272 None,
273 Some(vec![TokenLimit {
274 token: Address::ZERO,
275 limit: U256::from(100),
276 }])
277 )
278 .has_unlimited_spending()
279 );
280
281 assert!(make_auth(None, None).never_expires());
283 assert!(!make_auth(Some(1000), None).never_expires());
284 assert!(!make_auth(Some(0), None).never_expires()); }
286
287 fn make_auth_with_chain_id(chain_id: u64) -> KeyAuthorization {
288 KeyAuthorization {
289 chain_id,
290 key_type: SignatureType::Secp256k1,
291 key_id: Address::random(),
292 expiry: None,
293 limits: None,
294 }
295 }
296
297 #[test]
298 fn test_validate_chain_id_pre_t1c() {
299 let expected = 42431;
300
301 assert!(
303 make_auth_with_chain_id(expected)
304 .validate_chain_id(expected, false)
305 .is_ok()
306 );
307
308 assert!(
310 make_auth_with_chain_id(0)
311 .validate_chain_id(expected, false)
312 .is_ok()
313 );
314
315 let err = make_auth_with_chain_id(999)
317 .validate_chain_id(expected, false)
318 .unwrap_err();
319 assert_eq!(err.expected, expected);
320 assert_eq!(err.got, 999);
321 }
322
323 #[test]
324 fn test_validate_chain_id_post_t1c() {
325 let expected = 42431;
326
327 assert!(
329 make_auth_with_chain_id(expected)
330 .validate_chain_id(expected, true)
331 .is_ok()
332 );
333
334 let err = make_auth_with_chain_id(0)
336 .validate_chain_id(expected, true)
337 .unwrap_err();
338 assert_eq!(err.expected, expected);
339 assert_eq!(err.got, 0);
340
341 let err = make_auth_with_chain_id(999)
343 .validate_chain_id(expected, true)
344 .unwrap_err();
345 assert_eq!(err.expected, expected);
346 assert_eq!(err.got, 999);
347 }
348}