|
| 1 | +use crate::{definition::Definition, operator::Operator, version::V1_3}; |
| 2 | +use charon_eth2::{ |
| 3 | + eip712::{ |
| 4 | + Domain, Field, PRIMITIVE_STRING, PRIMITIVE_UINT256, Primitive, Type, TypedData, Value, |
| 5 | + hash_typed_data, |
| 6 | + }, |
| 7 | + network::fork_version_to_chain_id, |
| 8 | +}; |
| 9 | +use charon_k1util::{self as k1util, K1UtilError}; |
| 10 | +use k256::SecretKey; |
| 11 | + |
| 12 | +type ValueFunc = Box<dyn Fn(&Definition, &Operator) -> Value>; |
| 13 | + |
| 14 | +type Result<T> = std::result::Result<T, EIP712Error>; |
| 15 | + |
| 16 | +/// EIP712Error is the error type for EIP-712 errors. |
| 17 | +#[derive(Debug, thiserror::Error)] |
| 18 | +pub enum EIP712Error { |
| 19 | + /// Failed to convert fork version to chain ID. |
| 20 | + #[error("Network error: {0}")] |
| 21 | + NetworkError(#[from] charon_eth2::network::NetworkError), |
| 22 | + |
| 23 | + /// Failed to hash typed data. |
| 24 | + #[error("Failed to hash typed data: {0}")] |
| 25 | + FailedToHashTypedData(charon_eth2::eip712::Eip712Error), |
| 26 | + |
| 27 | + /// Failed to sign EIP-712. |
| 28 | + #[error("Failed to sign EIP-712: {0}")] |
| 29 | + FailedToSign(K1UtilError), |
| 30 | +} |
| 31 | + |
| 32 | +struct EIP712TypeField { |
| 33 | + pub field: &'static str, |
| 34 | + pub field_type: Primitive, |
| 35 | + pub value_func: ValueFunc, |
| 36 | +} |
| 37 | + |
| 38 | +struct EIP712Type { |
| 39 | + pub primary_type: &'static str, |
| 40 | + pub fields: Vec<EIP712TypeField>, |
| 41 | +} |
| 42 | + |
| 43 | +#[allow(dead_code)] // todo: remove this once it's used |
| 44 | +fn eip712_creator_config_hash() -> EIP712Type { |
| 45 | + EIP712Type { |
| 46 | + primary_type: "CreatorConfigHash", |
| 47 | + fields: vec![EIP712TypeField { |
| 48 | + field: "creator_config_hash", |
| 49 | + field_type: PRIMITIVE_STRING.to_string(), |
| 50 | + value_func: Box::new(|definition, _| { |
| 51 | + Value::String(format!("0x{}", hex::encode(&definition.config_hash))) |
| 52 | + }), |
| 53 | + }], |
| 54 | + } |
| 55 | +} |
| 56 | + |
| 57 | +#[allow(dead_code)] // todo: remove this once it's used |
| 58 | +fn eip712_operator_config_hash() -> EIP712Type { |
| 59 | + EIP712Type { |
| 60 | + primary_type: "OperatorConfigHash", |
| 61 | + fields: vec![EIP712TypeField { |
| 62 | + field: "operator_config_hash", |
| 63 | + field_type: PRIMITIVE_STRING.to_string(), |
| 64 | + value_func: Box::new(|definition, _| { |
| 65 | + Value::String(format!("0x{}", hex::encode(&definition.config_hash))) |
| 66 | + }), |
| 67 | + }], |
| 68 | + } |
| 69 | +} |
| 70 | + |
| 71 | +#[allow(dead_code)] // todo: remove this once it's used |
| 72 | +fn eip712_v1x3_config_hash() -> EIP712Type { |
| 73 | + EIP712Type { |
| 74 | + primary_type: "ConfigHash", |
| 75 | + fields: vec![EIP712TypeField { |
| 76 | + field: "config_hash", |
| 77 | + field_type: PRIMITIVE_STRING.to_string(), |
| 78 | + value_func: Box::new(|definition, _| { |
| 79 | + Value::String(format!("0x{}", hex::encode(&definition.config_hash))) |
| 80 | + }), |
| 81 | + }], |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +#[allow(dead_code)] // todo: remove this once it's used |
| 86 | +fn eip712_enr() -> EIP712Type { |
| 87 | + EIP712Type { |
| 88 | + primary_type: "ENR", |
| 89 | + fields: vec![EIP712TypeField { |
| 90 | + field: "ENR", |
| 91 | + field_type: PRIMITIVE_STRING.to_string(), |
| 92 | + value_func: Box::new(|_, operator| Value::String(operator.enr.clone())), |
| 93 | + }], |
| 94 | + } |
| 95 | +} |
| 96 | + |
| 97 | +fn eip712_terms_and_conditions() -> EIP712Type { |
| 98 | + EIP712Type { |
| 99 | + primary_type: "TermsAndConditions", |
| 100 | + fields: vec![ |
| 101 | + EIP712TypeField { |
| 102 | + field: "terms_and_conditions_hash", |
| 103 | + field_type: PRIMITIVE_STRING.to_string(), |
| 104 | + value_func: Box::new(|_, _| { |
| 105 | + Value::String( |
| 106 | + "0xd33721644e8f3afab1495a74abe3523cec12d48b8da6cb760972492ca3f1a273" |
| 107 | + .to_string(), |
| 108 | + ) |
| 109 | + }), |
| 110 | + }, |
| 111 | + EIP712TypeField { |
| 112 | + field: "version", |
| 113 | + field_type: PRIMITIVE_UINT256.to_string(), |
| 114 | + value_func: Box::new(|_, _| Value::Number(1)), |
| 115 | + }, |
| 116 | + ], |
| 117 | + } |
| 118 | +} |
| 119 | + |
| 120 | +#[allow(dead_code)] // todo: remove this once it's used |
| 121 | +fn get_operator_eip712_type(version: &str) -> EIP712Type { |
| 122 | + if !Definition::support_eip712_sigs(version) { |
| 123 | + unreachable!("invalid eip712 signature version"); // This should never happen |
| 124 | + } |
| 125 | + |
| 126 | + if version == V1_3 { |
| 127 | + return eip712_v1x3_config_hash(); |
| 128 | + } |
| 129 | + |
| 130 | + eip712_operator_config_hash() |
| 131 | +} |
| 132 | + |
| 133 | +fn digest_eip712( |
| 134 | + typ: &EIP712Type, |
| 135 | + definition: &Definition, |
| 136 | + operator: &Operator, |
| 137 | +) -> Result<Vec<u8>> { |
| 138 | + let chain_id = fork_version_to_chain_id(definition.fork_version.as_ref())?; |
| 139 | + |
| 140 | + let mut data = TypedData { |
| 141 | + domain: Domain { |
| 142 | + name: "Obol".to_string(), |
| 143 | + version: "1".to_string(), |
| 144 | + chain_id, |
| 145 | + }, |
| 146 | + primary_type: Type { |
| 147 | + name: typ.primary_type.to_string(), |
| 148 | + fields: vec![], |
| 149 | + }, |
| 150 | + }; |
| 151 | + |
| 152 | + for field in typ.fields.iter() { |
| 153 | + data.primary_type.fields.push(Field { |
| 154 | + name: field.field.to_string(), |
| 155 | + field_type: field.field_type.to_string(), |
| 156 | + value: (field.value_func)(definition, operator), |
| 157 | + }); |
| 158 | + } |
| 159 | + |
| 160 | + let digest = hash_typed_data(&data).map_err(EIP712Error::FailedToHashTypedData)?; |
| 161 | + |
| 162 | + Ok(digest) |
| 163 | +} |
| 164 | + |
| 165 | +fn sign_eip712( |
| 166 | + secret_key: &SecretKey, |
| 167 | + typ: &EIP712Type, |
| 168 | + definition: &Definition, |
| 169 | + operator: &Operator, |
| 170 | +) -> Result<Vec<u8>> { |
| 171 | + let digest = digest_eip712(typ, definition, operator)?; |
| 172 | + let signature = k1util::sign(secret_key, &digest).map_err(EIP712Error::FailedToSign)?; |
| 173 | + Ok(signature.to_vec()) |
| 174 | +} |
| 175 | + |
| 176 | +/// sign_terms_and_conditions returns the EIP712 signature for Obol's Terms and |
| 177 | +/// Conditions |
| 178 | +pub fn sign_terms_and_conditions( |
| 179 | + secret_key: &SecretKey, |
| 180 | + definition: &Definition, |
| 181 | +) -> Result<Vec<u8>> { |
| 182 | + sign_eip712( |
| 183 | + secret_key, |
| 184 | + &eip712_terms_and_conditions(), |
| 185 | + definition, |
| 186 | + &Operator::default(), |
| 187 | + ) |
| 188 | +} |
| 189 | + |
| 190 | +/// sign_cluster_definition_hash returns the EIP712 signature for the cluster |
| 191 | +/// definition hash |
| 192 | +pub fn sign_cluster_definition_hash( |
| 193 | + secret_key: &SecretKey, |
| 194 | + definition: &Definition, |
| 195 | +) -> Result<Vec<u8>> { |
| 196 | + sign_eip712( |
| 197 | + secret_key, |
| 198 | + &eip712_creator_config_hash(), |
| 199 | + definition, |
| 200 | + &Operator::default(), |
| 201 | + ) |
| 202 | +} |
| 203 | + |
| 204 | +#[cfg(test)] |
| 205 | +mod tests { |
| 206 | + use super::*; |
| 207 | + |
| 208 | + #[test] |
| 209 | + fn test_sign_terms_and_conditions() { |
| 210 | + let secret_key = SecretKey::from_slice( |
| 211 | + &hex::decode("0000000000000000000000000000000000000000000000000000000000000001") |
| 212 | + .unwrap(), |
| 213 | + ) |
| 214 | + .unwrap(); |
| 215 | + let definition = serde_json::from_str::<Definition>(include_str!( |
| 216 | + "examples/cluster-definition-000.json" |
| 217 | + )) |
| 218 | + .unwrap(); |
| 219 | + let signature = sign_terms_and_conditions(&secret_key, &definition).unwrap(); |
| 220 | + let expected_signature = hex::decode("4723ae21ae1d47cb76afc58177b40d1bf1b010147eec3eafedf467ad641290776c64336df8d3643eb637681b2d6429066f88877f987476a81ddf417603d74d0700").unwrap(); |
| 221 | + assert_eq!(signature, expected_signature); |
| 222 | + } |
| 223 | + |
| 224 | + #[test] |
| 225 | + fn test_sign_cluster_definition_hash() { |
| 226 | + let secret_key = SecretKey::from_slice( |
| 227 | + &hex::decode("0000000000000000000000000000000000000000000000000000000000000001") |
| 228 | + .unwrap(), |
| 229 | + ) |
| 230 | + .unwrap(); |
| 231 | + let definition = serde_json::from_str::<Definition>(include_str!( |
| 232 | + "examples/cluster-definition-000.json" |
| 233 | + )) |
| 234 | + .unwrap(); |
| 235 | + let signature = sign_cluster_definition_hash(&secret_key, &definition).unwrap(); |
| 236 | + let expected_signature = hex::decode("4d06378b88544748d27e656871fefdb258329ecbbecf2316cb03b3da1d499a2137fc8f1caddcaf47a8fd17a22d8f68c9333b21a031fd281c1e6e99623c1bd7f301").unwrap(); |
| 237 | + assert_eq!(signature, expected_signature); |
| 238 | + } |
| 239 | +} |
0 commit comments