Improved Sentinel Features - Enable Discovery & StaticNodes variable in config.json

I’m looking at ways to improve the sentinel / pillar setup where a user runs their own hardware / firewall and has more control over their network topology. Specifically, I would like to make the Discovery and StaticNodes variables configurable in the config.json file. The goal is to allow a user to setup a sentinel and pillar where the pillar sits in a VPC and it only connects to the sentinel for syncing. That way a pillar can be 100% isolated from the internet. Today, the Sentrify setup from Moon simply blocks access to all endpoints, but for an approved endpoint, at the local firewall level. This method works, but I suspect it would be more efficient to take advantage of the StaticNodes variable.

With the help of Cursor, I’ve laid out the next steps to implement this. @georgezgeorgez @vilkris do these steps look appropriate and are there any downstream issues you see with implementing this?


Making Discovery and StaticNodes Configurable via config.json

Currently, the discovery and staticNodes settings in the Zenon node are hardcoded and not configurable through config.json. This post explains the changes needed to make these settings configurable.

Current Implementation

Currently, these settings are hardcoded in node/node.go:

node.server = &p2p.Server{
    // ...
    Discovery: true,    // Always enabled
    StaticNodes: nil,   // Always empty
    // ...
}

The codebase already has robust node parsing functionality in p2p/discover/node.go that can parse node URLs in the format:

enode://<node-id>@<ip>:<port>

This is demonstrated by the existing Nodes() method in p2p/config.go which parses seeders:

func (c *Net) Nodes() ([]*discover.Node, error) {
    var err error
    nodes := make([]*discover.Node, len(c.Seeders))
    for index, nodeAddress := range c.Seeders {
        nodes[index], err = discover.ParseNode(nodeAddress)
        if err != nil {
            return nil, err
        }
    }
    return nodes, nil
}

Required Changes

1. Update the NetConfig Structure

First, we need to add the new fields to the NetConfig structure in node/config.go:

type NetConfig struct {
    ListenHost string
    ListenPort int

    MinPeers          int
    MinConnectedPeers int
    MaxPeers          int
    MaxPendingPeers   int

    Discovery    bool     // Add this field
    StaticNodes  []string // Add this field
    Seeders      []string
}

2. Update the Server Configuration

Modify the makeNetConfig function in node/config.go to include the new fields:

func (c *Config) makeNetConfig() *p2p.Net {
    networkDataDir := filepath.Join(c.DataPath, p2p.DefaultNetDirName)
    privateKeyFile := filepath.Join(c.DataPath, p2p.DefaultNetPrivateKeyFile)

    return &p2p.Net{
        PrivateKeyFile:    privateKeyFile,
        MaxPeers:          c.Net.MaxPeers,
        MaxPendingPeers:   c.Net.MaxPendingPeers,
        MinConnectedPeers: c.Net.MinConnectedPeers,
        Name:              fmt.Sprintf("%v %v", metadata.Version, c.Name),
        Discovery:         c.Net.Discovery,    // Add this line
        StaticNodes:       c.Net.StaticNodes,  // Add this line
        Seeders:           c.Net.Seeders,
        NodeDatabase:      networkDataDir,
        ListenAddr:        c.Net.ListenHost,
        ListenPort:        c.Net.ListenPort,
    }
}

3. Update Node Initialization

Modify the node initialization in node/node.go to use the config values and parse static nodes:

func NewNode(conf *Config) (*Node, error) {
    // ... existing code ...

    netConfig := conf.makeNetConfig()
    nodes, err := netConfig.Nodes()
    if err != nil {
        return nil, errors.Errorf("Unable to parse seeders. Reason: %v", err)
    }

    // Parse static nodes using the existing ParseNode functionality
    var staticNodes []*discover.Node
    if len(netConfig.StaticNodes) > 0 {
        staticNodes = make([]*discover.Node, len(netConfig.StaticNodes))
        for i, nodeStr := range netConfig.StaticNodes {
            staticNodes[i], err = discover.ParseNode(nodeStr)
            if err != nil {
                return nil, errors.Errorf("Unable to parse static node %s. Reason: %v", nodeStr, err)
            }
        }
    }

    node.server = &p2p.Server{
        PrivateKey:        netConfig.PrivateKey(),
        Name:              netConfig.Name,
        MaxPeers:          netConfig.MaxPeers,
        MinConnectedPeers: netConfig.MinConnectedPeers,
        MaxPendingPeers:   netConfig.MaxPendingPeers,
        Discovery:         netConfig.Discovery,    // Use config value
        NoDial:            false,
        StaticNodes:       staticNodes,           // Use parsed static nodes
        BootstrapNodes:    nodes,
        TrustedNodes:      nil,
        NodeDatabase:      netConfig.NodeDatabase,
        ListenAddr:        fmt.Sprintf("%v:%v", netConfig.ListenAddr, netConfig.ListenPort),
        Protocols:         node.z.Protocol().SubProtocols,
    }
    return node, nil
}

Example config.json

After these changes, you can configure discovery and static nodes in your config.json like this:

{
    "Net": {
        "ListenHost": "0.0.0.0",
        "ListenPort": 35995,
        "MaxPeers": 60,
        "MinConnectedPeers": 16,
        "MaxPendingPeers": 10,
        "Discovery": true,
        "StaticNodes": [
            "enode://<node-id>@<ip>:<port>",
            "enode://<node-id>@<ip>:<port>"
        ],
        "Seeders": [
            "enode://<node-id>@<ip>:<port>"
        ]
    }
}

Benefits

  1. Flexibility: Users can now configure whether to enable or disable peer discovery
  2. Control: Users can specify static nodes that should always be maintained
  3. Customization: Different nodes can have different discovery and connection strategies
  4. Reuse: Leverages existing node parsing functionality that’s already proven to work with seeders

Considerations

  1. When Discovery is set to false, make sure to provide enough StaticNodes to maintain network connectivity
  2. The StaticNodes list should contain valid enode URLs in the format: enode://<node-id>@<ip>:<port>
  3. Changes to these settings require a node restart to take effect
  4. The node parsing functionality is already well-tested through its use with seeders

If these changes make sense, I would like to propose a ZIP based on the discussion here.

Looking at the code a little more, if we are going to consider these changes, should we also look at activating TrustedNodes. Currently the value is nil

Purpose: The purpose of trusted nodes is to maintain connections with specific, highly trusted peers regardless of normal connection limits. This can be useful for:

  • Maintaining connections with critical infrastructure nodes
  • Ensuring connectivity with specific nodes even under high network load
  • Creating a trusted backbone network

Difference from Static Nodes: While static nodes are always reconnected when disconnected, trusted nodes have the additional privilege of bypassing the maximum peer limit.

Guys - I am utterly shocked. I was actually able to get this code to work. I have a test pillar syncing with a sentinel in a private subnet. Let’s see how fast it can sync from scratch.

Here is my test config.json

{
    "Net": {
        "MinPeers": 1,
        "MinConnectedPeers": 1,
        "MaxPeers": 1,
        "MaxPendingPeers": 1,
        "StaticNodes": [
            "enode://e422cec51b5440d1fabc3ea565666d8bda3525a809a7f21ff03e5af80e539e953fd80493ea69fbedee879f5ad728287276b6189f45604a919878f8c4c51dc3da@PRIVATE_IP:35995"]
    }
}
1 Like