tn-rust-skills

star 2

Reference skill containing all telcoin-network Rust coding conventions, rules, and anti-patterns. Invoked by tn-* subagents to load domain knowledge into their context window. NOT user-invocable. Invoked programmatically by tn-* agents via the Skill tool.

grantkee By grantkee schedule Updated 6/2/2026

name: tn-rust-skills description: | Reference skill containing all telcoin-network Rust coding conventions, rules, and anti-patterns. Invoked by tn-* subagents to load domain knowledge into their context window. NOT user-invocable. Invoked programmatically by tn-* agents via the Skill tool.

Telcoin Network Rust Conventions

Reference material for all tn-* agents working in the telcoin-network codebase. Invoke this skill to load coding conventions, rules, and anti-patterns before writing or reviewing Rust code.

Project Context

The node runs epochs. Each epoch has a committee of BLS-keyed validators that run:

  1. Workers — build transaction batches, seal them via quorum, gossip batch digests
  2. Primary — propose DAG headers referencing batch digests, collect votes, form certificates
  3. Bullshark consensus — order the certificate DAG into a committed sequence
  4. Executor — subscribe to committed subdags, build ConsensusOutput
  5. Engine — receive ConsensusOutput, execute EVM blocks via Reth, extend canonical chain

At epoch boundaries the on-chain committee contract is read, a new committee is formed, and all epoch-scoped resources (TaskManager, ConsensusBus epoch channels, storage) are recycled. Non-validator "observer" nodes follow consensus headers via gossipsub and execute blocks without participating in DAG construction.

Dependency direction rules: tn-types is the foundation — almost everything depends on it. Dependencies flow downward through layers (types → storage → consensus/execution → engine → node → CLI → binary). Never introduce upward dependencies (e.g., tn-types must not depend on tn-storage).

Architecture Awareness

Before writing code, study the codebase architecture. Pay close attention to:

  • Domain boundaries: execution, consensus, worker, primary, networking, storage, etc.
  • Module organization: understand which crate/module owns which responsibility
  • Existing patterns: match the idioms, error handling, and abstractions already in use
  • Dependency direction: never introduce circular dependencies or violate layering

If your change touches a domain boundary, pause and verify you're putting logic in the correct domain. Domain-level logic must stay isolated.

New Crate Policy

Always ask permission before adding a new crate to Cargo.toml. Explain:

  • What the crate does
  • Why it's needed (vs implementing it or using an existing dependency)
  • Its maintenance status and trust level

Conventions

Toolchain

  • Edition: Rust 2021
  • MSRV: 1.94
  • Nightly for fmt/clippy: nightly-2026-03-20 (used via cargo +nightly-2026-03-20 fmt)
  • Test runner: cargo nextest (parallel execution)
  • Workspace lints (in root Cargo.toml):
    • unused_must_use = "deny" -- all Result and #[must_use] values must be handled
    • missing_docs = "warn" -- add doc comments to public items
    • unreachable_pub = "warn" -- prefer pub(crate) over pub for internal items
    • unused_crate_dependencies = "warn" -- every dep must be used
    • rust_2018_idioms = "deny" -- modern Rust style enforced

Error handling

  • Domain errors: Use thiserror with enum variants. Each domain has its own error type (e.g., DagError, HeaderError, CertificateError, BlockSealError, SubscriberError).
  • Result type aliases: Define pub type FooResult<T> = Result<T, FooError>; alongside the error enum.
  • Context propagation: Use eyre::Result (via eyre::Report) for storage and infra errors where you want .wrap_err("context") chains rather than typed variants.
  • ensure! macro: Defined in tn_types::error (and tn_executor::errors). Usage:
    use tn_types::ensure;
    ensure!(condition, DagError::InvalidEpoch { expected, received });
    
    This returns Err(e) if condition is false. Do NOT use anyhow::ensure! -- use the crate's own.
  • StoreError: Type alias for eyre::Report, used throughout storage.
  • No unwrap() in production code -- use expect("reason") only where the invariant is truly guaranteed and document why. In test code, unwrap() is acceptable.

