Under the Hood of NKN

hero
Follow-up to "NKN in a Nutshell"

The nutshell sketched five ideas. This one opens the protobuf, follows a packet through a verifiable random function, and watches a million nodes agree without ever meeting.

fig0-signature-chain
Fig. 0 — The signature chain. A packet accumulates one signed element per hop; relay hops sign with a verifiable random function so the result is unforgeable, unpredictable, and unique. This single structure powers both Proof of Relay and leader election. (Original diagram, CC BY 4.0.)

In August 2018, “NKN in a Nutshell” introduced five ideas — a decentralized transmission network, Chord-based routing, Proof of Relay with a signature chain, MOCA consensus, and an incentive system — and explained each one for a reader without a cryptography degree. This article is the other half of that promise: the version for people who do want the protobuf, the serialization order, the VRF, and the state machine.

Nothing here contradicts the nutshell. The shape of the system is the same. But several of those five ideas have grown sharper teeth since 2018 — the signature chain now signs with a verifiable random function rather than a plain digital signature, the session layer became a documented multipath transport, and the whole thing settled into a stable V2 line that is, in the words of the repository, “Production (Stable and Feature-Complete).” The goal below is to connect each high-level idea to the concrete structure that implements it, so that when you open github.com/nknorg/nkn you already know what you are looking at.

How to read this
Each section names a nutshell concept, then drops one level into the protocol. Code and protobuf are quoted from the public docs and the Go full node. Where a value is governed by a block height or config, it is called out — NKN changes rules by activation height rather than hard forks of opinion.

01 — Topology

Two roles, one overlay

The nutshell described clients and nodes. The distinction is worth restating precisely, because it maps directly onto separate codebases. A client is an endpoint: it originates and consumes data, holds a key pair, and never relays anyone else’s traffic. A node is a full participant: it relays packets for the network, takes part in consensus, and earns rewards. Clients live in the SDKs; nodes live in the full node.

Repository Role What it does
nknorg/nkn Full node Go implementation. Relays data, runs MOCA consensus, maintains the blockchain. Ships nknd (the node) and nknc (control CLI).
nknorg/nnet Network stack The overlay-network library underneath the node — generic enough to build other decentralized systems on.
nkn-sdk-go / -js / -java Client Send and receive data, create wallets, sign transactions. No relaying, no consensus.
ncp-go / ncp-js Session The reusable session protocol (NCP) used by the SDKs for reliable, multipath streams.

The core invariant is small and worth memorizing: every node is connected to and aware of only a handful of others — its neighbors. No node holds a global view. Routing, consensus, and gossip all operate purely through neighbor relationships, which is exactly what lets the network claim scalability to “billions of nodes within seconds.” The blockchain sits underneath as the settlement and identity layer; the data plane rides on top of it.

fig1-network-planes

Fig. 1 — Planes of the network. Original diagram, CC BY 4.0. NKN wordmark © NKN Foundation, used for identification.

If you only ever run a node, the whole experience is two binaries. make produces nknd and nknc; nknd reads config.json, joins the mainnet by default, and writes ChainDB/ and Log/. One operational detail the README is emphatic about: each node needs a unique wallet, and you should set a beneficiaryAddr pointing at a cold wallet so mined rewards never sit on the hot node key.

02 — Identity

Addresses with the key baked in

The nutshell said each node gets a unique NKN address from its keys. The client scheme is where this gets elegant. An NKN client address is just a string:

client NKN address = arbitrary-identifier.client-public-key
client DHT address = sha256( client NKN address )

Read it like a URL. The trailing public key is the root identity — like a registered domain — and the identifier prefix in front is yours to choose, like a subdomain. One key pair can therefore mint as many addresses as you like (alice.<pk>, backup.<pk>, __0__.<pk> …), all settling to the same account. The DHT address that routing actually uses is simply the SHA-256 of that string, computed locally by whoever needs it.

Three properties fall out of this, and they are the reason the scheme exists:

  • Permanence. The address is derived from a key, not a location, so a client keeps the same address across networks and NATs — reachable wherever it connects from.
  • Collision resistance. Because the public key is part of the address, nobody else can occupy it or intercept packets destined for it.
  • Free end-to-end encryption. Knowing a peer’s address is knowing its public key. There is no separate key exchange to MITM — encryption keys are present the moment you have someone’s address.

fig2-address-derivation

Fig. 2 — From key pair to routable address. Original diagram, CC BY 4.0.

