/home/runner/work/lyquor/lyquor/crypto/src/ed25519.rs
Line | Count | Source |
1 | | use crate::EcdsaCipher; |
2 | | use ed25519_dalek::{Signer, SigningKey}; |
3 | | use lyquor_primitives::alloy_primitives::{self, U256, uint}; |
4 | | |
5 | | // The prime modulus: 2^255 - 19 ("p_0" in ed25519.sol) |
6 | | pub const P: U256 = uint!(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed_U256); |
7 | | // Edwards 'd' coefficient: -121665 * inv(121666) mod P ("d_0" in ed25519.sol) |
8 | | pub const D_0: U256 = uint!(0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3_U256); |
9 | | // SCL Mapping Constant 'delta' = A_mont / 3 ("delta" in ed25519.sol) |
10 | | pub const DELTA: U256 = uint!(0x2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad2451_U256); |
11 | | // SCL Mapping Constant 'c' ("c" in ed25519.sol) |
12 | | pub const C_VAL: U256 = uint!(0x70d9120b9f5ff9442d84f723fc03b0813a5e2c2eb482e57d3391fb5500ba81e7_U256); |
13 | | // Target Curve Coefficient 'a' = 1 - A^2/3 ("a_0" in ed25519.sol) |
14 | | pub const WEI_A: U256 = uint!(19298681539552699237261830834781317975544997444273427339909597334573241639236_U256); |
15 | | // Target Curve Coefficient 'b' = (2A^3 - 9A)/27 ("b_0" in ed25519.sol) |
16 | | pub const WEI_B: U256 = uint!(55751746669818908907645289078257140818241103727901012315294400837956729358436_U256); |
17 | | pub const SET_ED25519_SECP256K1_BINDING_PREFIX: &[u8] = b"lyquor_set_ed25519_secp256k1_binding\0"; |
18 | | |
19 | | /// Convert from an ed25519 public key from the Rust library to SCL's format (see |
20 | | /// src/eth/lib/ed25519.sol of lyquor-seq crate). |
21 | | #[derive(Debug, Clone)] |
22 | | pub struct SCLPubkey { |
23 | | pub qx: U256, |
24 | | pub qy: U256, |
25 | | pub compressed: U256, |
26 | | } |
27 | | |
28 | | impl SCLPubkey { |
29 | | /// Converts Ed25519 bytes -> SCL Weierstrass Inputs |
30 | 2 | pub fn new(key: [u8; 32]) -> SCLPubkey { |
31 | 2 | let (ed_x, ed_y) = recover_edwards_x(key); |
32 | 2 | let (qx, qy) = map_to_weierstrass(ed_x, ed_y); |
33 | 2 | let compressed = U256::from_be_bytes(key); |
34 | | |
35 | 2 | SCLPubkey { qx, qy, compressed } |
36 | 2 | } |
37 | | |
38 | 0 | pub fn to_ed25519(&self) -> [U256; 5] { |
39 | 0 | [ |
40 | 0 | self.qx, // Index 0: Mapped Weirstrass X |
41 | 0 | self.qy, // Index 1: Mapped Weirstrass Y |
42 | 0 | U256::ZERO, // Index 2: Unused/Padding |
43 | 0 | U256::ZERO, // Index 3: Unused/Padding |
44 | 0 | self.compressed, // Index 4: Original Ed25519 Compressed Y (for hash validation) |
45 | 0 | ] |
46 | 0 | } |
47 | | } |
48 | | |
49 | | /// Decompresses an Ed25519 public key (recovers `x` from `y`). |
50 | 2 | fn recover_edwards_x(mut y_bytes: [u8; 32]) -> (U256, U256) { |
51 | 2 | let p = P; |
52 | | |
53 | | // Parse Y and Sign Bit (Little Endian) |
54 | 2 | let sign_bit = (y_bytes[31] & 0x80) >> 7; |
55 | 2 | y_bytes[31] &= 0x7F; |
56 | 2 | let y = U256::from_le_bytes(y_bytes); |
57 | | |
58 | | // u = y^2 - 1 |
59 | 2 | let y_sq = y.mul_mod(y, p); |
60 | 2 | let one = U256::from(1); |
61 | 2 | let u = if y_sq >= one { y_sq - one } else { p + y_sq - one0 }; |
62 | | |
63 | | // v = d*y^2 + 1 |
64 | 2 | let d_times_y_sq = D_0.mul_mod(y_sq, p); |
65 | 2 | let v = d_times_y_sq.add_mod(one, p); |
66 | | |
67 | | // x^2 = u * v^-1 |
68 | 2 | let v_inv = v.inv_mod(p).expect("Invalid Key: v is zero"); |
69 | 2 | let x_sq = u.mul_mod(v_inv, p); |
70 | | |
71 | | // sqrt(x^2) using p = 5 mod 8 |
72 | 2 | let three = U256::from(3); |
73 | 2 | let eight = U256::from(8); |
74 | 2 | let exp = p.wrapping_add(three).checked_div(eight).unwrap(); |
75 | 2 | let mut x = x_sq.pow_mod(exp, p); |
76 | | |
77 | | // Check root, multiply by sqrt(-1) if needed |
78 | 2 | if x.mul_mod(x, p) != x_sq { |
79 | 2 | let four = U256::from(4); |
80 | 2 | let exp_i = p.wrapping_sub(one).checked_div(four).unwrap(); |
81 | 2 | let i = U256::from(2).pow_mod(exp_i, p); |
82 | 2 | x = x.mul_mod(i, p); |
83 | 2 | }0 |
84 | | |
85 | | // Adjust sign |
86 | 2 | let is_odd = x.bit(0); |
87 | 2 | let sign_is_set = sign_bit == 1; |
88 | 2 | if is_odd != sign_is_set { |
89 | 2 | x = p.wrapping_sub(x); |
90 | 2 | }0 |
91 | | |
92 | 2 | (x, y) |
93 | 2 | } |
94 | | |
95 | | /// Maps an Ed25519 point $(x_{ed}, y_{ed})$ to a Short Weierstrass point $(X_w, Y_w)$. |
96 | 2 | fn map_to_weierstrass(ed_x: U256, ed_y: U256) -> (U256, U256) { |
97 | 2 | let p = P; |
98 | 2 | let one = U256::from(1); |
99 | | |
100 | | // (1 + y) |
101 | 2 | let one_plus_y = one.add_mod(ed_y, p); |
102 | | |
103 | | // (1 - y)^-1 |
104 | 2 | let one_minus_y = if one >= ed_y { |
105 | 0 | one - ed_y |
106 | | } else { |
107 | 2 | p.wrapping_add(one).wrapping_sub(ed_y) |
108 | | }; |
109 | 2 | let one_minus_y_inv = one_minus_y.inv_mod(p).expect("Point at infinity?"); |
110 | | |
111 | | // X_w = delta + (1 + y)/(1 - y) |
112 | 2 | let term_x = one_plus_y.mul_mod(one_minus_y_inv, p); |
113 | 2 | let x_w = DELTA.add_mod(term_x, p); |
114 | | |
115 | | // Y_w = C * (1 + y) / ((1 - y) * x) |
116 | 2 | let num_y = C_VAL.mul_mod(one_plus_y, p); |
117 | 2 | let den_base = one_minus_y.mul_mod(ed_x, p); |
118 | 2 | let den_y = den_base.inv_mod(p).expect("Divide by zero in map"); |
119 | 2 | let y_w = num_y.mul_mod(den_y, p); |
120 | | |
121 | 2 | (x_w, y_w) |
122 | 2 | } |
123 | | |
124 | 2 | pub fn set_ed25519_secp256k1_binding_message( |
125 | 2 | addr: alloy_primitives::Address, pubkey: alloy_primitives::B256, |
126 | 2 | ) -> Vec<u8> { |
127 | 2 | let mut msg = Vec::new(); |
128 | 2 | msg.extend_from_slice(SET_ED25519_SECP256K1_BINDING_PREFIX); |
129 | 2 | msg.extend_from_slice(addr.as_slice()); |
130 | 2 | msg.extend_from_slice(pubkey.as_ref()); |
131 | 2 | msg |
132 | 2 | } |
133 | | |
134 | 1 | pub fn set_ed25519_address_args(kp: &SigningKey) -> Vec<String> { |
135 | 1 | let scl_pub = SCLPubkey::new(kp.verifying_key().to_bytes()); |
136 | 1 | let ecdsa_cipher = EcdsaCipher::new(kp); |
137 | 1 | let addr = ecdsa_cipher.address(); |
138 | 1 | let pubkey_bytes = alloy_primitives::B256::from(scl_pub.compressed.to_be_bytes::<32>()); |
139 | 1 | let msg = set_ed25519_secp256k1_binding_message(addr, pubkey_bytes); |
140 | | |
141 | 1 | let sig = kp.sign(&msg); |
142 | 1 | let sig_bytes = sig.to_bytes(); |
143 | 1 | let ec_sig = crate::SignatureCipher::sign(&ecdsa_cipher, &msg).expect("ecdsa signature"); |
144 | | |
145 | 1 | let r = alloy_primitives::U256::from_be_bytes::<32>(sig_bytes[0..32].try_into().unwrap()); |
146 | 1 | let s = alloy_primitives::U256::from_be_bytes::<32>(sig_bytes[32..64].try_into().unwrap()); |
147 | 1 | let pubkey = alloy_primitives::B256::from(scl_pub.compressed.to_be_bytes::<32>()); |
148 | | |
149 | 1 | vec![ |
150 | 1 | addr.to_string(), |
151 | 1 | format!("0x{:x}", pubkey), |
152 | 1 | format!("0x{:x}", scl_pub.qx), |
153 | 1 | format!("0x{:x}", scl_pub.qy), |
154 | 1 | format!("0x{:x}", r), |
155 | 1 | format!("0x{:x}", s), |
156 | 1 | format!("0x{}", lyquor_primitives::hex::encode(ec_sig)), |
157 | | ] |
158 | 1 | } |
159 | | |
160 | | #[cfg(test)] |
161 | | mod tests { |
162 | | use super::*; |
163 | | use core::str::FromStr; |
164 | | use ed25519_dalek::Signature; |
165 | | use lyquor_primitives::alloy_primitives::hex; |
166 | | use lyquor_test::test; |
167 | | |
168 | | #[test] |
169 | | fn test_rfc8032_example() { |
170 | | // RFC 8032 Test 1: https://datatracker.ietf.org/doc/html/rfc8032#section-7.1 |
171 | | let pubkey_hex = hex::decode("0xd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a").unwrap(); |
172 | | let mut bytes = [0u8; 32]; |
173 | | bytes.copy_from_slice(&pubkey_hex); |
174 | | let result = SCLPubkey::new(bytes); |
175 | | |
176 | | // GOLDEN VALUES (tested in sequencer/src/eth/ed25519_test.sol) |
177 | | let qx = uint!(0x5961a13b250782afee75256fdba2e6bec4d810f89f6ce1c033585acd96b48329_U256); |
178 | | let qy = uint!(0x53373f33d468fee07fb2e53496849c8e52b3db37af7729999b2c3d372aca8de5_U256); |
179 | | |
180 | | assert_eq!(result.qx, qx, "X-coordinate mismatch"); |
181 | | assert_eq!(result.qy, qy, "Y-coordinate mismatch"); |
182 | | } |
183 | | |
184 | | #[test] |
185 | | fn test_set_ed25519_address_args() { |
186 | | let kp = SigningKey::from_bytes(&[7u8; 32]); |
187 | | let args = set_ed25519_address_args(&kp); |
188 | | |
189 | | let addr: alloy_primitives::Address = args[0].parse().unwrap(); |
190 | | let pubkey_bytes = alloy_primitives::B256::from_str(&args[1]).unwrap(); |
191 | | let r = alloy_primitives::U256::from_str(&args[4]).unwrap(); |
192 | | let s = alloy_primitives::U256::from_str(&args[5]).unwrap(); |
193 | | let ec_sig = hex::decode(args[6].trim_start_matches("0x")).unwrap(); |
194 | | let msg = set_ed25519_secp256k1_binding_message(addr, pubkey_bytes); |
195 | | |
196 | | let mut sig_bytes = [0u8; 64]; |
197 | | sig_bytes[..32].copy_from_slice(&r.to_be_bytes::<32>()); |
198 | | sig_bytes[32..].copy_from_slice(&s.to_be_bytes::<32>()); |
199 | | let ed_sig = Signature::from_slice(&sig_bytes).unwrap(); |
200 | | |
201 | | kp.verifying_key().verify_strict(&msg, &ed_sig).unwrap(); |
202 | | assert!(crate::SignatureCipher::verify( |
203 | | &EcdsaCipher::new(&kp), |
204 | | &msg, |
205 | | &ec_sig, |
206 | | addr.as_slice() |
207 | | )); |
208 | | } |
209 | | } |