Style Guide (Rust)¶
Formatting¶
- Use 2-space indentation, LF line endings
- Files end with a single newline
- No trailing whitespace
Example code:
Naming¶
| Form | Used for |
|---|---|
UpperCamelCase |
Types, traits, enum variants, type parameters |
snake_case |
Functions, methods, variables, modules, crate names |
SCREAMING_SNAKE_CASE |
Constants and statics |
'lowercase |
Lifetimes ('a, 'de, 'src) |
- Treat acronyms as whole words: write
TxId, notTXID;BlsPublicKey, notBLSPublicKey - Getters omit the
get_prefix because the field name alone is unambiguous; a mutable getter appends_mut - Primary constructors are named
new; alternatives use descriptive names orwith_suffixes (with_capacity) - Conversion constructors use
from_followed by the source type:Hash256::from_bytes - Error type names follow verb-object-error word order so they sort and read predictably:
DecodeError, notErrorDecode
Example code:
const MAX_BLOCK_SIZE: usize = 2_000_000;
struct ChainTip {
height: u64,
hash: BlockHash,
}
impl ChainTip {
// Getter omits the get_ prefix; the field name alone
// is unambiguous.
fn height(&self) -> u64 {
self.height
}
// Mutable getter appends _mut.
fn hash_mut(&mut self) -> &mut BlockHash {
&mut self.hash
}
}
Type Safety¶
The type system is the first line of defence. A constraint expressed as a type is checked at compile time and costs nothing at runtime.
Tip
Prefer #[derive] for standard trait implementations. A manual implementation is warranted only when the
derived behaviour would be incorrect or when redaction is needed.
- Wrap primitive types in newtypes when two values of the same underlying type carry different semantics; this prevents accidental transposition of arguments
- Prefer enums over booleans for function parameters because
Script::classify(P2pkh)communicates intent whereScript::classify(true)does not - Make invalid states unrepresentable; if a combination of fields is logically impossible, restructure the type so the compiler rejects it
- Derive common traits eagerly on public types:
Clone,Debug,PartialEq,Eq,Hash; the orphan rule prevents downstream crates from adding them later - All public types implement
Debugbecause diagnostic output and test assertions depend on it; for types holding sensitive data, provide a custom implementation that redacts the secret
Example code:
// Good: newtypes prevent mixing up hash types at the call site.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct TxHash(Hash256);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
struct BlockHash(Hash256);
fn lookup_tx(
block: &BlockHash,
tx: &TxHash,
) -> Option<Transaction> {
// The compiler rejects lookup_tx(tx, block); the types differ.
}
Error Handling¶
Info
Never call .unwrap() or .expect() on Result or Option in library code. A panic converts a
recoverable failure into process-level failure; depending on the panic strategy, the code may unwind or
abort, but either outcome is unacceptable for routine error handling.
Propagate errors with ? or handle them explicitly with match. Both clippy::unwrap_used and
clippy::expect_used are denied at the workspace level.
- Define domain-specific error enums with a manual
Displayimplementation; gatestd::error::Errorbehind thestdfeature so the error type remains usable inno_stdcontexts - Error messages are lowercase and carry no trailing punctuation; they may be embedded in larger messages by callers, so capitalization and periods would read awkwardly in the middle of a sentence
- Keep
?chains short; if a function contains more than a few?calls on unrelated operations, each operation likely belongs in its own function - Use
#[expect]instead of#[allow]when suppressing a lint;expectcauses a warning if the suppression becomes unnecessary, preventing stale overrides from accumulating silently - The
.todo(),.unimplemented(), and.unreachable()family of panicking stubs are not permitted; if a branch is genuinely unreachable, restructure the types so the compiler can prove it
Example code:
use core::fmt;
/// Errors produced by BLS operations.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Error {
/// secret key bytes are not a valid scalar
InvalidSecretKey,
/// public key bytes are not a valid G1 point
InvalidPublicKey,
/// signature verification failed
VerifyFailed,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidSecretKey => {
write!(f, "invalid secret key bytes")
}
Self::InvalidPublicKey => {
write!(f, "invalid public key bytes")
}
Self::VerifyFailed => {
write!(f, "signature verification failed")
}
}
}
}
// Only available when the downstream crate enables `std`.
#[cfg(feature = "std")]
impl std::error::Error for Error {}
// Good: propagation with ?; the caller decides how to handle the error.
fn verify_header(
raw: &[u8],
) -> Result<BlockHeader, DecodeError> {
if raw.len() < 80 {
return Err(DecodeError::Eof {
needed: 80,
remaining: raw.len(),
});
}
let header = decode_header(raw)?;
validate_pow(&header)?;
Ok(header)
}
Ownership and Borrowing¶
Rust's ownership model eliminates data races and use-after-free at compile time. Working with it, rather than around it, produces code that is both safe and efficient.
- Prefer borrowing over cloning; cloning allocates and copies, and signals to readers that the caller needs an independent copy
- Accept the most general reference that satisfies the function:
&strrather than&String,&[T]rather than&Vec<T>; this lets callers pass any type that dereferences to the expected slice - Return owned values when the caller needs ownership; returning a reference to a local is a compile error, and this is intentional
- Let the caller decide when to clone; a function that silently clones its input adds hidden cost and prevents the caller from choosing a cheaper alternative
Example code:
Conversions¶
Consistent conversion names tell the reader the cost and ownership semantics of an operation at a glance.
| Prefix | Cost | Ownership | Example |
|---|---|---|---|
as_ |
Free | &T to &U |
KeyId::as_bytes() |
to_ |
Allocates or computes | Borrows input | Hash256::to_bytes() |
into_ |
Variable | Consumes input | String::into_bytes() |
- Implement
From<T>for infallible conversions; the blanket impl providesInto<T>automatically, so we never implementIntodirectly - Implement
TryFrom<T>for conversions that can fail; the associated error type documents what can go wrong - Implement
AsRef<T>for cheap reference-to-reference conversions
Example code:
struct BlockHash([u8; 32]);
// Good: From for an infallible conversion.
impl From<[u8; 32]> for BlockHash {
fn from(bytes: [u8; 32]) -> Self {
Self(bytes)
}
}
// Good: TryFrom for a fallible conversion; the error type documents failure.
impl TryFrom<&[u8]> for BlockHash {
type Error = InvalidHashLength;
fn try_from(
slice: &[u8],
) -> Result<Self, Self::Error> {
let arr: [u8; 32] = slice
.try_into()
.map_err(|_| InvalidHashLength(slice.len()))?;
Ok(Self(arr))
}
}
// Good: as_ prefix signals a free borrow with no allocation.
impl BlockHash {
fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
Traits and Implementations¶
- Derive standard traits eagerly; the orphan rule prevents downstream crates from adding them, so we provide everything applicable up front
- Place
SerializeandDeserializebehind an optionalserdefeature so crates that do not need serialization avoid the dependency; usedefault-features = falsewithallocandderivefeatures forno_stdcompatibility - Sealed traits prevent downstream implementations; this allows adding methods in future versions without a breaking change
Example code:
Generics¶
- Use
impl Traitin argument position for simple, single-use bounds; use named type parameters when the same bound appears in multiple arguments or the return type - Prefer static dispatch for performance-critical paths; use
dyn Traitwhen the set of concrete types is open or when reducing binary size matters more than call overhead - Do not add trait bounds to struct definitions unless the bound is required for the struct's own invariants; bounds on impls are sufficient and avoid constraining downstream code
Example code:
Iterators¶
Iterator chains express data transformations declaratively. The compiler often optimises them into tight loops with no intermediate allocations.
- Implement
iter(),iter_mut(), andinto_iter()on collection types; the return types are namedIter,IterMut, andIntoIterrespectively - Implement
FromIteratorandExtendso the collection works with.collect()and.extend() - Prefer
filter_map()when filtering and mapping happen together; it expresses the intent in one place - Avoid
.collect()when the result is only iterated once; returnimpl Iterator<Item = T>instead to defer allocation - Implement
size_hint()on custom iterators so downstream consumers can pre-allocate accurately
Example code:
Code Comments¶
Comments explain intent and context that the code alone cannot convey. Restating what the code does adds noise and drifts out of sync with the implementation.
Inline Comments¶
- Line comments (
//) must not exceed 80 characters wide and 3 lines tall - An extremely complex algorithm may use two short paragraphs separated by a blank comment line
- Focus on why a decision was made, not what the code does
Rustdoc Comments¶
- Documentation comments (
///) must not exceed 80 characters wide - The summary is at most 3 lines; do not restate the function name or signature in prose because the reader can see them directly above
- Document
# Errorswhen the function returnsResult, listing each error variant and its cause - Document
# Panicsonly when the function can panic, which should be rare - Pad lines so right columns align evenly for visual consistency
- Use
?in doc examples instead of.unwrap()so readers copy safe patterns
Example code:
/// Decode a compact-encoded block header from raw bytes, verifying the
/// proof-of-work target against the declared difficulty.
///
/// # Errors
///
/// Returns `Eof` when the slice holds fewer than 80 bytes, or `BadTarget` when
/// the header fails the proof-of-work threshold.
fn decode_header(
raw: &[u8],
) -> Result<Header, DecodeError> {
// We validate length before field access to prevent out-of-bounds reads when
// the slice comes from malformed or adversarial input.
if raw.len() < 80 {
return Err(DecodeError::Eof {
needed: 80,
remaining: raw.len(),
});
}
// ...
}
// Bad: restates the signature, wall of text.
/// This function is called decode_header. It takes a byte slice called raw and
/// returns a Result containing either a Header or a DecodeError. The raw
/// parameter is the bytes to decode. If decoding succeeds it returns Ok with
/// the header inside.
fn decode_header(
raw: &[u8],
) -> Result<Header, DecodeError> {
// ...
}
Development Guidelines¶
Input Validation¶
Danger
Assume every value decoded from the wire is adversarial. Trust boundaries include: consensus-encoded messages, peer-supplied byte streams, and any externally-produced data fed into a decoder.
- Validate early, fail loudly. Check length, range, and structural invariants before the value reaches domain logic; return a clear error that tells the caller exactly what is wrong
- Encode validation in types. A newtype that can only be constructed through a validating constructor carries its proof of validity with it; downstream code never needs to re-check
- Limit input size. Set maximum sizes for payloads and maximum element counts for collections; unbounded input is an invitation for resource exhaustion
- Reject non-minimal encodings. CompactSize, VarInt, and similar variable-length encodings must use their shortest representation; accepting non-minimal forms creates consensus divergence
Example code:
use core::fmt;
/// 20-byte key hash that can only be constructed from a validating constructor.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct KeyId([u8; 20]);
#[derive(Debug)]
pub struct InvalidKeyIdLength(pub usize);
impl fmt::Display for InvalidKeyIdLength {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "expected 20 bytes, got {}", self.0)
}
}
impl KeyId {
pub fn from_slice(
bytes: &[u8],
) -> Result<Self, InvalidKeyIdLength> {
let arr: [u8; 20] = bytes
.try_into()
.map_err(|_| InvalidKeyIdLength(bytes.len()))?;
Ok(Self(arr))
}
pub fn as_bytes(&self) -> &[u8; 20] {
&self.0
}
}
Security¶
Note
Audit dependencies regularly. A single compromised or unmaintained transitive dependency can undermine all other precautions in the codebase.
- Never log secrets. Private keys, key shares, and seed material must never appear in log output, debug strings, or error messages
- Implement
Debugto redact sensitive fields. A customDebugthat prints a placeholder prevents accidental exposure through{:?}formatting in panics - Use constant-time comparison for secrets. Timing side-channels in byte-by-byte comparison leak information about secret values; use a constant-time equality function from a vetted cryptographic library
- Zero sensitive memory after use. Stack and heap buffers that held secrets should be zeroised before
deallocation to reduce the window of exposure; the
zeroizecrate provides aZeroizetrait and aZeroizeOnDropderive for this purpose - Prefer explicit failure over silent defaults. A default value for a missing secret silently degrades to an insecure state; failing explicitly is always safer than falling back to a placeholder
Example code:
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Zeroize, ZeroizeOnDrop)]
struct SecretKey([u8; 32]);
// Custom Debug prevents the key from appearing in panic output.
impl core::fmt::Debug for SecretKey {
fn fmt(
&self,
f: &mut core::fmt::Formatter<'_>,
) -> core::fmt::Result {
write!(f, "SecretKey(..)")
}
}