Nodes get their identity on-chain instead, through a GENERATE_ID transaction that binds a 33-byte compressed public key to a node ID (with a registration fee — 10 NKN since block 2,570,000). Wallet addresses, meanwhile, follow the more familiar pattern of a 20-byte script hash with a Base58 check encoding and the human-readable NKN… prefix you see in the CLI.

03 — Routing

Chord, but verifiable

The nutshell’s ring picture holds: nodes sit on a circular key space ordered by DHT address, and a packet hops clockwise toward the node closest to the destination. The mechanism behind “finds the closest neighbor in intervals of 2ⁿ” is the Chord finger table: each node keeps pointers to nodes at exponentially increasing distances around the ring, so each hop can at least halve the remaining distance. The result is delivery in O(log N) hops with each node storing only ~log N pointers.

NKN’s contribution on top of textbook Chord is the word verifiable. Because positions are deterministic hashes of public keys and the ring is known, the choice of next hop is not a matter of trust — given the set of nodes, the route is reproducible and checkable. That property is not just elegant; it is load-bearing. It is what later lets a signature chain prove that a packet actually traversed the path it claims to have traversed, and it is why “distance” is over address space rather than geography: address-space routing stays well-defined even as nodes join and leave continuously.

Why not geographic routing?
Physical proximity changes every time the membership churns; a hash-space ring does not. Deterministic positions give you a route you can recompute and verify after the fact — the precondition for paying nodes for relay work they can’t fake.

For reliability, the SDK doesn’t lean on a single path. The multiclient trick creates several sub-clients off the same key by varying the identifier prefix — __0__.id.pk, __1__.id.pk, __2__.id.pk — each of which lands at a different DHT position and therefore takes a different route through the ring. Send across all of them and an arbitrary failure of any one path costs you nothing but a little redundancy. This is the foundation the session layer builds on in §06.

04 — Proof of Relay

The signature chain, opened up

This is the centerpiece, and it is where the protocol has moved furthest beyond the nutshell. The idea is unchanged — every hop signs the packet, building a chain of signatures that nobody can forge without holding every private key on the route. What’s new is how the relay hops sign. Here is the actual structure, encoded in protobuf at pb/sigchain.proto:

enum SigAlgo { SIGNATURE = 0; VRF = 1; }

message SigChainElem {
  bytes   id          = 1;   // DHT address of this client/node
  bytes   next_pubkey = 2;   // pins the next hop in the chain
  bool    mining      = 3;   // "consider me as block proposer"
  bytes   signature   = 4;
  SigAlgo sig_algo    = 5;
  bytes   vrf         = 6;   // VRF output, relay hops only
  bytes   proof       = 7;   // VRF proof, relay hops only
}

message SigChain {
  uint32 nonce      = 1;
  uint32 data_size  = 2;
  bytes  block_hash = 3;  // recent block (height − SigChainBlockDelay)
  bytes  src_id     = 4;
  bytes  src_pubkey = 5;
  bytes  dest_id    = 6;
  bytes  dest_pubkey = 7;
  repeated SigChainElem elems = 8;
}

The shape tells the story. A chain is some immutable metadata about the transfer (who, to whom, how big, anchored to a recent block_hash) followed by an ordered list of elems, one per participant. The first element is signed by the sender, the last by the receiver, and everyone in between is a relay node.

Two signing modes — and why VRF

Look at sig_algo. The endpoints (first and last elements) use plain SIGNATURE: a normal Ed25519 signature. Every relay element in the middle uses VRF — a Verifiable Random Function. This is the upgrade. A VRF takes a key and an input and produces two things: an output that looks random, and a proof that lets anyone holding the public key confirm the output was computed correctly. NKN runs the VRF over the chain’s block_hash.

Why go to this trouble instead of a plain signature at every hop? Because the relay signatures do double duty. They must (a) prove relay work, and (b) later serve as the source of randomness that elects a block proposer. A VRF gives all three properties that job needs at once:

  • Verifiable — anyone can check the output against the node’s public key, so the proof of relay is publicly auditable.
  • Unique & deterministic — a given key and input yield exactly one output; a node can’t grind for a favorable “signature.”
  • Unpredictable — until the node actually computes it, nobody (including the node) can predict the value.

We use a verifiable random function to compute the “signature” in the signature chain, because it provides both verifiability and uniqueness. In addition to the output it also produces a proof needed to verify it.

