referrerpolicy=no-referrer-when-downgrade

pallet_revive/evm/api/
signature.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17//! Ethereum signature utilities
18
19use super::*;
20use sp_core::{H160, U256};
21use sp_io::{crypto::secp256k1_ecdsa_recover, hashing::keccak_256};
22
23/// Recover an Ethereum address from a message hash and signature.
24///
25/// Rejects high-S signatures: EIP-2 for transactions, and EIP-7702 requires the same
26/// `s <= secp256k1n/2` bound on authorization tuples, so the check applies to every caller.
27///
28/// # Parameters
29/// - `message`: The message bytes to hash
30/// - `signature`: The 65-byte ECDSA signature (r, s, v)
31///
32/// # Returns
33/// The recovered Ethereum address, or an error if recovery fails
34pub fn recover_eth_address_from_message(message: &[u8], signature: &[u8; 65]) -> Result<H160, ()> {
35	if !sp_core::ecdsa::is_signature_normalized(signature) {
36		log::debug!(target: "evm", "Rejected high-S ECDSA signature (EIP-2 violation)");
37		return Err(());
38	}
39
40	let hash = keccak_256(message);
41	let pk = secp256k1_ecdsa_recover(signature, &hash).map_err(|_| ())?;
42	let mut addr = H160::default();
43	addr.assign_from_slice(&keccak_256(&pk[..])[12..]);
44	Ok(addr)
45}
46
47impl TransactionLegacySigned {
48	/// Get the recovery ID from the signed transaction.
49	/// See https://eips.ethereum.org/EIPS/eip-155
50	fn extract_recovery_id(&self) -> Option<u8> {
51		if let Some(chain_id) = self.transaction_legacy_unsigned.chain_id {
52			// self.v - chain_id * 2 - 35
53			let v: u64 = self.v.try_into().ok()?;
54			let chain_id: u64 = chain_id.try_into().ok()?;
55			let r = v.checked_sub(chain_id.checked_mul(2)?)?.checked_sub(35)?;
56			r.try_into().ok()
57		} else {
58			self.v.try_into().ok()
59		}
60	}
61}
62
63impl TransactionUnsigned {
64	/// Extract the unsigned transaction from a signed transaction.
65	pub fn from_signed(tx: TransactionSigned) -> Self {
66		match tx {
67			TransactionSigned::TransactionLegacySigned(signed) => {
68				Self::TransactionLegacyUnsigned(signed.transaction_legacy_unsigned)
69			},
70			TransactionSigned::Transaction7702Signed(signed) => {
71				Self::Transaction7702Unsigned(signed.transaction_7702_unsigned)
72			},
73			TransactionSigned::Transaction4844Signed(signed) => {
74				Self::Transaction4844Unsigned(signed.transaction_4844_unsigned)
75			},
76			TransactionSigned::Transaction1559Signed(signed) => {
77				Self::Transaction1559Unsigned(signed.transaction_1559_unsigned)
78			},
79			TransactionSigned::Transaction2930Signed(signed) => {
80				Self::Transaction2930Unsigned(signed.transaction_2930_unsigned)
81			},
82		}
83	}
84
85	/// Create a signed transaction from an [`TransactionUnsigned`] and a signature.
86	pub fn with_signature(self, signature: [u8; 65]) -> TransactionSigned {
87		let r = U256::from_big_endian(&signature[..32]);
88		let s = U256::from_big_endian(&signature[32..64]);
89		let recovery_id = signature[64];
90
91		match self {
92			TransactionUnsigned::Transaction7702Unsigned(transaction_7702_unsigned) => {
93				Transaction7702Signed {
94					transaction_7702_unsigned,
95					r,
96					s,
97					v: None,
98					y_parity: U256::from(recovery_id),
99				}
100				.into()
101			},
102			TransactionUnsigned::Transaction2930Unsigned(transaction_2930_unsigned) => {
103				Transaction2930Signed {
104					transaction_2930_unsigned,
105					r,
106					s,
107					v: None,
108					y_parity: U256::from(recovery_id),
109				}
110				.into()
111			},
112			TransactionUnsigned::Transaction1559Unsigned(transaction_1559_unsigned) => {
113				Transaction1559Signed {
114					transaction_1559_unsigned,
115					r,
116					s,
117					v: None,
118					y_parity: U256::from(recovery_id),
119				}
120				.into()
121			},
122
123			TransactionUnsigned::Transaction4844Unsigned(transaction_4844_unsigned) => {
124				Transaction4844Signed {
125					transaction_4844_unsigned,
126					r,
127					s,
128					y_parity: U256::from(recovery_id),
129				}
130				.into()
131			},
132
133			TransactionUnsigned::TransactionLegacyUnsigned(transaction_legacy_unsigned) => {
134				let v = transaction_legacy_unsigned
135					.chain_id
136					.map(|chain_id| {
137						chain_id
138							.saturating_mul(U256::from(2))
139							.saturating_add(U256::from(35u32 + recovery_id as u32))
140					})
141					.unwrap_or_else(|| U256::from(27u32 + recovery_id as u32));
142
143				TransactionLegacySigned { transaction_legacy_unsigned, r, s, v }.into()
144			},
145		}
146	}
147}
148
149impl TransactionSigned {
150	/// Get the raw 65 bytes signature from the signed transaction.
151	pub fn raw_signature(&self) -> Result<[u8; 65], ()> {
152		use TransactionSigned::*;
153		let (r, s, v) = match self {
154			TransactionLegacySigned(tx) => (tx.r, tx.s, tx.extract_recovery_id().ok_or(())?),
155			Transaction7702Signed(tx) => (tx.r, tx.s, tx.y_parity.try_into().map_err(|_| ())?),
156			Transaction4844Signed(tx) => (tx.r, tx.s, tx.y_parity.try_into().map_err(|_| ())?),
157			Transaction1559Signed(tx) => (tx.r, tx.s, tx.y_parity.try_into().map_err(|_| ())?),
158			Transaction2930Signed(tx) => (tx.r, tx.s, tx.y_parity.try_into().map_err(|_| ())?),
159		};
160		let mut sig = [0u8; 65];
161		r.write_as_big_endian(sig[0..32].as_mut());
162		s.write_as_big_endian(sig[32..64].as_mut());
163		sig[64] = v;
164		Ok(sig)
165	}
166
167	/// Recover the Ethereum address, from a signed transaction.
168	pub fn recover_eth_address(&self) -> Result<H160, ()> {
169		use TransactionSigned::*;
170
171		let mut s = rlp::RlpStream::new();
172		match self {
173			TransactionLegacySigned(tx) => {
174				let tx = &tx.transaction_legacy_unsigned;
175				s.append(tx);
176			},
177			Transaction7702Signed(tx) => {
178				let tx = &tx.transaction_7702_unsigned;
179				s.append(&tx.r#type.value());
180				s.append(tx);
181			},
182			Transaction4844Signed(tx) => {
183				let tx = &tx.transaction_4844_unsigned;
184				s.append(&tx.r#type.value());
185				s.append(tx);
186			},
187			Transaction1559Signed(tx) => {
188				let tx = &tx.transaction_1559_unsigned;
189				s.append(&tx.r#type.value());
190				s.append(tx);
191			},
192			Transaction2930Signed(tx) => {
193				let tx = &tx.transaction_2930_unsigned;
194				s.append(&tx.r#type.value());
195				s.append(tx);
196			},
197		}
198		let bytes = s.out().to_vec();
199		let signature = self.raw_signature()?;
200
201		recover_eth_address_from_message(&bytes, &signature)
202	}
203}
204
205#[cfg(test)]
206mod tests {
207	use super::*;
208	use crate::evm::TransactionUnsigned;
209
210	#[test]
211	fn sign_and_recover_work() {
212		let txs = [
213			// Legacy
214			"f86080808301e24194095e7baea6a6c7c4c2dfeb977efac326af552d87808026a07b2e762a17a71a46b422e60890a04512cf0d907ccf6b78b5bd6e6977efdc2bf5a01ea673d50bbe7c2236acb498ceb8346a8607c941f0b8cbcde7cf439aa9369f1f",
215			//// type 1: EIP2930
216			"01f89b0180808301e24194095e7baea6a6c7c4c2dfeb977efac326af552d878080f838f7940000000000000000000000000000000000000001e1a0000000000000000000000000000000000000000000000000000000000000000080a0c45a61b3d1d00169c649e7326e02857b850efb96e587db4b9aad29afc80d0752a070ae1eb47ab4097dbed2f19172ae286492621b46ac737ee6c32fb18a00c94c9c",
217			// type 2: EIP1559
218			"02f89c018080018301e24194095e7baea6a6c7c4c2dfeb977efac326af552d878080f838f7940000000000000000000000000000000000000001e1a0000000000000000000000000000000000000000000000000000000000000000080a055d72bbc3047d4b9d3e4b8099f187143202407746118204cc2e0cb0c85a68baea04f6ef08a1418c70450f53398d9f0f2d78d9e9d6b8a80cba886b67132c4a744f2",
219			// type 3: EIP4844
220			"03f8bf018002018301e24194095e7baea6a6c7c4c2dfeb977efac326af552d878080f838f7940000000000000000000000000000000000000001e1a0000000000000000000000000000000000000000000000000000000000000000080e1a0000000000000000000000000000000000000000000000000000000000000000001a0672b8bac466e2cf1be3148c030988d40d582763ecebbc07700dfc93bb070d8a4a07c635887005b11cb58964c04669ac2857fa633aa66f662685dadfd8bcacb0f21",
221		];
222		let account = Account::from_secret_key(hex_literal::hex!(
223			"a872f6cbd25a0e04a08b1e21098017a9e6194d101d75e13111f71410c59cd57f"
224		));
225
226		for tx in txs {
227			let raw_tx = alloy_core::hex::decode(tx).unwrap();
228			let tx = TransactionSigned::decode(&raw_tx).unwrap();
229
230			let address = tx.recover_eth_address();
231			assert_eq!(address.unwrap(), account.address());
232
233			let unsigned = TransactionUnsigned::from_signed(tx.clone());
234			let signed = account.sign_transaction(unsigned);
235			assert_eq!(tx, signed);
236		}
237	}
238
239	#[test]
240	fn high_s_signature_is_rejected() {
241		let order: [u8; 32] = [
242			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
243			0xff, 0xfe, 0xba, 0xae, 0xdc, 0xe6, 0xaf, 0x48, 0xa0, 0x3b, 0xbf, 0xd2, 0x5e, 0x8c,
244			0xd0, 0x36, 0x41, 0x41,
245		];
246
247		let raw_tx = alloy_core::hex::decode(
248			"f86080808301e24194095e7baea6a6c7c4c2dfeb977efac326af552d87808026a07b2e762a17a71a46b422e60890a04512cf0d907ccf6b78b5bd6e6977efdc2bf5a01ea673d50bbe7c2236acb498ceb8346a8607c941f0b8cbcde7cf439aa9369f1f",
249		)
250		.unwrap();
251		let tx = TransactionSigned::decode(&raw_tx).unwrap();
252
253		// The original transaction should recover successfully (low-S)
254		assert!(tx.recover_eth_address().is_ok());
255
256		let unsigned = TransactionUnsigned::from_signed(tx.clone());
257		let sig = tx.raw_signature().unwrap();
258
259		let s_bytes: [u8; 32] = sig[32..64].try_into().unwrap();
260		let mut s_prime = [0u8; 32];
261		let mut borrow = 0i16;
262		for i in (0..32).rev() {
263			let diff = order[i] as i16 - s_bytes[i] as i16 - borrow;
264			if diff < 0 {
265				s_prime[i] = (diff + 256) as u8;
266				borrow = 1;
267			} else {
268				s_prime[i] = diff as u8;
269				borrow = 0;
270			}
271		}
272
273		let mut malleable_sig = [0u8; 65];
274		malleable_sig[0..32].copy_from_slice(&sig[0..32]);
275		malleable_sig[32..64].copy_from_slice(&s_prime);
276		malleable_sig[64] = sig[64] ^ 1;
277
278		let malleable_tx = unsigned.with_signature(malleable_sig);
279
280		// Should be rejected by recover_eth_address due to high-S
281		assert!(
282			malleable_tx.recover_eth_address().is_err(),
283			"high-S signature should be rejected per EIP-2"
284		);
285	}
286}