Skip to content

Commit 5864a80

Browse files
committed
fix: reuse ssz helpers from charon-cluster
1 parent 4be2b92 commit 5864a80

8 files changed

Lines changed: 43 additions & 92 deletions

File tree

crates/charon-cluster/src/ssz.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ pub(crate) const SSZ_LEN_FORK_VERSION: usize = 4;
3232
/// Length of a K1 signature.
3333
pub(crate) const SSZ_LEN_K1_SIG: usize = 65;
3434
/// Length of a BLS signature.
35-
pub(crate) const SSZ_LEN_BLS_SIG: usize = 96;
35+
pub const SSZ_LEN_BLS_SIG: usize = 96;
3636
/// Length of a hash.
3737
pub(crate) const SSZ_LEN_HASH: usize = 32;
3838
/// Length of withdrawal credentials.
3939
pub(crate) const SSZ_LEN_WITHDRAW_CREDS: usize = 32;
4040
/// Length of a public key.
41-
pub(crate) const SSZ_LEN_PUB_KEY: usize = 48;
41+
pub const SSZ_LEN_PUB_KEY: usize = 48;
4242

4343
/// HashFunc is a function that hashes a definition
4444
pub type HashFuncWithBool<T, H> = fn(&T, &mut H, bool) -> Result<(), SSZError<H>>;

crates/charon/src/obolapi/client.rs

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use std::time::Duration;
77

88
use charon_cluster::lock::Lock;
9-
use reqwest::StatusCode;
9+
use reqwest::{Method, StatusCode};
1010
use url::Url;
1111

1212
use crate::obolapi::error::{Error, Result};
@@ -110,14 +110,11 @@ impl Client {
110110

111111
let status = response.status();
112112
if !status.is_success() {
113-
let body_text = response
114-
.text()
115-
.await
116-
.unwrap_or_else(|_| String::from("failed to read body"));
113+
let body_text = response.text().await.unwrap_or_default();
117114

118115
return Err(Error::HttpError {
119-
method: "POST".to_string(),
120-
status: status.as_u16(),
116+
method: Method::POST,
117+
status,
121118
body: body_text,
122119
});
123120
}
@@ -148,14 +145,11 @@ impl Client {
148145
return Err(Error::NoExit);
149146
}
150147

151-
let body_text = response
152-
.text()
153-
.await
154-
.unwrap_or_else(|_| String::from("failed to read body"));
148+
let body_text = response.text().await.unwrap_or_default();
155149

156150
return Err(Error::HttpError {
157-
method: "GET".to_string(),
158-
status: status.as_u16(),
151+
method: Method::GET,
152+
status,
159153
body: body_text,
160154
});
161155
}
@@ -187,8 +181,8 @@ impl Client {
187181
return Err(Error::NoExit);
188182
}
189183
return Err(Error::HttpError {
190-
method: "DELETE".to_string(),
191-
status: status.as_u16(),
184+
method: Method::default(),
185+
status,
192186
body: String::new(),
193187
});
194188
}

crates/charon/src/obolapi/error.rs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Error types for the Obol API client.
22
3+
use reqwest::{Method, StatusCode};
4+
35
/// Result type for Obol API operations.
46
pub type Result<T> = std::result::Result<T, Error>;
57