— paraphrased from nkn.org, “Powering the Shared Internet by Blockchain”

How a hop signs

The chaining rule is exact. For a relay element using VRF, the signature field is:

signature = sha256( prev_signature ‖ serialized_elem ‖ elem_vrf )

For the endpoint elements using plain signature mode, it is instead the Ed25519 signature of prev_signature ‖ serialized_elem. In the very first element, “previous signature” is defined as the serialized chain metadata — so the head of the chain is bound to who-sent-what-to-whom, and every subsequent link folds in the one before it. Change any byte upstream and every downstream signature breaks. The serialization that gets signed is deliberately minimal and ordered:

// unsigned sigchain element
WriteVarBytes(id) + WriteVarBytes(next_pubkey) + WriteBool(mining)

// unsigned sigchain metadata
WriteUint32(nonce) + WriteUint32(data_size) + WriteVarBytes(block_hash)
  + WriteVarBytes(src_id)  + WriteVarBytes(src_pubkey)
  + WriteVarBytes(dest_id) + WriteVarBytes(dest_pubkey)

Note next_pubkey: each element names the public key of the next signer, so the chain isn’t just a pile of signatures — it’s a linked list with each link committing to the identity of the one after it. A relay can’t be silently inserted or reordered.

fig3-relay-element

Fig. 3 — Anatomy of one relay element. Original diagram, CC BY 4.0.

From chain to consensus: random sampling

Not every packet’s chain hits the blockchain — that wouldn’t scale. Instead, as the README puts it, “a small and fixed portion of the packets will be randomly selected as proof,” and the selection itself is verifiable and uncontrollable thanks to the VRF values inside the chains. A node that wants to be paid (and to be eligible as a block proposer) submits its completed chain as a SIG_CHAIN_TXN:

message SigChainTxn {
  bytes sig_chain = 1;   // serialized signature chain
  bytes submitter = 2;
}

That transaction is the bridge from the data plane to the ledger: it converts “I relayed this” into something the chain can verify, reward, and — as we’ll see next — use as a fair coin to pick who writes the next block.

05 — Consensus

MOCA: agreement by majority of neighbors

The nutshell’s “become a Giants fan if your neighbors are” analogy is exactly right, and it has a precise pedigree. MOCA — Major-vote Cellular Automata — descends from the zero-temperature Ising model with spontaneous magnetization, the same physics that describes how magnetic domains align. Each node holds a binary state for a given decision (accept/reject this block), looks only at its neighbors, and flips to the majority. Run that local rule across the graph and global agreement emerges without any node ever seeing the whole network.

The documented procedure for a single binary decision, identified by a topic id, is just three event-driven steps:

  1. On the first message seen for a topic id, compute your own initial state and send it to your neighbors.
  2. On every message, record the sender’s state for that topic.
  3. If more than half of the neighbors who count you as a neighbor hold the opposite state, switch to the majority state and broadcast the change.

The process for a topic simply ends a fixed time after the first message arrives. The whitepaper carries the convergence proof; the practical claim is the striking one: agreement among an enormous number of nodes in a handful of neighbor-only rounds.

fig4-moca-consensus

Fig. 4 — A vote spreading by local majority (Ising-style convergence). Original diagram, CC BY 4.0. Stylized after the Ising-model consensus simulation in nknorg/nkn-simulation.

Two design choices distinguish this from the eventual-consistency posture of many chains. First, consensus runs for every block, so the chain doesn’t fork and then reconcile — it reaches a single agreed block or none, giving strong consistency. Second, the dominant time cost isn’t the voting; it’s the initial gossip that hands the candidate block to all participants, which is O(log N) and shrinkable toward O(log N / log k) by giving nodes more neighbors, Kademlia-style.

Who proposes the block? The lowest last signature

Here is where the signature chain pays off a second time. To pick a proposer, the network selects the signature chain whose last signature is the smallest value among chains on secure paths. Because each last signature is a VRF output that can’t be predicted or controlled without owning every key on the path, picking the minimum is — provably — an unpredictable, uncontrollable, yet fully verifiable random draw. It is leader election with no separate lottery and no stake to grind.

Broadcasting every chain just to find a minimum would be wasteful, so nodes only announce a chain if its last signature falls under a threshold t. With L chains and signatures uniform on (0,1], the chance that none beats the threshold is (1 − t)^L; requiring that to stay below a small ε and minimizing candidate chatter gives a clean closed form:

