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