feat: automated bond (#6860)

* initialise bonding automation

* initialise autobond flow

* docs for autobond

* tweak docs and add scraped stats

* resolve issues

* fix issues

* add extra command advice

* fix rabbitai suggestions

* fix rabbitai suggestions
This commit is contained in:
import this
2026-06-09 14:48:53 +02:00
committed by GitHub
parent 8ce06dbc0e
commit 4fcec99cc2
16 changed files with 790 additions and 75 deletions
+2
View File
@@ -83,3 +83,5 @@ test-tutorials/
# pnpm
.pnpm-store/
# operator tools
scripts/nym-node-setup/auto-bond/nodes.csv
+8
View File
@@ -0,0 +1,8 @@
---
- name: Nym node auto-bonding
hosts: all
gather_facts: false
serial: 1
roles:
- role: postinstall-auto
@@ -0,0 +1,38 @@
- name: Show which node is being bonded
tags: bonding
debug:
msg: "Bonding Nym node: {{ hostname }}"
- name: Get bonding details
tags: bonding
command: "/root/nym-binaries/nym-node bonding-information"
register: bondinfo
changed_when: false
- name: Display bonding info
tags: bonding
debug:
msg: "{{ item }}"
loop: "{{ bondinfo.stdout_lines }}"
- name: Sign bonding contract message on the node
tags: bonding
command:
argv:
- /root/nym-binaries/nym-node
- sign
- --contract-msg
- "{{ contract_msg }}"
- --output
- json
register: sign_output
- name: Display full signed message exactly as returned
tags: bonding
debug:
msg: "{{ sign_output.stdout }}"
- name: Display encoded signature
tags: bonding
debug:
msg: "ENCODED_SIGNATURE={{ (sign_output.stdout | from_json).encoded_signature }}"
+10 -6
View File
@@ -1,16 +1,20 @@
- name: Download quic_bridge_deployment.sh
tags: quic bridge deployment
get_url:
url: "{{ quic_bridge_deployment_url }}"
dest: "/root/nym-binaries/quic_bridge_deployment.sh"
command:
cmd: "curl -fsSL {{ quic_bridge_deployment_url }} -o /root/nym-binaries/quic_bridge_deployment.sh"
tags: quic
- name: Set quic_bridge_deployment permissions
file:
path: /root/nym-binaries/quic_bridge_deployment.sh
mode: "0755"
tags: quic
- name: Configure tunnel manager
tags: quic bridge deployment
become: true
command:
cmd: "/root/nym-binaries/quic_bridge_deployment.sh {{ item }}"
environment:
NONINTERACTIVE: "1"
loop:
- full_bridge_setup
- full_bridge_setup
tags: quic
+10 -4
View File
@@ -10,11 +10,17 @@
- ntm
- name: Download network tunnel manager
get_url:
url: "{{ tunnel_manager_url }}"
dest: /root/nym-binaries/network-tunnel-manager.sh
command:
cmd: "curl -fsSL {{ tunnel_manager_url }} -o /root/nym-binaries/network-tunnel-manager.sh"
tags:
- tunnel
- network_tunnel_manager
- ntm
- name: Set network tunnel manager permissions
file:
path: /root/nym-binaries/network-tunnel-manager.sh
mode: "0755"
force: yes
tags:
- tunnel
- network_tunnel_manager
@@ -1,6 +1,6 @@
{
"nodes": 677,
"locations": 77,
"mixnodes": 240,
"exit_gateways": 429
"nodes": 652,
"locations": 75,
"mixnodes": 239,
"exit_gateways": 405
}
@@ -1 +1 @@
Thursday, June 4th 2026, 11:40:35 UTC
Monday, June 8th 2026, 11:52:06 UTC
@@ -8,8 +8,10 @@ Commands:
help Print this message or the help of the given subcommand(s)
Options:
-c, --config-env-file <CONFIG_ENV_FILE> Path pointing to an env file that configures the Nym API [env: NYMAPI_CONFIG_ENV_FILE_ARG=]
--no-banner A no-op flag included for consistency with other binaries (and compatibility with nymvisor, oops) [env: NYMAPI_NO_BANNER_ARG=]
-c, --config-env-file <CONFIG_ENV_FILE> Path pointing to an env file that configures the Nym API [env:
NYMAPI_CONFIG_ENV_FILE_ARG=]
--no-banner A no-op flag included for consistency with other binaries (and compatibility with
nymvisor, oops) [env: NYMAPI_NO_BANNER_ARG=]
-h, --help Print help
-V, --version Print version
```
@@ -12,7 +12,8 @@ Commands:
help Print this message or the help of the given subcommand(s)
Options:
-c, --config-env-file <CONFIG_ENV_FILE> Path pointing to an env file that configures the nym-node and overrides any preconfigured values [env: NYMNODE_CONFIG_ENV_FILE_ARG=]
-c, --config-env-file <CONFIG_ENV_FILE> Path pointing to an env file that configures the nym-node and overrides any
preconfigured values [env: NYMNODE_CONFIG_ENV_FILE_ARG=]
--no-banner Flag used for disabling the printed banner in tty [env: NYMNODE_NO_BANNER=]
-h, --help Print help
-V, --version Print version
@@ -9,110 +9,144 @@ Options:
--config-file <CONFIG_FILE>
Path to a configuration file of this node [env: NYMNODE_CONFIG=]
--accept-operator-terms-and-conditions
Explicitly specify whether you agree with the terms and conditions of a nym node operator as defined at <https://nymtech.net/terms-and-conditions/operators/v1.0.0> [env:
NYMNODE_ACCEPT_OPERATOR_TERMS=]
Explicitly specify whether you agree with the terms and conditions of a nym node operator as defined at
<https://nymtech.net/terms-and-conditions/operators/v1.0.0> [env: NYMNODE_ACCEPT_OPERATOR_TERMS=]
--deny-init
Forbid a new node from being initialised if configuration file for the provided specification doesn't already exist [env: NYMNODE_DENY_INIT=]
Forbid a new node from being initialised if configuration file for the provided specification doesn't already exist
[env: NYMNODE_DENY_INIT=]
--init-only
If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses [env: NYMNODE_INIT_ONLY=]
If this is a brand new nym-node, specify whether it should only be initialised without actually running the
subprocesses [env: NYMNODE_INIT_ONLY=]
--local
Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=]
--mode [<MODE>...]
Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway,
exit-gateway, exit-providers-only]
--modes <MODES>
Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode,
entry-gateway, exit-gateway, exit-providers-only]
-w, --write-changes
If this node has been initialised before, specify whether to write any new changes to the config file [env: NYMNODE_WRITE_CONFIG_CHANGES=]
If this node has been initialised before, specify whether to write any new changes to the config file [env:
NYMNODE_WRITE_CONFIG_CHANGES=]
--bonding-information-output <BONDING_INFORMATION_OUTPUT>
Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the required bonding information is still a subject to change and this argument should be
treated only as a preview of future features [env: NYMNODE_BONDING_INFORMATION_OUTPUT=]
Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the required bonding
information is still a subject to change and this argument should be treated only as a preview of future features
[env: NYMNODE_BONDING_INFORMATION_OUTPUT=]
-o, --output <OUTPUT>
Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] [default: text] [possible values: text, json]
Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] [default: text]
[possible values: text, json]
--public-ips <PUBLIC_IPS>
Comma separated list of public ip addresses that will be announced to the nym-api and subsequently to the clients. In nearly all circumstances, it's going to be identical to the
address you're going to use for bonding [env: NYMNODE_PUBLIC_IPS=]
Comma separated list of public ip addresses that will be announced to the nym-api and subsequently to the clients. In
nearly all circumstances, it's going to be identical to the address you're going to use for bonding [env:
NYMNODE_PUBLIC_IPS=]
--hostname <HOSTNAME>
Optional hostname associated with this gateway that will be announced to the nym-api and subsequently to the clients [env: NYMNODE_HOSTNAME=]
Optional hostname associated with this gateway that will be announced to the nym-api and subsequently to the clients
[env: NYMNODE_HOSTNAME=]
--location <LOCATION>
Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or three-digit
numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=]
Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2
(e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or three-digit numeric-3 (e.g. '616') can be provided [env:
NYMNODE_LOCATION=]
--http-bind-address <HTTP_BIND_ADDRESS>
Socket address this node will use for binding its http API. default: `[::]:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=]
--landing-page-assets-path <LANDING_PAGE_ASSETS_PATH>
Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=]
--http-access-token <HTTP_ACCESS_TOKEN>
An optional bearer token for accessing certain http endpoints. Currently only used for prometheus metrics [env: NYMNODE_HTTP_ACCESS_TOKEN=]
An optional bearer token for accessing certain http endpoints. Currently only used for prometheus metrics [env:
NYMNODE_HTTP_ACCESS_TOKEN=]
--expose-system-info <EXPOSE_SYSTEM_INFO>
Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=] [possible values: true, false]
Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=]
[possible values: true, false]
--expose-system-hardware <EXPOSE_SYSTEM_HARDWARE>
Specify whether basic system hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false]
Specify whether basic system hardware information should be exposed. default: true [env:
NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false]
--expose-crypto-hardware <EXPOSE_CRYPTO_HARDWARE>
Specify whether detailed system crypto hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
--mixnet-bind-address <MIXNET_BIND_ADDRESS>
Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=]
--mixnet-announce-port <MIXNET_ANNOUNCE_PORT>
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=]
--nym-api-urls <NYM_API_URLS>
Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=]
Specify whether detailed system crypto hardware information should be exposed. default: true [env:
NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
--nyxd-urls <NYXD_URLS>
Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=]
--nyxd-websocket-url <NYXD_WEBSOCKET_URL>
Url to the websocket endpoint of a nyx validator, for example `wss://rpc.nymtech.net/websocket`. It is used for
subscribing to new block events [env: NYMNODE_NYXD_WEBSOCKET=]
--mixnet-bind-address <MIXNET_BIND_ADDRESS>
Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env:
NYMNODE_MIXNET_BIND_ADDRESS=]
--mixnet-announce-port <MIXNET_ANNOUNCE_PORT>
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the
node is behind a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=]
--nym-api-urls <NYM_API_URLS>
Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=]
--enable-console-logging <ENABLE_CONSOLE_LOGGING>
Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible values: true, false]
Specify whether running statistics of this node should be logged to the console [env:
NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible values: true, false]
--wireguard-enabled <WIREGUARD_ENABLED>
Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false]
Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true,
false]
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS>
Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env:
NYMNODE_WG_BIND_ADDRESS=]
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT>
Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances
where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX>
The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env:
NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4
is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
--wireguard-userspace <WIREGUARD_USERSPACE>
Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments without kernel WireGuard support [env: NYMNODE_WG_USERSPACE=]
[possible values: true, false]
Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized
environments without kernel WireGuard support [env: NYMNODE_WG_USERSPACE=] [possible values: true, false]
--verloc-bind-address <VERLOC_BIND_ADDRESS>
Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
Socket address this node will use for binding its verloc API. default: `[::]:1790` [env:
NYMNODE_VERLOC_BIND_ADDRESS=]
--verloc-announce-port <VERLOC_ANNOUNCE_PORT>
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=]
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the
node is behind a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=]
--entry-bind-address <ENTRY_BIND_ADDRESS>
Socket address this node will use for binding its client websocket API. default: `[::]:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=]
Socket address this node will use for binding its client websocket API. default: `[::]:9000` [env:
NYMNODE_ENTRY_BIND_ADDRESS=]
--announce-ws-port <ANNOUNCE_WS_PORT>
Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will be used instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address`
will be used instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
--announce-wss-port <ANNOUNCE_WSS_PORT>
If applicable, announced port for listening for secure websocket client traffic [env: NYMNODE_ENTRY_ANNOUNCE_WSS_PORT=]
If applicable, announced port for listening for secure websocket client traffic [env:
NYMNODE_ENTRY_ANNOUNCE_WSS_PORT=]
--enforce-zk-nyms <ENFORCE_ZK_NYMS>
Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] [possible
values: true, false]
Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts
non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] [possible values: true, false]
--mnemonic <MNEMONIC>
Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be generated [env: NYMNODE_MNEMONIC=]
Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be
generated [env: NYMNODE_MNEMONIC=]
--upgrade-mode-attestation-url <UPGRADE_MODE_ATTESTATION_URL>
Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets
and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY>
Expected public key of the entity signing the published attestation. This argument should never be set outside testnets and local networks [env:
NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
Expected public key of the entity signing the published attestation. This argument should never be set outside
testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
--upstream-exit-policy-url <UPSTREAM_EXIT_POLICY_URL>
Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=]
--open-proxy <OPEN_PROXY>
Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to resolve **ANY** request it receives [env: NYMNODE_OPEN_PROXY=] [possible values: true,
false]
Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to resolve **ANY** request it
receives [env: NYMNODE_OPEN_PROXY=] [possible values: true, false]
--nr-allow-local-ips <NR_ALLOW_LOCAL_IPS>
Allow the network requester to forward traffic to non-globally-routable addresses. Intended for local development, private-network deployments, and testnet scenarios. Not
recommended on production exit gateway unless you know what you're doing [env: NYMNODE_NR_ALLOW_LOCAL_IPS=] [possible values: true, false]
Allow the network requester to forward traffic to non-globally-routable addresses. Intended for local development,
private-network deployments, and testnet scenarios. Not recommended on production exit gateway unless you know what
you're doing [env: NYMNODE_NR_ALLOW_LOCAL_IPS=] [possible values: true, false]
--ipr-allow-local-ips <IPR_ALLOW_LOCAL_IPS>
Allow the IP packet router to forward traffic to non-globally-routable addresses. Intended for local development, private-network deployments, and testnet scenarios. Not recommended
on production exit gateway unless you know what you're doing [env: NYMNODE_IPR_ALLOW_LOCAL_IPS=] [possible values: true, false]
Allow the IP packet router to forward traffic to non-globally-routable addresses. Intended for local development,
private-network deployments, and testnet scenarios. Not recommended on production exit gateway unless you know what
you're doing [env: NYMNODE_IPR_ALLOW_LOCAL_IPS=] [possible values: true, false]
--lp-control-bind-address <LP_CONTROL_BIND_ADDRESS>
Bind address for the TCP LP control traffic. default: `[::]:41264` [env: NYMNODE_LP_CONTROL_BIND_ADDRESS=]
--lp-control-announce-port <LP_CONTROL_ANNOUNCE_PORT>
Custom announced port for listening for the TCP LP control traffic. If unspecified, the value from the `lp_control_bind_address` will be used instead [env:
NYMNODE_LP_CONTROL_ANNOUNCE_PORT=]
Custom announced port for listening for the TCP LP control traffic. If unspecified, the value from the
`lp_control_bind_address` will be used instead [env: NYMNODE_LP_CONTROL_ANNOUNCE_PORT=]
--lp-data-bind-address <LP_DATA_BIND_ADDRESS>
Bind address for the UDP LP data traffic. default: `[::]:51264` [env: NYMNODE_LP_DATA_BIND_ADDRESS=]
--lp-data-announce-port <LP_DATA_ANNOUNCE_PORT>
Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from the `lp_data_bind_address` will be used instead [env: NYMNODE_LP_DATA_ANNOUNCE_PORT=]
Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from the
`lp_data_bind_address` will be used instead [env: NYMNODE_LP_DATA_ANNOUNCE_PORT=]
--lp-use-mock-ecash <LP_USE_MOCK_ECASH>
Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When enabled, the LP listener will accept any credential without
blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false]
Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When
enabled, the LP listener will accept any credential without blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=]
[possible values: true, false]
-h, --help
Print help
```
@@ -11,7 +11,8 @@ Commands:
help Print this message or the help of the given subcommand(s)
Options:
-c, --config-env-file <CONFIG_ENV_FILE> Path pointing to an env file that configures the nymvisor and overrides any preconfigured values
-c, --config-env-file <CONFIG_ENV_FILE> Path pointing to an env file that configures the nymvisor and overrides any
preconfigured values
-h, --help Print help
-V, --version Print version
```
@@ -1,5 +1,6 @@
import { Callout } from 'nextra/components';
import { Tabs } from 'nextra/components';
import { MyTab } from 'components/generic-tabs.tsx';
import { Steps } from 'nextra/components';
import { RunTabs } from 'components/operators/nodes/node-run-command-tabs';
import { VarInfo } from 'components/variable-info.tsx';
@@ -195,9 +196,122 @@ ansible-playbook deploy.yml
###### 2. Bond
<Callout type="warning" emoji="⚠️">
Anyone having acces to your account mnemonic can take all your funds and manage manage your node, be careful where you store it!
</Callout>
Bonding can be managed via two playbooks:
1. `bond.yml`: an interactive way, requiring operator to use own wallet (desktop or CLI)
2. `auto-bond.yml`: automatic bonding flow requiring operator to prepare `nodes.csv` and have `nym-cli` installed
<div>
<Tabs items={[
<code>bond.yml</code>,
<code>auto-bond.yml</code>,
]} defaultIndex="1">
<MyTab>
A playbook to *interactively* register your nodes to Nym network by bonding it to Nyx blockchain accounts.
This playbook is intercative as it prompts user for data from Nym wallet to sign a message. It will run roles on one inventory entry at a time by default.
**Requirements**
- Nym Wallet or `nym-cli` to be used as a CLI wallet
- An account per each node
- At least 101 NYM per account
**Usage**
1. Sign in to the wallet per node
2. Follow steps in `Bond` section
3. Run the playbook on a side and follow the prompts
```sh
cd playbooks
ansible-playbook bond.yml
```
Your nodes are bonded and will show in the network in the next epoch (max 60min).
</MyTab>
<MyTab>
A playbook to *automatically* register your nodes to Nym Network by bonding it to Nyx blockchain accounts.
This automatic flow is slightly harder to setup in the beginning and it's recommended for operators bonding many nodes, as the initial work is worth it by saving the time of bonding a node at a time.
**Requirements**
- Installed [`nym-cli`](/developers/tools/nym-cli)
- Python3
- Nym repository with directory `scripts/nym-node-setup/auto-bond/`, containing:
- Python program [`auto_bond_all.py`](https://github.com/nymtech/nym/tree/develop/scripts/nym-node-setup/auto-bond/auto_bond_all.py)
- `nodes.csv.example` with correct data
**Usage**
1. Copy `nodes.csv.example` to your prefered location without `.example` suffix
2. Fill correctly the csv columns for each node you want to bond:
- `inventory_node_id`: Your Ansible inventory node ID (ie `node1`)
- `hostname`: same like in your `playbooks/inventory/all` (without `https://` !)
- `ip`: same like `ansible_host` value in your Ansible inventory
- `account`: Nyx account to bond this node with
- `mnemonic`: Your nyx acount mnemonic
- `identity_key`: node identity key - the easiest way to get it is to navigate to `playbooks/` and run:
```sh
ansible all -i inventory/all -a "/root/nym-binaries/nym-node bonding-information"
```
- `amount`: Amount to bond in `uNYM` (1 NYM = 1 000 000 uNYM), Make sure to leave extra 1 NYM (1 000 000 uNYM) for fees
- `operator_cost`: [Operator cost](/operators/tokenomics/mixnet-rewards#rewards-distribution) in `uNYM` (1 NYM = 1 000 000 uNYM)
3. Save the csv
4. Run `auto_bond_all.py` with all needed arguments.
- To see help menu:
```sh
python3 ./auto_bond_all.py --help
```
- To test your paths run with `--dry-run`
- Argument usage:
```sh
--ansible-repo ANSIBLE_REPO
Path to ansible playbooks directory (contains auto-bond.yml and inventory/)
--cli-dir CLI_DIR Directory containing the nym-cli binary
--dry-run Print commands without executing
```
- Example (note that the `nodes.csv` has no flag as it's a required argument):
```sh
python ./auto_bond_all.py \
--ansible-repo ~/admin/nym-nodes/nym-nodes-ansible/playbooks \
--cli-dir ~/repos/nymtech/nym/target/release \
~/admin/nym-nodes/nodes.csv
```
5. Your nodes should be bonded and come up in the next epoch (max 60min)
**Additional scripts**
Your `nodes.csv` can be used for other operations:
- [`show_balances.py`](https://github.com/nymtech/nym/tree/develop/scripts/nym-node-setup/auto-bond/auto_bond_all.py): Shows all accounts balances if provided with Nyx accounts (`account` column)
- [`unbond_all.py`](https://github.com/nymtech/nym/tree/develop/scripts/nym-node-setup/auto-bond/unbond_all.py): Unbond all nodes in the csv if provided with mnemonics (`mnemonic` column)
<br/>
</MyTab>
</Tabs>
</div>
A playbook to interactively register your node to Nym network by bonding it to Nyx blockchain account.
This playbook is intercative as it prompts user for data from Nym wallet to sign a message. It will run roles on one inventory entry at a time by default.
This playbook is interactive as it prompts user for data from Nym wallet to sign a message. It will run roles on one inventory entry at a time by default.
```sh
cd playbooks
@@ -305,6 +419,20 @@ ansible-playbook <PLAYBOOK>.yml --list-tags
ansible-playbook deploy.yml --list-tags
```
###### Arbitrary command output
You can use ansible to read a `STDOUT` from any command, using this logic:
```sh
ansible all -i inventory/all -a "<COMMAND>>"
# for example to get all node ID keys
ansible all -i inventory/all -a "/root/nym-binaries/nym-node bonding-information"
```
- Note that the command gets also run, be mindful what you executing
- This logic can be combined with the arguments above, for example to limit the range of nodes
###### nocows
Yes, by default there is a cow printed under each task, you can turn it off by opening `playbooks/ansible.cfg` and un-commenting the `nocows` line:
@@ -0,0 +1,254 @@
#!/usr/bin/env python3
"""
Automated Nym node bonding from CSV.
Usage:
python3 auto_bond_all.py nodes.csv [options]
Options:
--ansible-repo PATH Path to ansible playbooks directory (contains auto-bond.yml and inventory/)
--cli-dir PATH Path to directory containing nym-cli binary
--dry-run Print commands without executing
CSV format:
inventory_node_id,hostname,ip,account,mnemonic,identity_key,amount,operator_cost
"""
import argparse
import csv
import json
import subprocess
import sys
import re
from pathlib import Path
NYXD_URL = "https://rpc.nymtech.net"
NYM_API_URL = "https://validator.nymtech.net/api"
# ── Colors ──
G = "\033[0;32m" # green
R = "\033[0;31m" # red
Y = "\033[0;33m" # yellow
C = "\033[0;36m" # cyan
W = "\033[1;37m" # bold white
D = "\033[2;37m" # dim
NC = "\033[0m" # reset
def ok(msg): print(f" {G}{NC} {msg}")
def err(msg): print(f" {R}{NC} {msg}")
def info(msg): print(f" {C}{NC} {msg}")
def dim(msg): print(f" {D}{msg}{NC}")
def parse_args():
parser = argparse.ArgumentParser(description="Automated Nym node bonding from CSV")
parser.add_argument("csv_file", help="Path to nodes CSV file")
parser.add_argument(
"--ansible-repo",
type=Path,
default=None,
help="Path to ansible playbooks directory (contains auto-bond.yml and inventory/)",
)
parser.add_argument(
"--cli-dir",
type=Path,
default=None,
help="Directory containing the nym-cli binary",
)
parser.add_argument("--dry-run", action="store_true", help="Print commands without executing")
return parser.parse_args()
def resolve_paths(args):
script_dir = Path(__file__).resolve().parent
ansible_repo = args.ansible_repo.resolve() if args.ansible_repo else script_dir.parents[3]
nym_cli = (args.cli_dir.resolve() / "nym-cli") if args.cli_dir else (ansible_repo / "target" / "release" / "nym-cli")
ansible_pb = ansible_repo / "auto-bond.yml"
inventory = ansible_repo / "inventory" / "all"
errors = []
if not nym_cli.exists(): errors.append(f"nym-cli not found at: {nym_cli}")
if not ansible_pb.exists(): errors.append(f"auto-bond.yml not found at: {ansible_pb}")
if not inventory.exists(): errors.append(f"inventory not found at: {inventory}")
if errors and not args.dry_run:
for e in errors:
err(e)
sys.exit(1)
return nym_cli, ansible_pb, inventory
SENSITIVE_FLAGS = {"--mnemonic", "--signature"}
def redact_cmd(cmd: list) -> list[str]:
redacted = []
hide_next = False
for token in map(str, cmd):
if hide_next:
redacted.append("***REDACTED***")
hide_next = False
continue
redacted.append(token)
if token in SENSITIVE_FLAGS:
hide_next = True
return redacted
def run(cmd: list, capture=True) -> subprocess.CompletedProcess:
print(f" $ {' '.join(redact_cmd(cmd))}")
if dry_run:
return subprocess.CompletedProcess(cmd, 0, stdout='{"dry_run": true}', stderr="")
result = subprocess.run(cmd, capture_output=capture, text=True, cwd=cwd)
if result.returncode != 0:
if result.stdout: print(result.stdout)
if result.stderr: print(f"{R}{result.stderr}{NC}")
result.check_returncode()
return result
def extract_ansible_recap(output: str):
"""Extract PLAY RECAP block from ansible stdout."""
match = re.search(r"(PLAY RECAP \*+.*?)(?=\n\n|\Z)", output, re.DOTALL)
return match.group(0).strip() if match else None
def generate_payload(row: dict, nym_cli: Path, dry_run: bool) -> str:
result = run([
nym_cli, "mixnet", "operators", "nymnode",
"create-node-bonding-sign-payload",
"--host", row["hostname"],
"--identity-key", row["identity_key"],
"--amount", row["amount"],
"--mnemonic", row["mnemonic"],
"--interval-operating-cost", row["operator_cost"],
"--nyxd-url", NYXD_URL,
"--nym-api-url", NYM_API_URL,
"-o", "json",
], dry_run)
if dry_run:
return "DRY_RUN_PAYLOAD"
data = json.loads(result.stdout)
return data.get("payload") or data.get("sign_payload") or list(data.values())[0]
def ansible_sign(node_id: str, payload: str, ansible_pb: Path, inventory: Path, dry_run: bool):
"""Returns (signature, recap_block)."""
result = run([
"ansible-playbook", ansible_pb,
"-i", inventory,
"--limit", node_id,
"--tags", "bonding",
"--extra-vars", f"contract_msg={payload}",
], dry_run, cwd=ansible_pb.parent)
if dry_run:
return "DRY_RUN_SIGNATURE", "DRY_RUN_RECAP"
recap = extract_ansible_recap(result.stdout)
match = re.search(r'ENCODED_SIGNATURE=([1-9A-HJ-NP-Za-km-z]+)', result.stdout)
if not match:
raise ValueError(f"Could not find ENCODED_SIGNATURE in ansible output:\n{result.stdout}")
return match.group(1), recap
def bond_node(row: dict, signature: str, nym_cli: Path, dry_run: bool):
cmd = [
nym_cli, "mixnet", "operators", "nymnode", "bond",
"--host", row["hostname"],
"--identity-key", row["identity_key"],
"--amount", row["amount"],
"--mnemonic", row["mnemonic"],
"--signature", signature,
"--interval-operating-cost", row["operator_cost"],
"--nyxd-url", NYXD_URL,
"--nym-api-url", NYM_API_URL,
"--force",
]
dim("$ " + " ".join(str(c) for c in cmd))
if dry_run:
return
result = subprocess.run(cmd, text=True)
if result.returncode != 0:
raise RuntimeError(f"bond command failed with exit code {result.returncode}")
def main():
args = parse_args()
nym_cli, ansible_pb, inventory = resolve_paths(args)
print(f"\n {D}nym-cli : {nym_cli}{NC}")
print(f" {D}playbook : {ansible_pb}{NC}")
print(f" {D}inventory: {inventory}{NC}\n")
with open(args.csv_file) as f:
nodes = list(csv.DictReader(f))
print(f"{W}{''*70}{NC}")
dry_label = f" {Y}[DRY RUN]{NC}" if args.dry_run else ""
print(f" {W}Bonding {len(nodes)} node(s){NC}{dry_label}")
print(f"{W}{''*70}{NC}\n")
results = []
failures = []
for i, row in enumerate(nodes, 1):
hostname = row["hostname"]
node_id = row["inventory_node_id"]
print(f"\n{W}[{i}/{len(nodes)}]{NC} {C}{hostname}{NC} {D}({node_id}){NC}")
print(f" {D}{''*60}{NC}")
recap = None
try:
info("Generating bonding payload…")
payload = generate_payload(row, nym_cli, args.dry_run)
info("Signing on remote node via Ansible…")
signature, recap = ansible_sign(node_id, payload, ansible_pb, inventory, args.dry_run)
if recap:
print(f"\n {D}{recap}{NC}\n")
info("Submitting bond transaction…")
bond_node(row, signature, nym_cli, args.dry_run)
ok("Bonded successfully")
results.append((hostname, True, recap, None))
except Exception as e:
error_msg = str(e)
err(f"FAILED: {error_msg}")
results.append((hostname, False, recap, error_msg))
failures.append((hostname, error_msg))
print(f" {Y}↳ Continuing with next node…{NC}")
# ── Final summary ──
print(f"\n{W}{''*70}{NC}")
print(f" {W}SUMMARY{NC}")
print(f"{W}{''*70}{NC}")
for hostname, success, recap, error_msg in results:
status = f"{G}✓ OK {NC}" if success else f"{R}✗ FAILED{NC}"
print(f" {status} {C}{hostname}{NC}")
print(f"{W}{''*70}{NC}")
succeeded = len(results) - len(failures)
print(f" Total: {G}{succeeded} succeeded{NC} {R}{len(failures)} failed{NC}")
if failures:
print(f"\n {R}{W}FAILED NODES:{NC}")
for hostname, reason in failures:
print(f" {R}{NC} {C}{hostname}{NC}")
print(f" {D}{reason}{NC}")
print(f"{W}{''*70}{NC}\n")
if failures:
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,3 @@
inventory_node_id,hostname,ip,account,mnemonic,identity_key,amount,operator_cost
node1,nym-exit.node1.example.com,1.2.3.4,n1...,word1 word2 ...,5XjrYTR...,100000000,40000000
node2,nym-exit.node2.example.com,5.6.7.8,n1...,word1 word2 ...,5ZWdDN9...,100000000,40000000
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
Check balances for all accounts in nodes.csv.
Usage:
python3 show_balances.py nodes.csv [options]
Options:
--cli-dir PATH Directory containing the nym-cli binary
--dry-run Print commands without executing
"""
import argparse
import csv
import subprocess
import sys
from pathlib import Path
NYXD_URL = "https://rpc.nymtech.net"
# ── Colors ──
G = "\033[0;32m"
R = "\033[0;31m"
Y = "\033[0;33m"
C = "\033[0;36m"
W = "\033[1;37m"
D = "\033[2;37m"
NC = "\033[0m"
def parse_args():
parser = argparse.ArgumentParser(description="Check balances for all accounts in CSV")
parser.add_argument("csv_file", help="Path to nodes CSV file")
parser.add_argument(
"--cli-dir",
type=Path,
default=None,
help="Directory containing the nym-cli binary",
)
parser.add_argument("--dry-run", action="store_true", help="Print commands without executing")
return parser.parse_args()
def resolve_nym_cli(args):
if args.cli_dir:
nym_cli = args.cli_dir.resolve() / "nym-cli"
else:
nym_cli = Path(__file__).resolve().parents[3] / "target" / "release" / "nym-cli"
if not nym_cli.exists() and not args.dry_run:
print(f" {R}{NC} nym-cli not found at: {nym_cli}")
sys.exit(1)
return nym_cli
def get_balance(nym_cli: Path, account: str, dry_run: bool) -> str:
if dry_run:
return "DRY_RUN_BALANCE"
result = subprocess.run(
[nym_cli, "account", "balance", account, "--nyxd-url", NYXD_URL],
capture_output=True, text=True, check=True
)
return result.stdout.strip()
def main():
args = parse_args()
nym_cli = resolve_nym_cli(args)
print(f"\n {D}nym-cli: {nym_cli}{NC}\n")
with open(args.csv_file, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
required = {"hostname", "account"}
missing = required - set(reader.fieldnames or [])
if missing:
print(f" {R}{NC} Missing required CSV columns: {', '.join(sorted(missing))}")
sys.exit(1)
nodes = list(reader)
print(f"{W}{''*60}{NC}")
dry_label = f" {Y}[DRY RUN]{NC}" if args.dry_run else ""
print(f" {W}Checking {len(nodes)} account(s){NC}{dry_label}")
print(f"{W}{''*60}{NC}\n")
print(f" {W}{'HOSTNAME':<40} {'ACCOUNT':<45} BALANCE{NC}")
print(f" {D}{''*110}{NC}")
errors = 0
total_nym = 0.0
for row in nodes:
hostname = row["hostname"]
account = row["account"]
try:
balance = get_balance(nym_cli, account, args.dry_run)
print(f" {C}{hostname:<40}{NC} {D}{account:<45}{NC} {G}{balance}{NC}")
parts = balance.split()
if parts:
try:
total_nym += float(parts[0])
except ValueError:
pass
except Exception as e:
print(f" {C}{hostname:<40}{NC} {D}{account:<45}{NC} {R}✗ ERROR: {e}{NC}")
errors += 1
print(f" {D}{''*110}{NC}")
print(f" {W}Total balance: {G}{total_nym:,.6f} nym{NC}")
print(f" {W}Accounts: {G}{len(nodes) - errors} OK{NC} {R}{errors} errors{NC}\n")
if errors:
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
Unbond all nodes listed in nodes.csv.
Usage:
python3 unbond_all.py nodes.csv [options]
Options:
--cli-dir PATH Directory containing the nym-cli binary
--dry-run Print commands without executing
"""
import argparse
import csv
import subprocess
import sys
from pathlib import Path
NYXD_URL = "https://rpc.nymtech.net"
# ── Colors ──
G = "\033[0;32m"
R = "\033[0;31m"
Y = "\033[0;33m"
C = "\033[0;36m"
W = "\033[1;37m"
D = "\033[2;37m"
NC = "\033[0m"
def parse_args():
parser = argparse.ArgumentParser(description="Unbond all Nym nodes listed in CSV")
parser.add_argument("csv_file", help="Path to nodes CSV file")
parser.add_argument(
"--cli-dir",
type=Path,
default=None,
help="Directory containing the nym-cli binary",
)
parser.add_argument("--dry-run", action="store_true", help="Print commands without executing")
return parser.parse_args()
def resolve_nym_cli(args):
if args.cli_dir:
nym_cli = args.cli_dir.resolve() / "nym-cli"
else:
nym_cli = Path(__file__).resolve().parents[3] / "target" / "release" / "nym-cli"
if not nym_cli.exists() and not args.dry_run:
print(f" {R}{NC} nym-cli not found at: {nym_cli}")
sys.exit(1)
return nym_cli
def run(cmd, dry_run: bool):
redacted = [str(c) for c in cmd]
if "--mnemonic" in redacted:
i = redacted.index("--mnemonic")
if i + 1 < len(redacted):
redacted[i + 1] = "***REDACTED***"
print(f" {D}$ {' '.join(redacted)}{NC}")
if dry_run:
return
subprocess.run(cmd, check=True)
def main():
args = parse_args()
nym_cli = resolve_nym_cli(args)
print(f"\n {D}nym-cli: {nym_cli}{NC}\n")
with open(args.csv_file, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
required = {"hostname", "mnemonic"}
missing = required - set(reader.fieldnames or [])
if missing:
print(f" {R}{NC} Missing required CSV columns: {', '.join(sorted(missing))}")
sys.exit(1)
nodes = list(reader)
print(f"{W}{''*60}{NC}")
dry_label = f" {Y}[DRY RUN]{NC}" if args.dry_run else ""
print(f" {W}Unbonding {len(nodes)} node(s){NC}{dry_label}")
print(f"{W}{''*60}{NC}\n")
results = []
for i, row in enumerate(nodes, 1):
hostname = (row.get("hostname") or f"<row {i}>").strip()
mnemonic = (row.get("mnemonic") or "").strip()
print(f"\n{W}[{i}/{len(nodes)}]{NC} {C}{hostname}{NC}")
if not mnemonic:
print(f" {R}{NC} Missing mnemonic")
results.append((hostname, False))
continue
try:
run([
nym_cli, "mixnet", "operators", "nymnode", "unbond",
"--mnemonic", mnemonic,
"--nyxd-url", NYXD_URL,
], args.dry_run)
print(f" {G}{NC} Unbonded successfully")
results.append((hostname, True))
except subprocess.CalledProcessError as e:
print(f" {R}{NC} Failed with exit code {e.returncode}")
results.append((hostname, False))
print(f"\n{W}{''*60}{NC}")
print(f" {W}SUMMARY{NC}")
print(f"{W}{''*60}{NC}")
for hostname, success in results:
status = f"{G}✓ OK {NC}" if success else f"{R}✗ FAILED{NC}"
print(f" {status} {C}{hostname}{NC}")
succeeded = sum(1 for _, s in results if s)
print(f"{W}{''*60}{NC}")
print(f" Total: {G}{succeeded} succeeded{NC} {R}{len(results) - succeeded} failed{NC}")
print(f"{W}{''*60}{NC}\n")
if __name__ == "__main__":
main()