@@ -14,9 +16,9 @@ pub enum Error {
1416
#[error("HTTP {method} request failed: status {status}, body: {body}")]
1517
HttpError {
1618
/// HTTP method (GET, POST, DELETE).
17-
method: String,
19+
method: Method,
1820
/// HTTP status code.
19-
status: u16,
21+
status: StatusCode,
2022
/// Response body.
2123
body: String,
2224
},
@@ -37,15 +39,6 @@ pub enum Error {
3739
#[error("hex decoding error: {0}")]
3840
HexDecode(#[from] hex::FromHexError),
3941

40-
/// Invalid hex string length.
41-
#[error("invalid hex length: expected {expected} bytes, got {actual} bytes")]
42-
InvalidHexLength {
43-
/// Expected length in bytes.
44-
expected: usize,
45-
/// Actual length in bytes.
46-
actual: usize,
47-
},
48-
4942
/// Empty hex string.
5043
#[error("empty hex string")]
5144
EmptyHex,

crates/charon/src/obolapi/exit.rs

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use charon_crypto::{blst_impl::BlstImpl, tbls::Tbls, types::Signature};
99
use serde::{Deserialize, Serialize};
1010

1111
use charon_cluster::{
12-
helpers::left_pad,
12+
helpers::to_0x_hex,
13+
ssz::{SSZ_LEN_BLS_SIG, SSZ_LEN_PUB_KEY},
1314
ssz_hasher::{HashWalker, Hasher},
1415
};
1516
use eth2api::types::{
@@ -19,12 +20,15 @@ use eth2api::types::{
1920
use crate::obolapi::{
2021
client::Client,
2122
error::{Error, Result},
22-
helper::{bearer_string, from_0x, to_0x},
23+
helper::{bearer_string, from_0x},
2324
};
2425

2526
/// Type alias for signed voluntary exit from eth2api.
2627
pub type SignedVoluntaryExit = GetPoolVoluntaryExitsResponseResponseDatum;
2728

29+
// TODO: Unify SSZ hashing across the workspace. `charon-cluster` already has
30+
// SSZ hashing utilities. Consider extracting a shared SSZ crate (or promoting
31+
// the existing hasher) so all crates share one SSZ interface and error type.
2832
/// Trait for types that can be hashed using SSZ hash tree root.
2933
pub trait SszHashable {
3034
/// Hashes this value into the provided hasher.
@@ -44,7 +48,7 @@ impl SszHashable for SignedVoluntaryExit {
4448

4549
self.message.hash_with(hh)?;
4650
let sig_bytes = from_0x(&self.signature, SSZ_LEN_BLS_SIG)?;
47-
put_bytes_n(hh, &sig_bytes, SSZ_LEN_BLS_SIG)?;
51+
charon_cluster::helpers::put_bytes_n(hh, &sig_bytes, SSZ_LEN_BLS_SIG)?;
4852

4953
hh.merkleize(index)?;
5054
Ok(())
@@ -190,7 +194,7 @@ impl From<PartialExitRequest> for PartialExitRequestDto {
190194
fn from(req: PartialExitRequest) -> Self {
191195
Self {
192196
unsigned: req.unsigned,
193-
signature: to_0x(&req.signature),
197+
signature: to_0x_hex(&req.signature),
194198
}
195199
}
196200
}
@@ -232,16 +236,14 @@ impl SszHashable for FullExitAuthBlob {
232236
let index = hh.index();
233237

234238
hh.put_bytes(&self.lock_hash)?;
235-
put_bytes_n(hh, &self.validator_pubkey, SSZ_LEN_PUB_KEY)?;
239+
charon_cluster::helpers::put_bytes_n(hh, &self.validator_pubkey, SSZ_LEN_PUB_KEY)?;
236240
hh.put_uint64(self.share_index)?;
237241

238242
hh.merkleize(index)?;
239243
Ok(())
240244
}
241245
}
242246
const SSZ_MAX_EXITS: usize = 65536;
243-
const SSZ_LEN_PUB_KEY: usize = 48;
244-
const SSZ_LEN_BLS_SIG: usize = 96;
245247

246248
impl Client {
247249
/// Posts the set of msg's to the Obol API, for a given lock hash.
@@ -253,7 +255,7 @@ impl Client {
253255
identity_key: &k256::SecretKey,
254256
mut exit_blobs: Vec<ExitBlob>,
255257
) -> Result<()> {
256-
let lock_hash_str = to_0x(lock_hash);
258+
let lock_hash_str = to_0x_hex(lock_hash);
257259
let path = submit_partial_exit_url(&lock_hash_str);
258260

259261
let url = self.build_url(&path)?;
@@ -300,7 +302,7 @@ impl Client {
300302
// Validate public key is 48 bytes
301303
let val_pubkey_bytes = from_0x(val_pubkey, 48)?;
302304

303-
let path = fetch_full_exit_url(val_pubkey, &to_0x(lock_hash), share_index);
305+
let path = fetch_full_exit_url(val_pubkey, &to_0x_hex(lock_hash), share_index);
304306

305307
let url = self.build_url(&path)?;
306308

@@ -362,7 +364,7 @@ impl Client {
362364
epoch: epoch_u64.to_string(),
363365
validator_index: exit_response.validator_index.to_string(),
364366
},
365-
signature: to_0x(&full_sig),
367+
signature: to_0x_hex(&full_sig),
366368
},
367369
})
368370
}
@@ -380,7 +382,7 @@ impl Client {
380382
// Validate public key is 48 bytes
381383
let val_pubkey_bytes = from_0x(val_pubkey, 48)?;
382384

383-
let path = delete_partial_exit_url(val_pubkey, &to_0x(lock_hash), share_index);
385+
let path = delete_partial_exit_url(val_pubkey, &to_0x_hex(lock_hash), share_index);
384386

385387
let url = self.build_url(&path)?;
386388

@@ -423,19 +425,6 @@ fn fetch_full_exit_url(val_pubkey: &str, lock_hash: &str, share_index: u64) -> S
423425
format!("/exp/exit/{}/{}/{}", lock_hash, share_index, val_pubkey)
424426
}
425427

426-
fn put_bytes_n(hh: &mut Hasher, bytes: &[u8], expected_len: usize) -> Result<()> {
427-
if bytes.len() > expected_len {
428-
use charon_cluster::ssz::SSZError;
429-
return Err(Error::Ssz(SSZError::UnsupportedVersion(format!(
430-
"bytes too long: expected {}, got {}",
431-
expected_len,
432-
bytes.len()
433-
))));
434-
}
435-
let padded: Vec<u8> = left_pad(bytes, expected_len);
436-
Ok(hh.put_bytes(&padded)?)
437-
}
438-
439428
#[cfg(test)]
440429
mod tests {
441430
use super::*;
@@ -486,7 +475,7 @@ mod tests {
486475
404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f";
487476

488477
let exit_blob = ExitBlob {
489-
public_key: Some(to_0x(&validator_pubkey)),
478+
public_key: Some(to_0x_hex(&validator_pubkey)),
490479
signed_exit_message: SignedVoluntaryExit {
491480
message: Phase0SignedVoluntaryExitMessage {
492481
epoch: "194048".to_string(),

crates/charon/src/obolapi/helper.rs

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,22 @@
1-
//! Serialization helpers for hex encoding/decoding with 0x prefix.
2-
//!
3-
//! These helpers match the behavior of the Go implementation for handling
4-
//! hex-encoded data with optional 0x prefixes and strict length validation.
1+
use charon_cluster::helpers::to_0x_hex;
52

63
use crate::obolapi::error::{Error, Result};
74

85
/// Decodes a hex-encoded string and expects it to be exactly `expected_len`
96
/// bytes. Accepts both 0x-prefixed strings and plain hex strings.
10-
pub fn from_0x(data: &str, expected_len: usize) -> Result<Vec<u8>> {
7+
pub(crate) fn from_0x(data: &str, expected_len: usize) -> Result<Vec<u8>> {
118
if data.is_empty() {
129
return Err(Error::EmptyHex);
1310
}
14-
15-
let hex_str = data.strip_prefix("0x").unwrap_or(data);
16-
let bytes = hex::decode(hex_str)?;
17-
18-
if bytes.len() != expected_len {
19-
return Err(Error::InvalidHexLength {
20-
expected: expected_len,
21-
actual: bytes.len(),
22-
});
23-
}
24-
25-
Ok(bytes)
26-
}
27-
28-
/// Encodes bytes to a hex string with 0x prefix.
29-
/// Uses lowercase hex encoding and includes the 0x prefix.
30-
pub fn to_0x(data: &[u8]) -> String {
31-
format!("0x{}", hex::encode(data))
11+
Ok(charon_cluster::helpers::from_0x_hex_str(
12+
data,
13+
expected_len,
14+
)?)
3215
}
3316

3417
/// Formats bytes as a bearer token string.
35-
pub fn bearer_string(data: &[u8]) -> String {
36-
format!("Bearer {}", to_0x(data))
18+
pub(crate) fn bearer_string(data: &[u8]) -> String {
19+
format!("Bearer {}", to_0x_hex(data))
3720
}
3821

3922
#[cfg(test)]
@@ -61,13 +44,7 @@ mod tests {
6144
#[test]
6245
fn test_from_0x_wrong_length() {
6346
let result = from_0x("0x1234", 3);
64-
assert!(matches!(result, Err(Error::InvalidHexLength { .. })));
65-
}
66-
67-
#[test]
68-
fn test_to_0x() {
69-
let hex = to_0x(&[0x12, 0x34]);
70-
assert_eq!(hex, "0x1234");
47+
assert!(result.is_err());
7148
}
7249

7350
#[test]

crates/charon/src/obolapi/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
mod client;
44
mod error;
55
mod helper;
6-
mod test_api;
6+
mod test;
77

88
pub mod exit;
99
pub mod publish;

crates/charon/src/obolapi/publish.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@
66
use charon_cluster::lock::Lock;
77
use serde::{Deserialize, Serialize};
88

9-
use crate::obolapi::{
10-
client::Client,
11-
error::Result,
12-
helper::{bearer_string, to_0x},
13-
};
9+
use charon_cluster::helpers::to_0x_hex;
10+
11+
use crate::obolapi::{client::Client, error::Result, helper::bearer_string};
1412

1513
/// Request to sign Obol's Terms and Conditions.
1614
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -88,7 +86,7 @@ impl Client {
8886
address: user_addr.to_string(),
8987
version: 1,
9088
terms_and_conditions_hash: TERMS_AND_CONDITIONS_HASH.to_string(),
91-
fork_version: to_0x(fork_version),
89+
fork_version: to_0x_hex(fork_version),
9290
};
9391

9492
let body = serde_json::to_vec(&request)?;

0 commit comments

Comments
 (0)