fig-threshold-formula

t = −log(ε) ⁄ L — L estimated from the average number of signature chains in recent blocks.

Once the network agrees (via the same MOCA procedure) on which chain won, the leader is read out of it deterministically: with L relay nodes labeled 0…L−1 and last signature S, the proposer is the node at index S mod L. The leader proposes a block; nodes then run MOCA again on the block’s hash — and crucially, if a node sees two different blocks signed by the same leader, it votes for neither, which is how equivocation is punished.

Pipelined epochs

Leader selection and block creation don’t run end to end; they’re pipelined one epoch apart. Leader selection for epoch n runs in parallel with block creation for epoch n−1. Each phase is a small state machine — listening → waiting-for-candidates → candidate-consensus for selection, and listening → block-consensus for creation — with fixed time budgets per state. The overlap is what keeps block time low while still reaching full agreement on each block.

06 — Transport

NCP: a reliable stream over an unreliable overlay

The nutshell never mentioned this layer, but it’s where day-to-day SDK reliability comes from. NCP (the session protocol, with implementations in ncp-go and ncp-js) turns NKN’s best-effort packet delivery into ordered, reliable, optionally-streaming sessions — TCP-like guarantees, but built deliberately for a multipath overlay.

A session is established with a handshake carried in a new SESSION payload type. The dialer sends a handshake packet advertising the identifier prefixes it will use (["", "__0__", "__1__", …]), its receive window size, and its MTU; the accepter replies with its own, and both sides converge on the minimums:

connections = min( len(local prefixes), len(remote prefixes) )
send_window = min( local send window, remote receive window )
send_mtu    = min( local send MTU,    remote receive MTU )

Each prefix pair becomes a virtual connection — an independent path through the ring. The session keeps one byte-oriented sliding window, but each connection maintains its own packet-level send window and its own retransmission timeout, because different overlay paths can have wildly different latency. When the send buffer flushes, exactly one available connection picks up the job; a timed-out packet goes into a shared resend queue that any connection can drain.

The detail worth stealing
NKN deliberately does not reuse TCP’s Jacobson/Karels RTO estimator. Because every hop is already a TCP connection with its own flow and congestion control, that estimator over-fired “timeouts.” NCP instead targets an RTO of roughly 3× the measured RTT with a smoothing curve.

The RTO update on each acknowledged packet is:

rto = rto + tanh( (3·rtt − rto) / 1000ms ) · 100ms

The tanh is the clever part: when the gap between the current RTO and the 3×RTT target is small, the correction is near-linear and gentle; when the gap is large, tanh saturates and limits how far a single outlier RTT can yank the estimate. The per-connection window grows and shrinks exponentially too — not for congestion control (the underlying TCP hops already do that) but to stop the scheduler from piling work onto a path that’s currently slow, protecting overall session throughput.

Stream vs. non-stream mode is fixed at both ends, not negotiated: stream mode may split or coalesce writes across packets (no length limit), while non-stream mode sends each write as exactly one packet (so writes can’t exceed the MTU). Acks are aggregated greedily into ranges and piggyback a bytes_read counter, which is how the sender’s window learns it can slide forward.

fig5-multipath-session

Fig. 5 — One session, many paths. Original diagram, CC BY 4.0. One logical session is striped across independent paths; the scheduler favors the responsive ones.

07 — Ledger

What you can actually put on chain

A transaction is an UnsignedTx (a typed Payload, a random Nonce, a Fee in Fixed64 units of 10⁻⁸ NKN, and attributes) wrapped with signature Programs. Ten payload types exist; here are the ones a developer reaches for, beyond plain transfers:

Type What it’s for Notable fields / rules
SIG_CHAIN_TXN Submit a relay proof The Proof-of-Relay bridge from §04; carries the serialized chain + submitter.
GENERATE_ID Mint a node identity 33-byte pubkey → node ID; 10 NKN fee since height 2,570,000; rate-limited.
REGISTER_NAME Human-readable names ^[A-Za-z0-9][A-Za-z0-9-_.+]{2,254}$; min 10 NKN; ~1-year default duration.
TRANSFER_NAME / DELETE_NAME Manage names Only the current owner; deleting frees the name for re-registration.
SUBSCRIBE / UNSUBSCRIBE Pub/Sub topics Duration up to 400,000 blocks; meta ≤ 1,024 chars; buckets ≤ 1,000. Powers group messaging (e.g. D-Chat).
NANO_PAY Off-chain micropayments Payment-channel style with dual expiration; ideal for paying relays per-packet without flooding the chain.
COINBASE Mining reward Auto-generated per block; no signature; reward by height schedule.