Async patterns

TaskManager / TaskSpawner

Structured concurrency primitive defined in tn_types::task_manager.

  • TaskManager owns a FuturesUnordered<TaskHandle> and tracks critical/non-critical tasks
  • TaskSpawner is Clone, lives with components that need to spawn short-lived work
  • Critical tasks: when they exit, the manager breaks its join loop and triggers shutdown
  • Non-critical tasks: exit silently, other tasks continue
  • TaskManager::drop calls local_shutdown.notify() -- all spawned tasks get cancelled
  • Epoch-scoped managers are dropped at epoch boundaries to clean up all epoch tasks
  • Get a spawner: let spawner = task_manager.get_spawner();
  • Spawn: spawner.spawn_task("name", async { ... }) or spawner.spawn_critical_task("name", future)
  • Also implements reth_tasks::TaskSpawner for Reth integration

Notifier / Noticer (shutdown)

Defined in tn_types::notifier. Lightweight one-shot broadcast for shutdown signals.

  • Notifier is Clone -- hand it to things that need to trigger shutdown
  • Noticer is a Future that resolves when notifier.notify() is called
  • Noticer is NOT Clone -- each subscriber gets its own via notifier.subscribe()
  • Use in tokio::select!:
    let rx_shutdown = shutdown.subscribe();
    tokio::select! {
        _ = &rx_shutdown => { /* shutdown */ }
        msg = channel.recv() => { /* work */ }
    }
    
  • Check without awaiting: rx_shutdown.noticed() returns bool

ConsensusBus

Defined in crates/consensus/primary/src/consensus_bus.rs. Central channel container for consensus message flow. Two lifetimes:

  • App-lifetime (ConsensusBusApp): watch channels for round updates, sync status, recent blocks, epoch records. Persists across epochs.
  • Epoch-lifetime (ConsensusBusEpochInner): QueChannels for certificates, headers, digests, committed subdags. Recycled each epoch.

QueChannel / TnSender / TnReceiver

Defined in tn_types::sync. Abstraction over tokio channel types:

  • TnSender<T>: trait implemented by mpsc::Sender<T> and broadcast::Sender<T>
  • TnReceiver<T>: trait implemented by mpsc::Receiver<T> and broadcast::Receiver<T>
  • QueChannel: managed mpsc channel wrapper used in ConsensusBus, enforces single-subscriber
  • CHANNEL_CAPACITY: default 10,000
  • Broadcast recv() handles Lagged by logging and continuing (not returning None)

Future-as-struct pattern

Long-running services implement Future directly on their struct (e.g., TaskHandle). This avoids boxing and gives clear ownership semantics.

Concurrency

  • parking_lot for synchronous mutexes. NEVER hold a parking_lot lock across an .await point -- this will deadlock the tokio runtime.
  • dashmap for concurrent hash maps where read-heavy access patterns dominate.
  • tokio::sync::Mutex / tokio::sync::RwLock when you must hold a lock across .await.
  • Arc<Mutex<T>> (parking_lot) for shared state within a single async task tree.

Serialization

Three serialization formats, each with a specific purpose:

  1. BCS (bcs crate) -- consensus messages, network wire payload (before compression), general encode/decode. Functions: tn_types::encode(), tn_types::decode(), tn_types::try_decode(), tn_types::encode_into_buffer(). Deterministic and canonical.

  2. bincode (big-endian, fixint) -- DB keys ONLY. Functions: tn_types::encode_key(), tn_types::decode_key(), tn_types::try_decode_key(). Produces binary-sortable bytes required for ordered iteration in the storage layer. NEVER use for non-key data.

  3. snap (Snappy compression) -- network wire format wraps BCS-encoded bytes in snappy frames. Wire format: [4-byte uncompressed_len][4-byte compressed_len][compressed_data]. Implemented in crates/network-libp2p/src/codec.rs via TNCodec.

