Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion .claude/skills/rust-style/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,41 @@ Rules:
- Prefer copying doc comments from Go and adapting to Rust conventions (avoid “Type is a …”).
- Avoid leaving TODOs in merged code. If a short-lived internal note is necessary, use `// TODO:` and remove before PR merge.

## Imports

Import **modules, types, traits, enums, and constants** — but **not free (standalone) functions**. Call free functions qualified through their parent module so the call site shows where the function comes from.

```rust
// Bad — free function imported bare
use crate::name::peer_name;
let label = peer_name(&id);

// Good — import the module, call qualified
use crate::name;
let label = name::peer_name(&id);
```

For a function re-exported at a crate root, qualify through the crate name (already in scope) rather than adding a bare `use`:

```rust
// Bad
use pluto_k1util::load;
let key = load(path)?;

// Good
let key = pluto_k1util::load(path)?;
```

Rules:

- Types, structs, enums, and constants **should** be imported bare (`use foo::Bar;`, then `Bar`).
- Traits **must** be imported bare — they need to be in scope for method resolution.
- Only free functions are qualified through their module. When a `use` mixes types and a free function from the same module, add `self` and drop the function: `use foo::bar::{self, SomeType};`, then call `bar::the_fn()`.
- This mirrors how helpers such as `peer_name`, `hash_proto`, and `to_0x_hex` are called across the workspace (`module::func()`), keeping call sites self-documenting.

## Generalized Parameter Types

Prefer generic parameters over concrete types when a function only needs the behavior of a trait. This mirrors the standard library's own conventions and makes functions callable with a wider range of inputs without extra allocations.
Prefer generic parameters over concrete types when a function only needs the behavior of a trait. This mirrors the standard library's own conventions and makes functions callable with a wider range of inputs without extra allocations. Apply this especially to **public APIs**, and keep it consistent across sibling functions in the same module — a module should not mix `&str` and `impl AsRef<str>` for the same kind of argument. Full uniformity is not the goal: private helpers and hot internal paths may keep concrete types.

| Instead of | Prefer | Accepts |
| --- | --- | --- |
Expand All @@ -138,6 +170,7 @@ Prefer generic parameters over concrete types when a function only needs the beh
| `&[u8]` | `impl AsRef<[u8]>` | `&[u8]`, `Vec<u8>`, arrays, … |
| `&Vec<T>` | `impl AsRef<[T]>` | `Vec<T>`, slices, arrays, … |
| `String` (owned, read-only) | `impl Into<String>` | `&str`, `String`, … |
| `&[T]` / iterator | `impl IntoIterator<Item = T>` | `Vec<T>`, arrays, iterators, … |

Examples:

Expand Down
4 changes: 2 additions & 2 deletions crates/app/src/monitoringapi/checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use super::{
metrics::MONITORING_METRICS,
readiness::{ReadinessError, ReadyResult, ReadyState},
};
use crate::eth2wrap::version::check_beacon_node_version;
use crate::eth2wrap::version;

/// Slots behind head after which the beacon node is considered too far behind.
const BN_FAR_BEHIND_SLOTS: u64 = 320;
Expand Down Expand Up @@ -128,7 +128,7 @@ async fn set_beacon_node_version(beacon_node: &EthBeaconNodeApiClient) {
MONITORING_METRICS.beacon_node_version[&label].set(1);

// The semantic compatibility check uses the FULL (untruncated) version.
check_beacon_node_version(&version);
version::check_beacon_node_version(&version);
}

/// Maximum length (in bytes) for the upstream-supplied beacon-node version
Expand Down
38 changes: 19 additions & 19 deletions crates/app/src/obolapi/exit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ use pluto_crypto::{tbls, types::Signature};
use serde::{Deserialize, Serialize};

use pluto_cluster::{
helpers::to_0x_hex,
helpers,
ssz::{SSZ_LEN_BLS_SIG, SSZ_LEN_PUB_KEY},
};
use pluto_eth2api::types::{
GetPoolVoluntaryExitsResponseResponseDatum, Phase0SignedVoluntaryExitMessage,
};
use pluto_ssz::{HashRoot, HashWalker, Hasher, put_bytes_n};
use pluto_ssz::{HashRoot, HashWalker, Hasher};

use crate::obolapi::{
client::Client,
error::{Error, Result},
helper::{bearer_string, from_0x},
helper,
};