Two things to internalize. First, names, pub/sub, and micropayments are first-class protocol features, not afterthoughts — a subscription is literally a transaction type, which is why decentralized chat can exist with no servers. Second, validation is height-dependent by design: regexes, fee minimums, durations, and version rules change at named block heights rather than via contentious forks. When you read the validator, always read it as “what is true at this height.”

08 — Incentives & building

Where the tokens come from — and where you come in

The nutshell named two rewards; now you can see the exact rails. Relay reward flows through micropayments: senders pay, and NANO_PAY channels let that payment be split among the relays that actually carried the data without a chain write per packet. Mining reward is the COINBASE transaction minted for the block — and the node that earns it is the leader chosen by the lowest-last-signature draw from §05. Both rewards are ultimately anchored to verifiable relay work, which is the whole point: the proof of work is useful work.

The reason proposer probability tracks relay volume is mechanical, not policy: more packets relayed on secure paths means more signature chains you contributed to, which means more chances your chain holds the minimum last signature. Useful work and consensus weight are the same quantity.

Getting your hands on it

  • Run a node. Grab a release or docker pull nknorg/nkn, or build with make for nknd/nknc. Mind the unique-wallet rule, set a beneficiaryAddr cold wallet, and open the configured ports (30001–30005) or rely on UPnP/NAT-PMP auto-forwarding.
  • Write a client. Pick an SDK — nkn-sdk-go, nkn-sdk-js, nkn-java-sdk, or the gomobile build for iOS/Android. You get addresses, end-to-end encryption, and NCP sessions essentially for free.
  • Build your own overlay. The network stack, nnet, is factored out specifically so you can build other decentralized systems on the same Chord-DHT-plus-gossip substrate.
  • Tunnel real traffic. nConnect shows the data plane carrying ordinary TCP/UDP — remote desktop, NAS access — over the network, optionally accelerated through Tuna.

The same five ideas, now load-bearing

“NKN in a Nutshell” was right about the architecture, and almost nothing in it has been overturned. What’s changed is that each idea now has a precise mechanism you can audit. The decentralized transmission network is two role-specific codebases over the nnet overlay. Chord routing is verifiable because positions are key-derived hashes. Proof of Relay is a protobuf signature chain whose relay hops sign with a VRF, making the proof simultaneously a fair coin for leader election. MOCA is the zero-temperature Ising model expressed as a three-step neighbor-majority rule that reaches strong consistency every block. And the incentive system is just the visible surface of all of the above — coinbase to the lowest-signature leader, nano-pay channels to the relays.

If the nutshell told you that NKN works, this hopefully showed you how. The fastest way to make it concrete is to clone the node, read pb/sigchain.proto with §04 open beside it, and trace a single packet from a client’s arbitrary-identifier.public-key all the way to a SIG_CHAIN_TXN on the ledger. Every link in that path is now something you can name.

Go deeper
Full node & README — github.com/nknorg/nkn · Protocol docs — docs.nkn.org · Consensus design — consensus & blockchain · Session protocol — NCP · MOCA origins (IEEE) — doi 8663808.

Sources

  • Allen Dixon, “NKN in a Nutshell” (2018) — the article this piece follows up.
  • NKN Protocol Documentation — Signature Chain, Session Protocol, Client Address Scheme, Transaction Types, Consensus & Blockchain — docs.nkn.org.
  • nknorg/nkn full-node README and pb/sigchain.protogithub.com/nknorg/nkn.
  • nkn.org, “Powering the Shared Internet by Blockchain”; nknorg/nkn-simulation (Ising consensus); MOCA (IEEE Xplore 8663808).

Image credits

  • NKN wordmark / logo — © NKN Foundation. Source: github.com/nknorg (project wiki assets). Used for identification of the subject. → images/nkn_logo.png
  • Figures 0–5 — Original technical diagrams created for this article. Released under CC BY 4.0. Fig. 4 is stylized after the public Ising-model consensus simulation in nknorg/nkn-simulation. Fig. 0 is an animated GIF (images/fig0-signature-chain.gif); the rest are PNG.

This is an independent technical write-up synthesizing public NKN documentation and source code. Not an official NKN publication. Verify protocol specifics against the live docs and repository, which change by activation height.