Skip to main content

eth_valkyoth_verify/
set_code_authorization.rs

1use core::fmt;
2
3use eth_valkyoth_hash::Keccak256;
4use eth_valkyoth_primitives::Address;
5use eth_valkyoth_protocol::SetCodeAuthorization;
6
7#[cfg(feature = "secp256k1-k256")]
8use crate::K256Secp256k1Backend;
9use crate::{
10    EthereumSignature, RecoverableSecp256k1, SetCodeAuthorizationSigningHash,
11    TransactionSigningHashError, recover_sender_from_digest_with_backend,
12    set_code_authorization_signing_hash,
13};
14
15/// A decoded EIP-7702 authorization tuple signature that recovered correctly.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub struct ValidatedSetCodeAuthorization {
18    authority: Address,
19    signing_hash: SetCodeAuthorizationSigningHash,
20}
21
22impl ValidatedSetCodeAuthorization {
23    /// Creates a validated authorization result from checked components.
24    #[must_use]
25    pub(crate) const fn new(
26        authority: Address,
27        signing_hash: SetCodeAuthorizationSigningHash,
28    ) -> Self {
29        Self {
30            authority,
31            signing_hash,
32        }
33    }
34
35    /// Returns the recovered authorizing account.
36    #[must_use]
37    pub const fn authority(self) -> Address {
38        self.authority
39    }
40
41    /// Returns the authorization signing hash that was recovered against.
42    #[must_use]
43    pub const fn signing_hash(self) -> SetCodeAuthorizationSigningHash {
44        self.signing_hash
45    }
46}
47
48/// EIP-7702 authorization tuple signature validation failure.
49#[non_exhaustive]
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum SetCodeAuthorizationValidationError {
52    /// Authorization signing-hash construction failed before recovery.
53    SigningHash(TransactionSigningHashError),
54    /// Signature scalar, y-parity, or public-key recovery failed.
55    InvalidSignature,
56    /// The recovered authority does not match the expected account.
57    WrongAuthority,
58}
59
60impl SetCodeAuthorizationValidationError {
61    /// Stable machine-readable error code.
62    #[must_use]
63    pub const fn code(self) -> &'static str {
64        match self {
65            Self::SigningHash(error) => error.code(),
66            Self::InvalidSignature => "ETH_SET_CODE_AUTHORIZATION_SIGNATURE_INVALID",
67            Self::WrongAuthority => "ETH_SET_CODE_AUTHORIZATION_WRONG_AUTHORITY",
68        }
69    }
70
71    /// Stable human-readable error message.
72    #[must_use]
73    pub const fn message(self) -> &'static str {
74        match self {
75            Self::SigningHash(_) => "set-code authorization signing hash construction failed",
76            Self::InvalidSignature => "set-code authorization signature is not accepted",
77            Self::WrongAuthority => {
78                "set-code authorization signature recovered a different authority"
79            }
80        }
81    }
82
83    /// Stable high-level category for policy decisions.
84    #[must_use]
85    pub const fn category(self) -> SetCodeAuthorizationValidationErrorCategory {
86        match self {
87            Self::SigningHash(_) => SetCodeAuthorizationValidationErrorCategory::SigningHash,
88            Self::InvalidSignature | Self::WrongAuthority => {
89                SetCodeAuthorizationValidationErrorCategory::Signature
90            }
91        }
92    }
93}
94
95impl fmt::Display for SetCodeAuthorizationValidationError {
96    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
97        formatter.write_str(self.message())
98    }
99}
100
101#[cfg(feature = "std")]
102impl std::error::Error for SetCodeAuthorizationValidationError {}
103
104/// Stable high-level set-code authorization validation categories.
105#[non_exhaustive]
106#[derive(Clone, Copy, Debug, Eq, PartialEq)]
107pub enum SetCodeAuthorizationValidationErrorCategory {
108    /// Signing preimage or hash construction failure.
109    SigningHash,
110    /// Signature representation, recovery, or authority-match failure.
111    Signature,
112}
113
114/// Validates a decoded EIP-7702 authorization tuple signature.
115///
116/// This validates only the tuple signature domain
117/// `keccak256(0x05 || rlp([chain_id, address, nonce]))`. Chain-ID policy,
118/// nonce/account-state checks, delegation indicator checks, and empty-list
119/// rejection are the v0.24.2 transaction-validity gate.
120#[cfg(feature = "secp256k1-k256")]
121pub fn validate_set_code_authorization_signature<H1, H2>(
122    authorization: SetCodeAuthorization,
123    expected_authority: Option<Address>,
124    scratch: &mut [u8],
125    signing_hasher: H1,
126    address_hasher: H2,
127) -> Result<ValidatedSetCodeAuthorization, SetCodeAuthorizationValidationError>
128where
129    H1: Keccak256,
130    H2: Keccak256,
131{
132    validate_set_code_authorization_signature_with_backend(
133        authorization,
134        expected_authority,
135        scratch,
136        signing_hasher,
137        K256Secp256k1Backend,
138        address_hasher,
139    )
140}
141
142/// Validates a decoded EIP-7702 authorization tuple signature through a backend.
143pub fn validate_set_code_authorization_signature_with_backend<B, H1, H2>(
144    authorization: SetCodeAuthorization,
145    expected_authority: Option<Address>,
146    scratch: &mut [u8],
147    signing_hasher: H1,
148    secp256k1_backend: B,
149    address_hasher: H2,
150) -> Result<ValidatedSetCodeAuthorization, SetCodeAuthorizationValidationError>
151where
152    B: RecoverableSecp256k1,
153    H1: Keccak256,
154    H2: Keccak256,
155{
156    let signing_hash = set_code_authorization_signing_hash(authorization, scratch, signing_hasher)
157        .map_err(SetCodeAuthorizationValidationError::SigningHash)?;
158    let signature =
159        EthereumSignature::from_parts(authorization.r, authorization.s, authorization.y_parity);
160    let authority = recover_sender_from_digest_with_backend(
161        signing_hash.to_b256(),
162        signature,
163        secp256k1_backend,
164        address_hasher,
165    )
166    .map_err(|_| SetCodeAuthorizationValidationError::InvalidSignature)?;
167    if let Some(expected) = expected_authority
168        && authority != expected
169    {
170        return Err(SetCodeAuthorizationValidationError::WrongAuthority);
171    }
172    Ok(ValidatedSetCodeAuthorization::new(authority, signing_hash))
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::test_crypto::{RealKeccak, TestSecp256k1Backend};
179    use eth_valkyoth_primitives::Nonce;
180    use eth_valkyoth_protocol::{
181        SET_CODE_AUTHORIZATION_MAGIC, SetCodeAuthorizationChainId, SignatureYParity,
182        encode_set_code_authorization_signing_preimage,
183    };
184    use k256::ecdsa::SigningKey;
185
186    fn signing_key() -> Result<SigningKey, SetCodeAuthorizationValidationError> {
187        SigningKey::from_bytes((&fixture_bytes(b"set-code authorization test key")).into())
188            .map_err(|_| SetCodeAuthorizationValidationError::InvalidSignature)
189    }
190
191    fn unsigned_authorization() -> SetCodeAuthorization {
192        let mut chain_id = [0_u8; 32];
193        if let Some(last) = chain_id.last_mut() {
194            *last = u8::try_from("chain-one".len()).unwrap_or_default();
195        }
196        SetCodeAuthorization {
197            chain_id: SetCodeAuthorizationChainId::from_be_bytes(chain_id),
198            address: Address::from_bytes(address_bytes(b"delegate")),
199            nonce: Nonce::new(u64::try_from("nonce".len()).unwrap_or_default()),
200            y_parity: SignatureYParity::Even,
201            r: [0_u8; 32],
202            s: [0_u8; 32],
203        }
204    }
205
206    fn signed_authorization() -> Result<SetCodeAuthorization, SetCodeAuthorizationValidationError> {
207        let authorization = unsigned_authorization();
208        let mut scratch = [0_u8; 128];
209        let signing_hash =
210            set_code_authorization_signing_hash(authorization, &mut scratch, RealKeccak::default())
211                .map_err(SetCodeAuthorizationValidationError::SigningHash)?;
212        let key = signing_key()?;
213        let (signature, recovery_id) = key
214            .sign_prehash_recoverable(&<[u8; 32]>::from(signing_hash.to_b256()))
215            .map_err(|_| SetCodeAuthorizationValidationError::InvalidSignature)?;
216        let bytes = signature.to_bytes();
217        let r = bytes
218            .get(..32)
219            .and_then(|value| <[u8; 32]>::try_from(value).ok())
220            .ok_or(SetCodeAuthorizationValidationError::InvalidSignature)?;
221        let s = bytes
222            .get(32..)
223            .and_then(|value| <[u8; 32]>::try_from(value).ok())
224            .ok_or(SetCodeAuthorizationValidationError::InvalidSignature)?;
225        let y_parity = SignatureYParity::try_new(u64::from(recovery_id.to_byte()))
226            .map_err(|_| SetCodeAuthorizationValidationError::InvalidSignature)?;
227        Ok(SetCodeAuthorization {
228            y_parity,
229            r,
230            s,
231            ..authorization
232        })
233    }
234
235    fn expected_authority() -> Result<Address, SetCodeAuthorizationValidationError> {
236        let key = signing_key()?;
237        let encoded = key.verifying_key().to_encoded_point(false);
238        let public_key = encoded
239            .as_bytes()
240            .get(1..)
241            .ok_or(SetCodeAuthorizationValidationError::InvalidSignature)?;
242        let digest = eth_valkyoth_hash::hash_one(RealKeccak::default(), public_key);
243        let bytes = <[u8; 32]>::from(digest);
244        let address = bytes
245            .get(12..)
246            .and_then(|value| <[u8; 20]>::try_from(value).ok())
247            .ok_or(SetCodeAuthorizationValidationError::InvalidSignature)?;
248        Ok(Address::from_bytes(address))
249    }
250
251    fn fixture_bytes(label: &[u8]) -> [u8; 32] {
252        let mut bytes = [0_u8; 32];
253        fill_fixture_bytes(&mut bytes, label);
254        bytes
255    }
256
257    fn address_bytes(label: &[u8]) -> [u8; 20] {
258        let mut bytes = [0_u8; 20];
259        fill_fixture_bytes(&mut bytes, label);
260        bytes
261    }
262
263    fn fill_fixture_bytes(bytes: &mut [u8], label: &[u8]) {
264        if label.is_empty() {
265            return;
266        }
267        for (index, byte) in bytes.iter_mut().enumerate() {
268            let Some(label_index) = index.checked_rem(label.len()) else {
269                return;
270            };
271            let label_byte = label.get(label_index).copied().unwrap_or_default();
272            *byte = label_byte.wrapping_add(u8::try_from(index).unwrap_or_default());
273        }
274    }
275
276    #[test]
277    fn validates_authorization_signature_in_authorization_domain() {
278        let authorization = signed_authorization();
279        let expected = expected_authority();
280        assert!(authorization.is_ok());
281        assert!(expected.is_ok());
282
283        if let (Ok(authorization), Ok(expected)) = (authorization, expected) {
284            let mut scratch = [0_u8; 128];
285            let result = validate_set_code_authorization_signature_with_backend(
286                authorization,
287                Some(expected),
288                &mut scratch,
289                RealKeccak::default(),
290                TestSecp256k1Backend,
291                RealKeccak::default(),
292            );
293            assert_eq!(
294                result.map(ValidatedSetCodeAuthorization::authority),
295                Ok(expected)
296            );
297        }
298    }
299
300    #[test]
301    fn authorization_preimage_uses_magic_domain() {
302        let authorization = unsigned_authorization();
303        let mut output = [0_u8; 128];
304        let written = encode_set_code_authorization_signing_preimage(authorization, &mut output);
305        assert!(written.is_ok());
306        if let Ok(written) = written {
307            assert_eq!(output.first(), Some(&SET_CODE_AUTHORIZATION_MAGIC));
308            assert_ne!(output.get(1..written), Some(&[][..]));
309        }
310    }
311
312    #[test]
313    fn rejects_wrong_authority_and_short_scratch() {
314        let authorization = signed_authorization();
315        assert!(authorization.is_ok());
316        if let Ok(authorization) = authorization {
317            let mut scratch = [0_u8; 128];
318            assert_eq!(
319                validate_set_code_authorization_signature_with_backend(
320                    authorization,
321                    Some(Address::from_bytes(address_bytes(b"wrong"))),
322                    &mut scratch,
323                    RealKeccak::default(),
324                    TestSecp256k1Backend,
325                    RealKeccak::default(),
326                ),
327                Err(SetCodeAuthorizationValidationError::WrongAuthority)
328            );
329
330            let mut short = [0_u8; 8];
331            assert!(matches!(
332                validate_set_code_authorization_signature_with_backend(
333                    authorization,
334                    None,
335                    &mut short,
336                    RealKeccak::default(),
337                    TestSecp256k1Backend,
338                    RealKeccak::default(),
339                ),
340                Err(SetCodeAuthorizationValidationError::SigningHash(_))
341            ));
342        }
343    }
344}