Cryptography

  • BLS12-381 via the blst crate (min-sig variant)
  • BlsSigner trait (tn_types::crypto): request_signature_direct(&self, msg) (sync, runs on blocking thread pool) and request_signature(&self, msg) (async wrapper). Also: public_key(&self) -> BlsPublicKey.
  • IntentMessage<T> wrapping: ALL signatures sign IntentMessage { intent, value } -- never raw data. The intent includes IntentScope, IntentVersion, and AppId to prevent cross-domain replay.
  • Intent scopes: ProofOfPossession, EpochBoundary, ConsensusDigest, SystemMessage
  • Proof of Possession: validators prove key ownership when joining the committee
  • secp256k1: used for Ethereum-compatible signing (validator rewards, etc.)
  • blake3: used for hashing in some contexts

Determinism rules

These are consensus-critical. Violations cause chain splits.

  • No HashMap/HashSet in consensus paths -- iteration order is non-deterministic. Use BTreeMap/BTreeSet or IndexMap (with consistent insertion order).
  • No floating point in consensus calculations
  • No SystemTime for consensus timestamps -- use the crate's now() function (tn_types::now) which returns TimestampSec
  • BCS for canonical encoding -- it produces deterministic byte output for the same input
  • No thread-dependent ordering in consensus logic

Storage

Database traits (tn_types::database_traits)

  • Database: top-level trait. Send + Sync + Clone + Unpin + 'static. Methods: open_table, read_txn, write_txn, get, insert, remove, iter, skip_to, reverse_iter, last_record, compact, persist.
  • DbTx: read transaction. get, contains_key.
  • DbTxMut: write transaction (extends DbTx). insert, remove, clear_table, commit.
  • Table trait: Key: KeyT, Value: ValueT, const NAME, const HINT: TableHint.
  • KeyT / ValueT: Serialize + DeserializeOwned + Send + Sync + Clone + Debug + 'static (KeyT also requires Ord).

TableHint

  • TableHint::Epoch -- cleared/recycled at epoch boundaries
  • TableHint::Kad -- kademlia DHT records, persisted across epochs
  • TableHint::Cache -- temporary cache data, can be cleared

tables! macro (crates/storage/src/lib.rs)

Generates table structs implementing the Table trait:

tables!(
    Certificates;CERTIFICATES_CF;TableHint::Epoch;<CertificateDigest, Certificate>,
    Payload;PAYLOAD_CF;TableHint::Epoch;<(BlockHash, WorkerId), PayloadToken>
);

Dual backend

  • redb: default persistent backend for consensus data
  • mdbx: optional via reth-libmdbx feature flag
  • CompositeDatabase composes backends based on TableHint
  • MemDatabase: in-memory implementation for tests

Networking

libp2p (v0.56)

The TNBehavior struct composes sub-behaviors using #[derive(NetworkBehaviour)]:

struct TNBehavior<C, DB> {
    peer_manager: PeerManager,  // MUST be first -- ban enforcement
    gossipsub: gossipsub::Behaviour,
    req_res: request_response::Behaviour<C>,
    kademlia: kad::Behaviour<KadStore<DB>>,
    stream: StreamBehavior,
}

Field order matters: NetworkBehaviour derive calls handle_established_*_connection in declaration order, short-circuiting on ConnectionDenied. peer_manager first means banned peers are rejected before other behaviors register them.

NetworkHandle / NetworkCommand

Command pattern: components send NetworkCommand variants through NetworkHandle (wraps an mpsc::Sender). The network event loop processes commands and emits NetworkEvents.

Wire format

BCS serialization + Snappy compression via TNCodec. Messages implement the TNMessage trait (adds peer_exchange_msg() interception at the network layer).

Testing

