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 viacargo +nightly-2026-03-20 fmt) - Test runner:
cargo nextest(parallel execution) - Workspace lints (in root
Cargo.toml):unused_must_use = "deny"-- allResultand#[must_use]values must be handledmissing_docs = "warn"-- add doc comments to public itemsunreachable_pub = "warn"-- preferpub(crate)overpubfor internal itemsunused_crate_dependencies = "warn"-- every dep must be usedrust_2018_idioms = "deny"-- modern Rust style enforced
Error handling
- Domain errors: Use
thiserrorwith 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(viaeyre::Report) for storage and infra errors where you want.wrap_err("context")chains rather than typed variants. ensure!macro: Defined intn_types::error(andtn_executor::errors). Usage:
This returnsuse tn_types::ensure; ensure!(condition, DagError::InvalidEpoch { expected, received });Err(e)if condition is false. Do NOT useanyhow::ensure!-- use the crate's own.StoreError: Type alias foreyre::Report, used throughout storage.- No
unwrap()in production code -- useexpect("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.
TaskManagerowns aFuturesUnordered<TaskHandle>and tracks critical/non-critical tasksTaskSpawnerisClone, 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::dropcallslocal_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 { ... })orspawner.spawn_critical_task("name", future) - Also implements
reth_tasks::TaskSpawnerfor Reth integration
Notifier / Noticer (shutdown)
Defined in tn_types::notifier. Lightweight one-shot broadcast for shutdown signals.
NotifierisClone-- hand it to things that need to trigger shutdownNoticeris aFuturethat resolves whennotifier.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()returnsbool
ConsensusBus
Defined in crates/consensus/primary/src/consensus_bus.rs. Central channel container for
consensus message flow. Two lifetimes:
- App-lifetime (
ConsensusBusApp):watchchannels 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 bympsc::Sender<T>andbroadcast::Sender<T>TnReceiver<T>: trait implemented bympsc::Receiver<T>andbroadcast::Receiver<T>QueChannel: managed mpsc channel wrapper used in ConsensusBus, enforces single-subscriberCHANNEL_CAPACITY: default 10,000- Broadcast
recv()handlesLaggedby logging and continuing (not returningNone)
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_lotfor synchronous mutexes. NEVER hold aparking_lotlock across an.awaitpoint -- this will deadlock the tokio runtime.dashmapfor concurrent hash maps where read-heavy access patterns dominate.tokio::sync::Mutex/tokio::sync::RwLockwhen 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 (
bcscrate) -- 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 incrates/network-libp2p/src/codec.rsviaTNCodec.
Cryptography
- BLS12-381 via the
blstcrate (min-sig variant) BlsSignertrait (tn_types::crypto):request_signature_direct(&self, msg)(sync, runs on blocking thread pool) andrequest_signature(&self, msg)(async wrapper). Also:public_key(&self) -> BlsPublicKey.IntentMessage<T>wrapping: ALL signatures signIntentMessage { intent, value }-- never raw data. The intent includesIntentScope,IntentVersion, andAppIdto 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/HashSetin consensus paths -- iteration order is non-deterministic. UseBTreeMap/BTreeSetorIndexMap(with consistent insertion order). - No floating point in consensus calculations
- No
SystemTimefor consensus timestamps -- use the crate'snow()function (tn_types::now) which returnsTimestampSec - 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 (extendsDbTx).insert,remove,clear_table,commit.Tabletrait:Key: KeyT,Value: ValueT,const NAME,const HINT: TableHint.KeyT/ValueT:Serialize + DeserializeOwned + Send + Sync + Clone + Debug + 'static(KeyT also requiresOrd).
TableHint
TableHint::Epoch-- cleared/recycled at epoch boundariesTableHint::Kad-- kademlia DHT records, persisted across epochsTableHint::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-libmdbxfeature flag CompositeDatabasecomposes backends based onTableHintMemDatabase: 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. Fromtn-test-utils-committee:let fixture = CommitteeFixture::builder(MemDatabase::default).randomize_ports(true).build();test-utilsfeature 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_dependencieswarnings withuse crate_name as _;at the crate root under#[cfg(test)]:#[cfg(test)] use tn_test_utils as _; MemDatabase: in-memory DB for tests, avoids filesystemtempfile: 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 crateuse_field_init_shorthand = true--Foo { name }notFoo { name: name }reorder_imports = true-- alphabeticalwrap_comments = true,comment_width = 100use_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 testsblocks 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:
useimports- 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:
/// 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.
Never use
HashMap/HashSetin consensus-critical paths. Iteration order is non-deterministic and will cause validators to disagree. UseBTreeMap/BTreeSetorIndexMap.Never hold a
parking_lotlock across.await. This blocks the tokio worker thread and deadlocks the runtime. Usetokio::sync::Mutexif 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 viaspawn_task, the node hangs instead of restarting the epoch.PeerManagermust be the first field inTNBehavior. TheNetworkBehaviourderive 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 viaNotifier. Leaking tasks across epochs causes stale message processing.unused_must_useis deny. EveryResult,MustUsereturn value must be handled. Uselet _ = ...only when deliberately discarding is correct and add a comment why.Run
cargo +nightly-2026-03-20 fmtbefore committing. The project uses nightly rustfmt features. Stablecargo fmtwill produce different output.Add
use crate_name as _;for test-only dependencies to suppressunused_crate_dependencieswarnings. Place at crate root under#[cfg(test)].
Anti-patterns
Common mistakes to avoid in this codebase:
Using
anyhowinstead ofeyre-- this project useseyrefor error context. Do not addanyhowas a dependency.Creating new channel types instead of using
ConsensusBus-- if you need a channel between consensus components, add it toConsensusBusrather than threading channels through constructors. The bus exists specifically to centralize message flow.Using
tokio::sync::broadcastdirectly without theTnReceivertrait -- the trait'srecv()implementation handlesLaggederrors by logging and continuing. Raw broadcast receivers converted via.ok()silently dropLaggedasNone, which kills consumer loops.Adding
async-traitfor 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 needSendbounds.async-traitis only kept for libp2pCodeccompatibility.Putting crate-specific types in
tn-types--tn-typesis the shared foundation. Types specific to one domain (e.g., worker-only message types) belong in that crate.Using
std::collections::HashMapwhereBTreeMapis 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
bincodefor anything other than DB keys -- bincode is configured withwith_big_endian().with_fixint_encoding()specifically for binary-sortable DB keys. For everything else, use BCS viaencode()/decode().Spawning long-running tasks without a
TaskManager-- baretokio::spawncreates orphaned tasks that survive epoch boundaries. Always spawn through aTaskManagerorTaskSpawner.