| 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:
- Workers — build transaction batches, seal them via quorum, gossip batch digests
- Primary — propose DAG headers referencing batch digests, collect votes, form certificates
- Bullshark consensus — order the certificate DAG into a committed sequence
- Executor — subscribe to committed subdags, build
ConsensusOutput
- 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
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.
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:
-
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.
-
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.
-
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,
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
cargo nextest run -p tn-primary --no-fail-fast
cargo nextest run -p e2e-tests --run-ignored ignored-only --all-features
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:
use imports
- The file's primary type (matching the filename) — struct/enum + impl blocks
- Public auxiliary types that support the primary type
- Public traits related to the primary type
- Private helper types
- 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:
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:
Rules
These are hard constraints. Violating them causes bugs, test failures, or consensus splits.
-
Never use HashMap/HashSet in consensus-critical paths. Iteration order is
non-deterministic and will cause validators to disagree. Use BTreeMap/BTreeSet or
IndexMap.
-
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.
-
Always sign IntentMessage<T>, never raw data. Signatures without intent wrapping
are vulnerable to cross-domain replay attacks.
-
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.
-
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.
-
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.
-
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.
-
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.
-
Run cargo +nightly-2026-03-20 fmt before committing. The project uses nightly rustfmt
features. Stable cargo fmt will produce different output.
-
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.