ContentsAccess and exposure

Access and exposure

On this page
  1. The control channel
  2. The REST interface
  3. Exposing a node

The reference node has two interfaces with very different trust levels. The control channel is private: it alone administers the node, and only a holder of the node's secret token can use it. REST is public: everything it offers is meant for anyone who can reach it.

The control channel#

The CLI administers the node over its own TCP listener, which must be on a loopback address: a configuration naming any other address is refused. Both sides hold the secret in control.token: 64 lower-case hex characters, a random 256-bit value created at init. Only its owner can read or write the file: on Unix by its mode, on Windows by an access list that grants the user who ran ting init and nobody else, whatever the data directory's folder would pass down. The token never crosses the connection, nor appears in status output, logs or peer messages; keep the file private.

A control request takes one connection of its own and three frames, framed as in the peer protocol. Proofs are HMAC-SHA-256 keyed with the token's 64 characters, over the concatenation of the parts listed, nonces as their 32 bytes:

  1. The client sends {"nonce": …}, 32 random bytes in hex.
  2. The node answers {"nonce": …, "proof": …}: its own 32 random bytes, and a proof over ting-control-node, the client's nonce and its own. The client checks it before it sends anything more, so a listener that is not the node, such as one on a port a stale runtime.json names, learns nothing it could use.
  3. The client sends {"request": …, "proof": …}: the request as a string of JSON, such as {"method":"status","params":{}}, and a proof over ting-control-client, the node's nonce, its own and the request string's bytes. This frame may be as large as twice the next block's data capacity and 64 KiB more, at least 16 MiB, so that submit carries a transaction of any size a block can hold.

The node checks the proof in constant time and answers {"ok": true, "result": …}, or {"ok": false, "error": …}, with unauthorized for a wrong proof and unknown control method for any other method than these:

Method Purpose CLI
status Chain status plus the listener addresses actually bound ting status, ting docs
peers Configured and discovered peers with their last success or error, and the size of the address book ting peers
add_peer, remove_peer Change the peer list ting add node, ting remove node
api Enable or disable REST, with or without CORS ting api enable, ting api disable
miner Start, stop or describe the built-in miner; set producer data and capacity votes ting miner …
votes Where capacity voting stands, with this node's votes ting votes
template, solution The node's half of mining ting template, ting solution
mine Template, search and solution in one bounded request ting mine
submit Submit a transaction, deferred with not_before ting submit
invalidate, reconsider Mark a block invalid, or clear invalid marks (overriding validity) ting invalidate, ting reconsider
stop Graceful shutdown ting stop

The node serves at most 16 control connections at once, and the CLI waits up to 35 seconds for an answer. Only this channel can mine, manage peers, toggle REST, override block validity and shut the node down. Mining software holding the token uses template and solution exactly as the CLI does; mining has no REST surface.

runtime.json records the addresses actually bound, and the process ID. It is never treated as authority to stop a process.

The REST interface#

REST is off by default. When on, it binds to loopback unless you choose another address, and it can be switched on or off while the node runs. Enabling it binds the listener before recording success, so a failed bind leaves REST off and reports the error. Switching it off, or stopping the node, closes idle connections at once and gives requests in progress a second to finish; then their connections close too.

Everything it offers is public by design:

  • reads of blocks, transactions, events, contracts, storage, status and metrics;
  • transaction submission into the bounded mempool with POST /v1/transactions, with no token, because programs define their own authorization.

REST status leaves out process IDs, listener addresses and control-channel details. REST's reads run on threads of their own, one per processor core, beside the thread that validates and applies blocks, and its few requests to that thread have their own share of its queue, so REST traffic cannot slow block processing or crowd out peers (how the node is put together). There is no rate per client. Connections, requests, bodies, queues and pages are bounded:

Bound Value When exceeded
Connections at once 512, or what the open-file limit leaves beside what peers and storage may need Further connections wait to be accepted
Connections from one client, an IPv4 address or an IPv6 /64 16; this machine's own, such as a reverse proxy's, are not counted Closed at once
Time to send a request's header, and to leave a connection idle after an answer 10 seconds; 5 minutes for this machine's own, longer than a reverse proxy keeps an idle connection to the node, so that the proxy closes it first Connection closed
Time to send an answer, from when it is ready A minute, and ten seconds for every MiB of it Connection closed
Requests in progress, each until all but at most 128 KiB of its answer is written to its connection 32 per processor core 503, HTTP service busy
REST's requests waiting for the chain thread (submissions, the mempool, pending status) 128 503, node request queue unavailable
Time per request 35 seconds; a submission also ten seconds for every MiB its body may have 504, HTTP deadline exceeded
A submission's body Twice the data capacity of the next block, and 4 KiB 413
A batch storage read's body 256 bytes for each of the 1,024 records it may name, and 4 KiB: 260 KiB 413
Any other request body 16 MiB 413
A JSON page Just under 16 MiB The page stops early; follow next

Every response carries X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer and Cache-Control: no-store. The endpoints are listed on Explore the chain.

REST never takes the descriptors that peers and storage may need. Those are one for each peer connection the configuration allows, max_inbound in and max_peers out and 32 configured peers, and 64 for the rest: the seeds, control connections, block files, the database and the log. On Unix the node raises its open-file limit at start to that and 512 more, as far as the system's hard limit allows, and REST takes what is left of the limit, at most 512. When that is fewer, the node says so in its log. Windows has no such limit.

Exposing a node#

  • Remote REST access is an explicit choice of bind address. For a remote audience, put a TLS reverse proxy in front.
  • By default REST grants no cross-origin permissions. ting api enable --cors answers browser applications from any origin, such as a wallet in a web page; leave it off unless such applications must reach this node.
  • The explorer, the API page and the wiki load no external assets. The API page's Swagger UI (5.33.0) is vendored with its license, notice and an integrity manifest and served by the node; its online schema validator, remote example servers and external documentation assets are disabled, and its requests go to this node's own API.
  • The explorer inserts all dynamic data as text, never as HTML.
  • Wiki pages come from a fixed registry compiled into the node. No user-controlled Markdown and no filesystem path is ever rendered.
  • Peer traffic is unencrypted and unauthenticated, but it carries only public chain data, and no peer message reaches administration.
  • Proof of work bounds what blocks cost, not how many alternatives to one block there are: every child of a block has the same target, so each sibling of the tip costs one block of work. A block a peer pushes unasked is stored only when its branch overtakes the tip, and it is executed at once: valid, it moves the tip; invalid, it stays on disk and its sender is banned for a day. One that does not overtake is held in memory, 64 MiB at most for all connections (pushed blocks). Nor can a peer grow the node's header tree with such alternatives: headers sync keeps a chain only once it overtakes the best header, and checks any other without keeping it (headers first).
  • What a node asks for along the best header's branch it stores whether or not it is ever executed, as Bitcoin does: a peer the node follows, whose header chain overtakes the best header, can withhold one of its blocks and have the node store the others, which it never connects without that one. Each such block costs the peer a block of work on a branch heavier than any the node knows, and only a peer the node chose to follow can do it; on a network with little hashpower that work is cheap, so this bounds the cost of such storage, not its possibility.
  • With discovery on, a node tells its peers the addresses of nodes it has recently synchronized with, and announces its own listening port. With "discovery": false it does neither. Its address can still spread: a node that configures it, synchronizes with it and has discovery on shares it like any other (finding more peers).