Conventions

  • proptest files: named *_props.rs (e.g., consensus_props.rs, economics_props.rs)
  • Unit test modules: inline #[cfg(test)] mod test { ... } or via #[path] attribute:
    #[cfg(test)]
    #[path = "tests/proposer_tests.rs"]
    mod proposer_tests;
    
  • Integration tests: in crates/*/tests/it/ directories
  • CommitteeFixture: builder pattern for test committees. From tn-test-utils-committee:
    let fixture = CommitteeFixture::builder(MemDatabase::default).randomize_ports(true).build();
    
  • test-utils feature flag: crates expose test helpers behind [features] test-utils = []. Dev-dependencies enable this: tn-types = { workspace = true, features = ["test-utils"] }.
  • Unused test deps: suppress unused_crate_dependencies warnings with use crate_name as _; at the crate root under #[cfg(test)]:
    #[cfg(test)]
    use tn_test_utils as _;
    
  • MemDatabase: in-memory DB for tests, avoids filesystem
  • tempfile: for tests needing real file paths

Test commands

cargo nextest run --workspace --no-fail-fast          # all unit tests
cargo nextest run -p tn-primary --no-fail-fast        # single crate
cargo nextest run -p e2e-tests --run-ignored ignored-only --all-features  # e2e

Code style

rustfmt.toml

  • imports_granularity = "Crate" -- merge imports per crate
  • use_field_init_shorthand = true -- Foo { name } not Foo { name: name }
  • reorder_imports = true -- alphabetical
  • wrap_comments = true, comment_width = 100
  • use_small_heuristics = "Max"

Tracing

Use string targets prefixed with tn:::

tracing::info!(target: "tn::consensus", round, "proposing header");
tracing::error!(target: "tn::tasks", "task failed: {err}");
tracing::warn!(target: "tn::network", peer_id = %id, "peer misbehaved");
tracing::debug!(target: "tn::execution", ?block_hash, "executing block");

Common targets: tn::tasks, tn::consensus, tn::execution, tn::network, tn::reth, tn::config, tn::observer, tn::generate_keys, tn::storage.

Documentation

  • Add //! module-level docs to every file
  • Use /// for public items (workspace lint warns on missing)
  • Doc comments on error variants in thiserror enums

Imports

  • Group: std, external crates, workspace crates (tn_*), crate-local
  • Use crate:: imports granularity within the crate
  • Avoid glob imports except in mod tests blocks and re-exports

Code Formatting

Run make fmt after writing or modifying code. Do not present code as complete without formatting.

Type Ordering in Files

Follow this strict ordering convention:

  1. use imports
  2. The file's primary type (matching the filename) — struct/enum + impl blocks
  3. Public auxiliary types that support the primary type
  4. Public traits related to the primary type
  5. Private helper types
  6. Private helper functions

Never add new types or traits above the file's primary type.

Doc Comments

Write doc comments for the intended audience of code maintainers and security researchers. Use proper punctuation, complete sentences, and natural human writing style.

  • Every public type, trait, and function must have a doc comment
  • Start with a concise summary line
  • Add detail paragraphs for complex behavior, constraints, safety requirements
  • Document panics, errors, and safety invariants
  • Use /// for item docs, //! for module docs

Example:

/// Validates a block's transaction list against consensus rules.
///
/// Returns an error if any transaction violates the current fork's
/// gas limits or signature requirements. Empty transaction lists
/// are valid per EIP-1559.
pub fn validate_transactions(block: &Block) -> Result<(), ValidationError>

Code Comments

Write concise code comments in all lowercase letters. Comments must remain valuable after the PR is merged — future readers only see the current code, not PR context.

Comment when:

  • Non-obvious behavior or edge cases
  • Performance trade-offs
  • Safety requirements (unsafe blocks must always be documented)
  • Limitations or gotchas
  • Why simpler alternatives don't work
  • Constraints and assumptions

Don't comment when:

  • Code is self-explanatory
  • Just restating the code in English
  • Describing what changed (PR context)
  • Stating the obvious

Comment style:

// hashmap provides o(1) symbol lookups during trace replay

// timeout set to 5s to match evm block processing limits

// we reset limits at task start because tokio reuses threads
// in spawn_blocking pool

Rules

These are hard constraints. Violating them causes bugs, test failures, or consensus splits.

  1. Never use HashMap/HashSet in consensus-critical paths. Iteration order is non-deterministic and will cause validators to disagree. Use BTreeMap/BTreeSet or IndexMap.

  2. Never hold a parking_lot lock across .await. This blocks the tokio worker thread and deadlocks the runtime. Use tokio::sync::Mutex if you must lock across await points.

  3. Always sign IntentMessage<T>, never raw data. Signatures without intent wrapping are vulnerable to cross-domain replay attacks.

  4. Use encode()/decode() (BCS) for data, encode_key()/decode_key() (bincode) for DB keys. Mixing them up breaks binary sort order in storage or produces non-deterministic serialization.

  5. Critical tasks must be spawned with spawn_critical_task. If a critical component (proposer, certifier, subscriber) exits silently via spawn_task, the node hangs instead of restarting the epoch.

  6. PeerManager must be the first field in TNBehavior. The NetworkBehaviour derive processes fields in order; PeerManager must deny banned connections before other behaviors register them.

  7. Every epoch-scoped resource must be owned by an epoch-scoped TaskManager. When the epoch ends and the manager is dropped, all tasks are cancelled via Notifier. Leaking tasks across epochs causes stale message processing.

  8. unused_must_use is deny. Every Result, MustUse return value must be handled. Use let _ = ... only when deliberately discarding is correct and add a comment why.

  9. Run cargo +nightly-2026-03-20 fmt before committing. The project uses nightly rustfmt features. Stable cargo fmt will produce different output.

  10. Add use crate_name as _; for test-only dependencies to suppress unused_crate_dependencies warnings. Place at crate root under #[cfg(test)].

Anti-patterns

Common mistakes to avoid in this codebase:

  • Using anyhow instead of eyre -- this project uses eyre for error context. Do not add anyhow as a dependency.

  • Creating new channel types instead of using ConsensusBus -- if you need a channel between consensus components, add it to ConsensusBus rather than threading channels through constructors. The bus exists specifically to centralize message flow.

  • Using tokio::sync::broadcast directly without the TnReceiver trait -- the trait's recv() implementation handles Lagged errors by logging and continuing. Raw broadcast receivers converted via .ok() silently drop Lagged as None, which kills consumer loops.

  • Adding async-trait for new traits -- Rust now supports async fn in traits natively. The codebase uses the desugared pattern (fn foo(&self) -> impl Future<Output = T> + Send) for traits that need Send bounds. async-trait is only kept for libp2p Codec compatibility.

  • Putting crate-specific types in tn-types -- tn-types is the shared foundation. Types specific to one domain (e.g., worker-only message types) belong in that crate.

  • Using std::collections::HashMap where BTreeMap is needed -- even outside consensus hot paths, prefer deterministic collections when the data might flow into consensus later.

  • Forgetting #[cfg(test)] on test module path attributes -- the #[path = ...] attribute must be paired with #[cfg(test)] to avoid compiling test code in release builds.

  • Using bincode for anything other than DB keys -- bincode is configured with with_big_endian().with_fixint_encoding() specifically for binary-sortable DB keys. For everything else, use BCS via encode()/decode().

  • Spawning long-running tasks without a TaskManager -- bare tokio::spawn creates orphaned tasks that survive epoch boundaries. Always spawn through a TaskManager or TaskSpawner.

Install via CLI
npx skills add https://github.com/grantkee/claude-extensions --skill tn-rust-skills
Repository Details
star Stars 2
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator