Protocol Architecture
NOXY L0 internals: transaction envelope, block structure, cryptographic primitives, consensus modes, and validator lifecycle.
Protocol Architecture
NOXY L0 is a post-quantum blockchain built around a canonical transaction envelope, a Merkle-committed block ledger, and a pluggable consensus layer. This page documents what is implemented in the Rust codebase.
Hashing
All consensus-critical digests use BLAKE3 with domain separation:
NOXY-L0/v0.1/<domain>\0 || data
Examples of domain strings used in the codebase:
| Domain | Used for |
|--------|----------|
| tx-signing | Transaction signing digest |
| block-header | Block header hash |
| tx-root | Merkle root over transaction hashes |
| state-root | Merkle root over state key-value leaves |
| event-root | Merkle root over event hashes |
| evidence-root | Merkle root over evidence sidecar hashes |
| validator-set | Validator set hash commitment |
Transaction envelope
Every user action is submitted as a TransactionEnvelope:
TransactionEnvelope {
version: u16 // wire version
chain_id: ChainId // must match node's chain_id
account_id: AccountId // 32-byte account identifier
nonce: u64 // per-account sequence number
fee: {
fee_asset_id: AssetId
max_fee: u64
witness_byte_fee_limit: u64
}
payload: TransactionPayload
auth: {
key_id: KeyId
algorithm_id: SignatureAlgorithmId
signature: bytes // ML-DSA-44 signature
optional_hybrid_signature: bytes? // Ed25519, for validator consensus ops
}
}
The signing digest is computed over the canonical encoding of the envelope body (everything except auth.signature).
Transaction types
| Type | Description |
|------|-------------|
| CreateAccount | Create a new account with an initial ML-DSA-44 key |
| RegisterKey | Register an additional key on an existing account |
| Transfer | Transfer a fungible asset between accounts |
| RegisterValidator | Register a validator record bound to an operator account |
| Batch | Sequence of BatchAction items in one atomic envelope |
| ActivateValidator | (Admin) Schedule validator activation at a given epoch |
| DeactivateValidator | (Admin) Schedule validator deactivation |
| SubmitEvidence | Submit a DoubleSignEvidenceSidecar to trigger jailing |
| RegisterServiceDefinition | Register a service definition |
| RegisterServiceChain | Register a service chain |
| SubmitCheckpoint | Submit a checkpoint |
| SubmitMessageEnvelope | Cross-chain message envelope |
Batch is the composable variant: it wraps a Vec<BatchAction> where each action is one of RegisterKey, RegisterValidator, Transfer, ActivateValidator, DeactivateValidator, SubmitEvidence, and others.
Block structure
Block {
header: BlockHeader {
height: u64
timestamp_ms: u64
prev_block_hash: Hash32
tx_root: Hash32 // Merkle root over tx hashes
state_root: Hash32 // Merkle root after applying this block
event_root: Hash32 // Merkle root over events emitted
evidence_root: Hash32 // Merkle root over evidence sidecars
validator_set_hash: Hash32 // Hash of active validator set
consensus_cert_meta: {
kind: ConsensusCertificateKind
height: u64
view: u32
validator_set_hash: Hash32
signer_bitmap: bytes
}
}
body: BlockBody {
transactions: Vec<bytes> // canonically-encoded tx envelopes
}
}
The first block at height 1 is built on genesis (version 0, height 0). prev_block_hash at height 1 is the genesis hash.
Cryptography
ML-DSA-44
Account keys are ML-DSA-44 (FIPS 204). Transaction envelopes are signed with the account's primary key. Additional keys can be registered via RegisterKey. ML-DSA-44 public keys are approximately 1312 bytes; signatures approximately 2420 bytes.
Strict hybrid
Validator consensus votes in the BFT path carry both an ML-DSA-44 signature and an Ed25519 signature. Verification requires both to pass — there is no fallback to one signature alone. A ConsensusVote that carries only one valid signature is rejected.
QUIC + TLS 1.3
Peer-to-peer transport uses QUIC (via quinn) with rustls for TLS 1.3 and rcgen for ephemeral peer certificates. The PeerHello handshake binds chain_id, genesis_hash, node_id, and a timestamp that is clock-skew validated against max_clock_skew_ms.
Consensus modes
The node selects a consensus mode at startup via [consensus] mode in config.
single_validator
One producer owns the validator set. The block producer loop:
- Pulls pending transactions from the mempool.
- Assembles a block (up to
max_block_txs/max_block_bytes). - Executes transactions and derives the state root.
- Signs and commits the block.
- Waits for
block_time_msbefore the next round.
No networking or peer communication. This mode is the default and is suitable for local development and integration testing.
bft (BFT)
A HotStuff/Jolteon BFT consensus runtime with up to 4 validators connected over QUIC.
Roles per round:
- A leader proposes a block for the current (height, view).
- All validators broadcast a
Votemessage (both ML-DSA-44 + Ed25519 signatures). - The leader aggregates votes into a
CommitCertificatewhen ≥ ⌊2n/3⌋ + 1 distinct validators have voted. - The certificate is attached to the finalized block as
CommitCertificateSidecar. - All validators commit the finalized block and advance to the next height.
Pacemaker: view timeout starts at consensus_base_timeout_ms and doubles on each view change.
Double-sign detection: each node runs an EvidenceDetector that deduplicates divergent votes by (height, view, validator_id). On first detection it mints a DoubleSignEvidenceSidecar and submits a SubmitEvidence transaction to the local mempool.
Validator lifecycle
Candidate → Active → Inactive | Jailed | Exiting → Exited
| Status | Description |
|--------|-------------|
| Candidate | Registered but not yet scheduled for activation |
| Active | In the current active set, producing and voting on blocks |
| Inactive | Deactivated, not in active set |
| Jailed | Jailed due to double-sign evidence; excluded from active set |
| Exiting | Scheduled for removal at active_until_epoch |
| Exited | Fully removed from the validator set |
A validator record binds:
operator_account_id— the account that owns lifecycle actionspq_consensus_key_id— ML-DSA-44 key for BFT votesclassical_consensus_key_id— Ed25519 key for hybrid votesvoting_power— weight in the quorum calculationactive_from_epoch/active_until_epoch— epoch-bounded membership
The active set at each height is derived from committed state, not from local config.
Epoch transitions
Epochs advance deterministically based on block height. At each epoch boundary:
- The execution layer derives a new active set from the current validator records.
- Validators scheduled for activation at this epoch move from
Candidate→Active. - Validators with a matching
active_until_epochmove toExitingorInactive. - Jailed validators (with committed
DoubleSignEvidence) are excluded from the new active set.
Evidence
Double-sign evidence captures a validator equivocating on the same (height, view):
DoubleSignEvidenceSidecar {
accused_validator_id: ValidatorId
vote_a: ConsensusVote // first observed vote
vote_b: ConsensusVote // conflicting vote at same (height, view)
}
The evidence sidecar is broadcast over a dedicated Evidence QUIC channel (channel tag 0x0050) before the corresponding SubmitEvidence transaction is admitted to the mempool. A per-node in-memory cache (EvidenceCache) buffers evidence by evidence_hash so the execution layer can verify the sidecar during transaction admission without a DB round-trip.
On commitment of a block containing a valid SubmitEvidence transaction:
- The accused validator's
double_sign_evidence_countis incremented. - The validator's status moves to
Jailed. - The next epoch excludes the jailed validator from the active set.