Skip to content

Commit 04d7ffc

Browse files
committed
feat: add eip712 utils
1 parent 97760ff commit 04d7ffc

9 files changed

Lines changed: 846 additions & 3 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/charon-cluster/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ rand_core.workspace = true
2121
libp2p.workspace = true
2222
charon-p2p.workspace = true
2323
charon-eth2.workspace = true
24+
charon-k1util.workspace = true
25+
k256.workspace = true
2426

2527
[build-dependencies]
2628
prost-build.workspace = true

crates/charon-cluster/src/definition.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,8 +432,7 @@ impl Definition {
432432
/// Returns true if the provided definition version supports EIP712
433433
/// signatures. Note that Definition versions prior to v1.3.0 don't
434434
/// support EIP712 signatures.
435-
#[allow(unused)]
436-
fn support_eip712_sigs(version: &str) -> bool {
435+
pub(crate) fn support_eip712_sigs(version: &str) -> bool {
437436
!matches!(version, V1_0 | V1_1 | V1_2)
438437
}
439438

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
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+
}

crates/charon-cluster/src/operator.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use serde_with::serde_as;
44

55
/// Operator represents a charon node operator.
66
#[serde_as]
7-
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
7+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
88
#[serde(rename_all = "snake_case")]
99
pub struct Operator {
1010
/// The Ethereum address of the operator

crates/charon-eth2/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ sha3.workspace = true
1414
hex.workspace = true
1515
charon-testutil.workspace = true
1616
charon-k1util.workspace = true
17+
chrono.workspace = true
1718

1819
[lints]
1920
workspace = true

0 commit comments

Comments
 (0)