A relay, limiter, token and protection system for Nano node RPC & websocket interface
NanoRPCProxy is a relay and protection system that operates between a client and a Nano node RPC interface. It makes it possible to set the RPC interface public to the Internet without compromising the security of the node itself. The Nano node has no built-in functionality for user authentication, rate-limiting or caching which makes it dangerous to open up without protection as this proxy provides. With NanoRPCProxy you can, for example, serve a mobile app or web frontend with indirect node calls.
In reality, it can be used for Nano wallets, exchanges, block explorers, public APIs, monitor systems, Point of Sale or anything that communicates with a node.
The built-in token system makes it possible to serve requests beyond the default limits and monetize your backend via direct Nano token purchases. More info in the token section.
Demo clients/code for Curl, JS, REACT, Python and Flask are available to test your own server.
Public API demo client with token support: https://api.nanos.cc
In addition to the RPC interface, NanoRPCProxy also has built-in support for certain websocket subscriptions offered by the Nano node, for example block confirmations. Similar to the RPC, the websocket is DDOS protected and acts as a secure layer between the client and the node. This allows, for example, a public websocket open to everyone who needs to track certain accounts in real-time, without the need to set up their own node. That could be, but not limited to:
Demo clients for websocket is available for JS (web) and Node.js. More info in the websocket section.
Public websocket demo: https://api.nanos.cc/socket
Apart from increased security, NanoRPCProxy solves the scalability issue where a node can't serve too many clients and lets the node do what it's supposed to do, ie. process blocks as fast as possible. The system can be scaled up to serve thousands of clients with only one connection to the node per proxy server.
Possible use cases
These will not be affected if later updating the server via git pull
cp creds.json.default creds.json
cp settings.json.default settings.json
cp user_settings.json.default user_settings.json
cp token_settings.json.default token_settings.json
https://pm2.keymetrics.io/docs/usage/quick-start/
Make sure you have build the project (npm run build) first as specified in the Setup nodejs and test server
section.
Setting files need to be in the dist
folder for the proxy to read them properly.
Before making changes, stop any running servers with "pm2 stop dist/proxy.js" and delete the process with "pm delete proxy.js"
Example of PM2 web monitor. Can track all your apps and with realtime logs.
https://expeditedsecurity.com/blog/deploy-node-on-linux/#node-linux-service-systemd
Make sure you have build the project (npm run build) first as specified in the Setup nodejs and test server
section.
Setting files need to be in the dist
folder for the proxy to read them properly.
change to your actual location of proxy.js
[Unit]
Description=NanoRPCProxy
After=network.target
[Service]
ExecStart=/usr/local/bin/node /home/NanoRPCProxy/dist/proxy.js
Restart=always
RestartSec=10 #wait 10sec before restart
#User=nobody
#Group=nogroup
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
WorkingDirectory=/home/NanoRPCProxy/
[Install]
WantedBy=multi-user.target
It may happen that the settings files are expanded. In that case, you need to do this again in order for the new variables to be modified by you (or insert them manually). Save your old settings first!
cp settings.json.default settings.json
cp user_settings.json.default user_settings.json
cp token_settings.json.default token_settings.json
The docker image is available publicly on nanojson/nanorpcproxy but if you want to build it yourself:
Ensure that Docker is installed.
In order to run the latest stable image with default (no) config:
$ docker run -it nanojson/nanorpcproxy:latest
You can also pull the master branch (:master) or previous release tags. To build and run from source:
$ docker build . -t nanorpcproxy
Then run it without the public "nanojson" registry
$ docker run -it nanorpcproxy:latest
To run with configuration, first copy default settings:
$ cp creds.json.default creds.json
$ cp settings.json.default settings.json
$ cp user_settings.json.default user_settings.json
$ cp token_settings.json.default token_settings.json
To run the docker container with configuration you have to map the configuration files inside the container to /root
.
Here's an example mounting settings.json
from the current work directory, to /root/settings.json
in the container:
$ docker run -it -p 9950:9950 -v $(pwd)/:/root nanojson/nanorpcproxy:latest
The same goes for rest of the settings files. You can place them in a separate folder (settings) if you like and map that instead of the base folder.
$ docker run -it -p 9950:9950 -v $(pwd)/settings:/root nanojson/nanorpcproxy:latest
Here is an example how to run it indefinitely in the background and restart on fail or machine boot (with the name rpcproxy):
$ docker run -d --restart unless-stopped --name rpcproxy -p 9950:9950 -v $(pwd)/:/root nanojson/nanorpcproxy:latest
There's also a docker-compose.yml
file present. To run with docker compose,
first copy files as above. Then run image with:
$ docker-compose up
All files but settings.json is disabled in the docker-compose file by default.
docker-compose.yml
version: "3"
services:
node:
image: "nanocurrency/nano:latest"
restart: "unless-stopped"
ports:
- "7075:7075"
volumes:
- "./nano_node:/root"
nanorpcproxy:
image: "nanojson/nanorpcproxy:latest"
restart: "unless-stopped"
ports:
- "9950:9950"
- "9952:9952"
volumes:
- ./:/root
Terminal
$ docker-compose up
Example of config-node.toml
[node.websocket]
address = "::ffff:0.0.0.0"
enable = true
[rpc]
enable = true
To upgrade you need to first turn off running container ("docker stop xxx", or "docker-compose down"). Also remove the container if you want to keep the same name (label)
If you used docker compose to build from local source (nanorpcproxy:latest) you will have to rebuild:
The proxy server is configured via the settings.json file found in the server folder
The following parameters can be set in user_settings.json to override the default ones for specific users defined in creds.json. Anything in this file will override even if there are less sub entries like only 1 allowed command or 2 limited commands.
The following parameters can be set in token_settings.json for configuration of the token system. The system require the <use_tokens> to be active in settings.json
More info about the token system in this section
The effect of the settings
You call the proxy server just like you would call the node RPC. It's a normal POST request to "/proxy" with json formatted data. The node commands are found here: https://docs.nano.org/commands/rpc-protocol/
It also support URL queries via GET request, which means you can even run the commands from a web browser via links such as "/proxy/?action=block_count" and get a json response. However, if authentication is activated in the server settings, basic auth headers are needed so that won't work in a browser.
The proxy server also support special commands not supported in the Nano RPC. They need to be listed in the settings.json under "allowed_commands"
Returns the latest Nano price quote from Coinpaprika. Will always be cached for 10sec.
{
"id": "nano-nano",
"name": "Nano",
"symbol": "NANO",
"rank": 62,
"circulating_supply": 133248297,
"total_supply": 133248297,
"max_supply": 133248290,
"beta_value": 0.975658,
"last_updated": "2020-05-28T12:34:54Z",
"quotes": {
"USD": {
"price": 0.86498056,
"volume_24h": 5450637.5460105,
"volume_24h_change_24h": 12,
"market_cap": 115257186,
"market_cap_change_24h": -5.21,
"percent_change_1h": -0.59,
"percent_change_12h": -5.67,
"percent_change_24h": -5.21,
"percent_change_7d": -6.96,
"percent_change_30d": 45.21,
"percent_change_1y": -49.99,
"ath_price": 37.6212,
"ath_date": "2018-01-02T06:39:00Z",
"percent_from_price_ath": -97.7
}
}
}
Returns a list of verified accounts from https://mynano.ninja/. These can be used as suggestions for representative accounts. This response is cached for 1 minute:
[
{
"votingweight": 7.231803912122739e+35,
"delegators": 1359,
"uptime": 99.72967712635973,
"score": 99,
"account": "nano_33ad5app7jeo6jfe9ure6zsj8yg7knt6c1zrr5yg79ktfzk5ouhmpn6p5d7p",
"alias": "warai"
},
...
]
Converts NANO to raw
{
"amount": "1000000000000000000000000000000"
}
Converts raw to NANO
{
"amount": "0.000000000000000000000000000001"
}
The curl command looks just a tiny bit different than for a direct node request. You just have to define it with a json content type. You can also use the -i flag to include response headers.
POST: No authentication
curl -d '{"action":"block_count"}' http://localhost:9950/proxy
POST: With authentication
curl --user user1:user1 -d '{"action":"block_count"}' http://127.0.0.1:9950/proxy
GET: No authentication
curl http://localhost:9950/proxy?action=block_count
GET: With authentication
curl --user user1:user1 http://localhost:9950/proxy?action=block_count
Using Windows Powershell 7 - Escape quotes
curl -d '{\"action\":\"block_count\"}' http://localhost:9950/proxy
POST: No authentication
import requests
import json
try:
r = requests.post("http://localhost:9950/proxy", json={"action":"block_count"})
status = r.status_code
print("Status code: ", status)
if (status == 200):
print("Success!")
try:
print(r.json())
except:
print(r)
except Exception as e:
print("Fatal error", e)
POST: With authentication Note: verify=False means we ignore possible SSL certificate errors. Recommended to set to True
r = requests.post('http://localhost:9950/proxy', json={"action":"block_count"}, verify=False, auth=HTTPBasicAuth(username, password))
GET: With authentication
r = requests.get('http://localhost:9950/proxy?action=block_count', auth=HTTPBasicAuth(username, password))
POST: Async with authentication (Without: remove the Authorization header)
See the js demo client for full example with error handling
For html file: <script src="https://cdn.jsdelivr.net/npm/[email protected]/base64.min.js"></script>
async function postData(data = {}, server='http://localhost:9950/proxy') {
const response = await fetch(server, {
method: 'POST',
cache: 'no-cache',
headers: {
'Authorization': 'Basic ' + Base64.encode('user1:user1')
},
body: JSON.stringify(data)
})
return await response.json()
}
postData({"action":"block_count"})
.then((data) => {
console.log(JSON.stringify(data, null, 2))
})
.catch(function(error) {
console.log(error)
})
GET: No authentication using jquery
$.get("http://localhost:9950/proxy?action=block_count", function(data, status){
console.log(data)
})
GET: Authentication using jquery and ajax
$.ajax({
url: "http://localhost:9950/proxy?action=block_count",
type: "GET",
beforeSend: function(xhr){xhr.setRequestHeader('Authorization', 'Basic ' + Base64.encode('user1:user1'))},
success: function(data, status) {console.log(data)}
})
Only a certain amount of requests per time period is allowed and configured in the settings. Users who need more requests (however still affected by the "slow down rate limiter") can purchase tokens with Nano. The system requires the <use_tokens> to be active in settings.json. The system will also check orders older than 1h one time per hour and repair broken orders (by assigning tokens for any pending found), also removing unprocessed and empty orders older than one month.
Any RPC command can be made by including a request key. For each request 1 token will be deducted and the total left will be included in each response as tokens_total.
{
"count": "24613996",
"unchecked": "0",
"cemented": "24613996",
"tokens_total": 4999
}
As an alternative, it's also valid to include the token key via the header "Token: xyz".
curl -H "Token: 815c8c736756da0965ca0994e9ac59a0da7f635aa0675184eff96a3146c49d74" -d '{"action":"block_count"}' http://127.0.0.1:9950/proxy
Initiates a new order of 10 tokens and respond with a deposit account, a token key and the amount of Nano to pay
{
"address": "nano_3m497b1ghppe316aiu4o5eednfyueemzjf7a8wye3gi5rjrkpk1p59okghwb",
"token_key": "815c8c736756da0965ca0994e9ac59a0da7f635aa0675184eff96a3146c49d74",
"payment_amount": 0.001
}
Initiates a refill order of existing key for 10 tokens
{
"address": "nano_3m497b1ghppe316aiu4o5eednfyueemzjf7a8wye3gi5rjrkpk1p59okghwb",
"token_key": "815c8c736756da0965ca0994e9ac59a0da7f635aa0675184eff96a3146c49d74",
"payment_amount": 0.001
}
Check status of initiated order. Either the time left to pay the order:
{
"token_key": "741eb3ad2df88427e19c9b01ec326c36c184fbcbd0bf25004982e9bb223e1acf",
"order_time_left": 135
}
Or status:
{
"error": "Order timed out for key: 741eb3ad2df88427e19c9b01ec326c36c184fbcbd0bf25004982e9bb223e1acf"
}
Or final tokens bought based on the amount paid:
{
"token_key": "741eb3ad2df88427e19c9b01ec326c36c184fbcbd0bf25004982e9bb223e1acf",
"tokens_ordered": 1000,
"tokens_total": 2000
}
Reset the deposit account and return last private key to be used for recovery
{
"priv_key": "2aad399e19f926c7358a2d21d3c320e32bfedb774e0a43dba684853a1ca2cf56",
"status": "Order canceled and account replaced. You can use the private key to claim any leftover funds."
}
Returns the total amount of tokens bound to the key and status of last order
{
"tokens_total": 10,
"status": "OK"
}
Returns the current price set by the server
{
"token_price": 0.0001
}
Demo of purchasing tokens using the React demo client:
Order completed:
A Nano node provides a websocket that can be subscribed to for real-time messages, for example block confirmation, voting analysis and telemetry. More info can be found here. Like with the RPC interface, NanoRPCProxy provides a websocket server with blacklist / DDOS protection and bandwidth limitation by only allowing certain subscriptions and data amount. It subscribes to the Nano node locally with the clients subscribing to the proxy itself to act as a secure layer and protect the node. This means only one node subscription is needed to serve all clients and several clients can listen on the same account with no increase in node communication. Thus, the node websocket does not need to be exposed publicly.
The supported messages are shown below:
Subscribe to block confirmations Just like the node you can subscribe to confirmed blocks on the network. However, one exception is you MUST specify a list of accounts. The maximum allowed number is defined in the settings parameter <websocket_max_accounts>. The account is tracked based on both "account" or "link_as_account" which means it can be used also to detect pending transactions (which would be subtype=send and the tracked account as "link_as_account").
{
"action": "subscribe",
"topic": "confirmation",
"options": {
"accounts": [<account1>,<account2>]
}
}
Response
{
"topic": "confirmation",
"time": "1590331435605",
"message": {
"account": "nano_3jsonxwips1auuub94kd3osfg98s6f4x35ksshbotninrc1duswrcauidnue",
"amount": "10000000",
"hash": "2B779B43B3CF95AFAA63AD696E6546DB7945BCE5CC5A78F670FFD41BCA998D1E",
"confirmation_type": "active_quorum",
"block": {
"type": "state",
"account": "nano_3jsonxwips1auuub94kd3osfg98s6f4x35ksshbotninrc1duswrcauidnue",
"previous": "FC5AE29E3BD13A5D8D26EA2632871D2CFE7856BF4E83E75FA90B72AC95054635",
"representative": "nano_3jsonxwips1auuub94kd3osfg98s6f4x35ksshbotninrc1duswrcauidnue",
"balance": "946740088999999996972769989996",
"link": "90A12364A96F6F31EDC3ADA115E88B3AEAEA05C6A78A79023CFDEFB4D901FCD6",
"link_as_account": "nano_36736fkckuuh89pw9df34qnapgqcxa4wfbwch635szhhpmei5z8pttkxawk1",
"signature": "9C6A45460C946387A267EE6B5AEFE17C4C036C7B5E10239BC492CAAD180B4E0AD42A02875DC7B4FEF52B5FE8FD73BA3D28E0CCF8FDCFF86AA2938822E88A600B",
"work": "bed7a7d8ab438039",
"subtype": "send"
}
}
}
If error or warnings occurs in the server when calling it the client will need to handle that. The response is (along with a http status code != 200):
The server will write a standard log depending on the "log_level" in settings.json and token_settings.json. Additionally, a request-stat.log is written in the server dir every day at midnight with a daily request count and timestamp.
The proxy server can be tested and experimented with using provided demo clients. They can also help you getting starting with your own setup.
Exit pipenv: exit
Note: The credentials for authentication is hard coded in the javascript and to my knowledge it's not possible to hide. However, the reactjs client is compiled and thus have the creds hidden inside the app. As far as I know, that should be safe as long as the source code cred file is not shared publicly.
The only demo client that has full functionality for purchasing request tokens
To run the pre-built app:
To run or build the app from source
Exit pipenv: exit
Exit pipenv: exit
The proxy allows data scraping using Prometheus. That can for example be visualized in Grafana in your browser.
The following data points are enabled (prom-client.ts):
You can whitelist Prometheus per IP or subnet via the setting: "enable_prometheus_for_ips" If you are using docker it's recommended to whitelist the whole docker subnet since a container IP can change.
Easiest way to get started with Prometheus and Grafana is to use docker-compose. That works fine together with other node or RPCProxy containers you may be running. You can put them all in the same composer or you can use a local network as shown below called "mynet" that allow different containers to talk to each other. For example several other proxy servers running in different containers.
For persistant storage you can use docker volumes or local data folders (prom_data and graf_data) as shown below (you need to create them first). For access rights in this case you need your user ID, which was "0" in this example. To get ID you can run "id -u in linux".
More info in the section about docker
Node + proxy + prometheus + grafana: docker-compose.yml.
version: "3.7"
services:
node:
image: "nanocurrency/nano:latest"
restart: "unless-stopped"
ports:
- "7075:7075"
volumes:
- "./nano_node:/root"
nanorpcproxy:
image: "nanojson/nanorpcproxy:latest"
restart: "unless-stopped"
ports:
- "9950:9950"
- "9952:9952"
volumes:
- ./:/root
prometheus:
image: prom/prometheus
user: "0"
volumes:
- ./prom_data:/prometheus
- ./prometheus.yml:/etc/prometheus/prometheus.yml
depends_on:
- nanorpcproxy
ports:
- 9090:9090
grafana:
image: grafana/grafana:latest
user: "0"
volumes:
- ./graf_data:/var/lib/grafana
depends_on:
- prometheus
ports:
- 3000:3000
prometheus + grafana using a docker network: docker-compose.yml.
version: '3.7'
services:
prometheus:
image: prom/prometheus
user: "0"
volumes:
- ./prom_data:/prometheus
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- 9090:9090
grafana:
image: grafana/grafana:latest
user: "0"
volumes:
- ./graf_data:/var/lib/grafana
depends_on:
- prometheus
ports:
- 3000:3000
networks:
default:
external:
name: mynet
Before you start prometheus you also need a config file. You can have as many different jobs or targets as you like. That can be filtered later in grafana:
prometheus.yml
global:
scrape_interval: 30s
scrape_timeout: 10s
scrape_configs:
- job_name: proxy
metrics_path: /prometheus
static_configs:
- targets:
- 'nanorpcproxy:9950'
Then you can add panels and attach to the prometheus data points that are measured by the RPCproxy. Some examples below:
sum by (action)(round(rate(process_request{job="proxy"}[1h])*3600))
sum by (action)(increase(process_request{job="proxy"}[1h]))
sum(rate(process_request{job="proxy"}[1h])*3600)
sum by (action)(rate(time_rpc_call_sum{job="proxy"}[1h]) / rate(time_rpc_call_count{job="proxy"}[1h]))
sum(rate(websocket_message{job="proxy"}[1h])) * 3600
sum by (ip)(round(increase(process_request{job="proxy", action="work_generate"}[1d]))) > 100
sum by (ip)(round(rate(user_ddos{job="proxy"}[1h])*3600 + 1)) > 1
topk(20,sum by (ip)(increase(process_request{job="proxy"}[1d])))
topk(20,sum by (ip)(max_over_time(user_slow_down{job="proxy"}[1w])))
Find this useful? Consider sending me a Nano donation at nano_1gur37mt5cawjg5844bmpg8upo4hbgnbbuwcerdobqoeny4ewoqshowfakfo
Discord support server and feedback: https://discord.gg/RVCuFvc