/// Type alias for signed voluntary exit from eth2api.
Expand All @@ -44,8 +44,8 @@ impl SszHashable for SignedVoluntaryExit {
let index = hh.index();

self.message.hash_with(hh)?;
let sig_bytes = from_0x(&self.signature, SSZ_LEN_BLS_SIG)?;
put_bytes_n(hh, &sig_bytes, SSZ_LEN_BLS_SIG)?;
let sig_bytes = helper::from_0x(&self.signature, SSZ_LEN_BLS_SIG)?;
pluto_ssz::put_bytes_n(hh, &sig_bytes, SSZ_LEN_BLS_SIG)?;

hh.merkleize(index)?;
Ok(())
Expand Down Expand Up @@ -88,7 +88,7 @@ impl SszHashable for ExitBlob {
"missing public key".to_string(),
))
})?;
let pk_bytes = from_0x(pk, SSZ_LEN_PUB_KEY)?;
let pk_bytes = helper::from_0x(pk, SSZ_LEN_PUB_KEY)?;
hh.put_bytes(&pk_bytes)?;

self.signed_exit_message.hash_with(hh)?;
Expand Down Expand Up @@ -178,7 +178,7 @@ impl TryFrom<PartialExitRequestDto> for PartialExitRequest {
type Error = Error;

fn try_from(dto: PartialExitRequestDto) -> Result<Self> {
let signature = from_0x(&dto.signature, 65)?;
let signature = helper::from_0x(&dto.signature, 65)?;

Ok(Self {
unsigned: dto.unsigned,
Expand All @@ -191,7 +191,7 @@ impl From<PartialExitRequest> for PartialExitRequestDto {
fn from(req: PartialExitRequest) -> Self {
Self {
unsigned: req.unsigned,
signature: to_0x_hex(&req.signature),
signature: helpers::to_0x_hex(&req.signature),
}
}
}
Expand Down Expand Up @@ -233,7 +233,7 @@ impl SszHashable for FullExitAuthBlob {
let index = hh.index();

hh.put_bytes(&self.lock_hash)?;
put_bytes_n(hh, &self.validator_pubkey, SSZ_LEN_PUB_KEY)?;
pluto_ssz::put_bytes_n(hh, &self.validator_pubkey, SSZ_LEN_PUB_KEY)?;
hh.put_uint64(self.share_index)?;

hh.merkleize(index)?;
Expand All @@ -252,7 +252,7 @@ impl Client {
identity_key: &k256::SecretKey,
mut exit_blobs: Vec<ExitBlob>,
) -> Result<()> {
let lock_hash_str = to_0x_hex(lock_hash);
let lock_hash_str = helpers::to_0x_hex(lock_hash);
let path = submit_partial_exit_url(&lock_hash_str);

let url = self.build_url(&path)?;
Expand Down Expand Up @@ -297,9 +297,9 @@ impl Client {
identity_key: &k256::SecretKey,
) -> Result<ExitBlob> {
// Validate public key is 48 bytes
let val_pubkey_bytes = from_0x(val_pubkey, 48)?;
let val_pubkey_bytes = helper::from_0x(val_pubkey, 48)?;

let path = fetch_full_exit_url(val_pubkey, &to_0x_hex(lock_hash), share_index);
let path = fetch_full_exit_url(val_pubkey, &helpers::to_0x_hex(lock_hash), share_index);

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

Expand All @@ -316,7 +316,7 @@ impl Client {

let headers = vec![(
"Authorization".to_string(),
bearer_string(&lock_hash_signature),
helper::bearer_string(&lock_hash_signature),
)];

let response_body = self.http_get(url, Some(&headers)).await?;
Expand All @@ -338,7 +338,7 @@ impl Client {
}

// A BLS signature is 96 bytes long
let sig_bytes = from_0x(sig_str, 96)?;
let sig_bytes = helper::from_0x(sig_str, 96)?;

// Convert to Signature type
let mut sig = [0u8; 96];
Expand Down Expand Up @@ -366,7 +366,7 @@ impl Client {
epoch: epoch_u64.to_string(),
validator_index: exit_response.validator_index.to_string(),
},
signature: to_0x_hex(&full_sig),
signature: helpers::to_0x_hex(&full_sig),
},
})
}
Expand All @@ -382,9 +382,9 @@ impl Client {
identity_key: &k256::SecretKey,
) -> Result<()> {
// Validate public key is 48 bytes
let val_pubkey_bytes = from_0x(val_pubkey, 48)?;
let val_pubkey_bytes = helper::from_0x(val_pubkey, 48)?;

let path = delete_partial_exit_url(val_pubkey, &to_0x_hex(lock_hash), share_index);
let path = delete_partial_exit_url(val_pubkey, &helpers::to_0x_hex(lock_hash), share_index);

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

Expand All @@ -400,7 +400,7 @@ impl Client {

let headers = vec![(
"Authorization".to_string(),
bearer_string(&lock_hash_signature),
helper::bearer_string(&lock_hash_signature),
)];

self.http_delete(url, Some(&headers)).await?;
Expand Down Expand Up @@ -477,7 +477,7 @@ mod tests {
404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f";

let exit_blob = ExitBlob {
public_key: Some(to_0x_hex(&validator_pubkey)),
public_key: Some(helpers::to_0x_hex(&validator_pubkey)),
signed_exit_message: SignedVoluntaryExit {
message: Phase0SignedVoluntaryExitMessage {
epoch: "194048".to_string(),
Expand Down
16 changes: 12 additions & 4 deletions crates/build-proto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
//!
//! This crate compiles the protobuf files.

use std::{fs, io::Result, path::PathBuf};
use std::{
fs,
io::Result,
path::{Path, PathBuf},
};

/// Compiles the protobuf files in the given directory.
pub fn compile_protos(proto_dir: &str) -> Result<()> {
pub fn compile_protos(proto_dir: impl AsRef<Path>) -> Result<()> {
let proto_dir = proto_dir.as_ref();
let proto_files: Vec<PathBuf> = {
let mut files: Vec<PathBuf> = fs::read_dir(proto_dir)?
.filter_map(|entry| entry.ok())
Expand All @@ -17,7 +22,10 @@ pub fn compile_protos(proto_dir: &str) -> Result<()> {
};

if proto_files.is_empty() {
println!("cargo:warning=No .proto files found in {}", proto_dir);
println!(
"cargo:warning=No .proto files found in {}",
proto_dir.display()
);
return Ok(());
}

Expand All @@ -44,7 +52,7 @@ pub fn compile_protos(proto_dir: &str) -> Result<()> {
}

/// Adds file attributes to the generated files.
fn add_file_attributes(proto_dir: &str) -> Result<()> {
fn add_file_attributes(proto_dir: &Path) -> Result<()> {
let header = r#"// This file is @generated by prost-build.
#![allow(dead_code)]
#![allow(missing_docs)]
Expand Down
13 changes: 6 additions & 7 deletions crates/cli/src/commands/create_cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,12 @@ use pluto_eth2util::{
network, registration as eth2util_registration,
};
use pluto_p2p::k1 as p2p_k1;
use pluto_ssz::to_0x_hex;
use rand::rngs::OsRng;
use tracing::{debug, info, warn};

use crate::{
commands::{
address_validation::validate_addresses,
address_validation,
constants::{MIN_NODES, MIN_THRESHOLD},
create_dkg,
},
Expand Down Expand Up @@ -938,7 +937,7 @@ fn new_def_from_config(args: &CreateClusterArgs) -> Result<Definition> {
return Err(CreateClusterError::MissingNumValidatorsOrDefinitionFile);
}

let (fee_recipient_addrs, withdrawal_addrs) = validate_addresses(
let (fee_recipient_addrs, withdrawal_addrs) = address_validation::validate_addresses(
num_validators,
&args.fee_recipient_addrs,
&args.withdrawal_addrs,
Expand Down Expand Up @@ -1204,7 +1203,7 @@ async fn load_definition(

info!(
url = def_file,
definition_hash = to_0x_hex(&def.definition_hash),
definition_hash = pluto_ssz::to_0x_hex(&def.definition_hash),
"Cluster definition downloaded from URL"
);

Expand All @@ -1216,7 +1215,7 @@ async fn load_definition(

info!(
path = def_file,
definition_hash = to_0x_hex(&def.definition_hash),
definition_hash = pluto_ssz::to_0x_hex(&def.definition_hash),
"Cluster definition loaded from disk",
);

Expand Down Expand Up @@ -2342,7 +2341,7 @@ mod tests {
// "insufficient fee recipient addresses": 0 addrs for 4 validators →
// error
{
let err = super::validate_addresses(4, &[], &[]).unwrap_err();
let err = address_validation::validate_addresses(4, &[], &[]).unwrap_err();
let err_str = format!("{err}");
assert!(
err_str.contains("mismatching --num-validators and --fee-recipient-addresses"),
Expand All @@ -2354,7 +2353,7 @@ mod tests {
// validator → error
{
let fee_addr = "0x0000000000000000000000000000000000000000".to_string();
let err = super::validate_addresses(1, &[fee_addr], &[]).unwrap_err();
let err = address_validation::validate_addresses(1, &[fee_addr], &[]).unwrap_err();
let err_str = format!("{err}");
assert!(
err_str.contains("mismatching --num-validators and --withdrawal-addresses"),
Expand Down
29 changes: 14 additions & 15 deletions crates/cli/src/commands/create_dkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@ use pluto_cluster::{
definition::{Creator, Definition},
operator::Operator,
};
use pluto_consensus::protocols::is_supported_protocol_name;
use pluto_consensus::protocols;
use pluto_eth2util::{
deposit::{eths_to_gweis, verify_deposit_amounts},
deposit,
enr::Record,
helpers::{checksum_address, public_key_to_address},
network::{
GNOSIS, GOERLI, HOODI, MAINNET, PRATER, SEPOLIA, network_to_fork_version, valid_network,
},
helpers,
network::{self, GNOSIS, GOERLI, HOODI, MAINNET, PRATER, SEPOLIA},
};
use thiserror::Error;
use tracing::{info, warn};
Expand Down Expand Up @@ -328,7 +326,7 @@ async fn run_create_dkg(mut args: CreateDkgArgs) -> Result<(), CreateDkgError> {
}

for (i, addr) in args.operator_addresses.iter().enumerate() {
let checksum_addr = checksum_address(addr)
let checksum_addr = helpers::checksum_address(addr)
.map_err(|source| CreateDkgError::InvalidOperatorAddress { index: i, source })?;
operators.push(Operator {
address: checksum_addr,
Expand All @@ -351,12 +349,12 @@ async fn run_create_dkg(mut args: CreateDkgArgs) -> Result<(), CreateDkgError> {
args.threshold
};

let fork_version_hex = network_to_fork_version(&args.network)?;
let fork_version_hex = network::network_to_fork_version(&args.network)?;

let (priv_key, creator) = if args.publish {
// Temporary creator address
let key = SecretKey::random(&mut OsRng);
let addr = public_key_to_address(&key.public_key());
let addr = helpers::public_key_to_address(&key.public_key());
(
Some(key),
Creator {
Expand All @@ -368,7 +366,7 @@ async fn run_create_dkg(mut args: CreateDkgArgs) -> Result<(), CreateDkgError> {
(None, Creator::default())
};

let deposit_amounts_gwei: Vec<u64> = eths_to_gweis(&args.deposit_amounts);
let deposit_amounts_gwei: Vec<u64> = deposit::eths_to_gweis(&args.deposit_amounts);

let mut def = Definition::new(
args.name.clone(),
Expand Down Expand Up @@ -430,16 +428,17 @@ fn validate_dkg_config(
return Err(CreateDkgError::TooFewOperators { num_operators });
}

if !valid_network(network) {
if !network::valid_network(network) {
return Err(CreateDkgError::UnsupportedNetwork);
}

if !deposit_amounts.is_empty() {
let gweis = eths_to_gweis(deposit_amounts);
verify_deposit_amounts(&gweis, compounding)?;
let gweis = deposit::eths_to_gweis(deposit_amounts);
deposit::verify_deposit_amounts(&gweis, compounding)?;
}

if !consensus_protocol.is_empty() && !is_supported_protocol_name(consensus_protocol) {
if !consensus_protocol.is_empty() && !protocols::is_supported_protocol_name(consensus_protocol)
{
return Err(CreateDkgError::UnsupportedConsensusProtocol);
}

Expand Down Expand Up @@ -485,7 +484,7 @@ pub fn validate_withdrawal_addrs(
network: &str,
) -> Result<(), WithdrawalValidationError> {
for addr in addrs {
let checksum_addr = checksum_address(addr).map_err(|e| {
let checksum_addr = helpers::checksum_address(addr).map_err(|e| {
WithdrawalValidationError::InvalidWithdrawalAddress {
address: addr.clone(),
reason: e.to_string(),
Expand Down
Loading
Loading