Subnet Security

Securing a subnet starts with securing the Protocols, which is primarily where nodes communicate. It's important to implement security measures to avoid unwanted entries, record spam, sybil attacks, etc.

There are two main features to secure a subnet:


Authorizers are between peer-to-peer communication requests and responses. With Authorizers, custom logic can be deployed in protocols for all node communication. For example, an authorizer can include rate limiting, for example, in an inference subnet, require a proof-of-stake, for example, when joining a subnet, or require signing requests and responses for communication between nodes.

Note: The template comes with the above options, and developers can create their own custom Authorizers.

At a minimum, the DHT protocol (the main protocol for node communication and records storage) should implement the PoS authorizer to ensure only nodes with a proof of stake can join the subnet and access the records.

As well, at a minimum, each custom protocol should implement a signature authorizer.

Each DHT store request will first go through an Authorizer, if an Authorizer is implemented into the DHT Protocol (see example implementations below). For instance, records are stored through the DHT Protocol (see /dht/protocol.py). When you request another peer to store something, or another peer requests you to store something, both the request and response will go through the Authorizer first (see get_stub(p2p, peer) for a more detailed explanation), then the Record Validator is called before the record is stored.

Record validators validate the records that are stored in the decentralized database (see DHT Records).

Record validators enable use cases where the subnet can have database conditions, just like a standard database. Such as key, expiration, Pydantic schema validation conditions, owned and protected records, or any use case the subnet needs to validate records on both store requests and get requests.

For example, if utilizing a commit-reveal, you will likely implement the predicate validator or even create a custom validator. A predicate validator allows Python callables to be accessed when storing or getting data from the DHT records, such as accessing blockchain RPC methods. This can be used to have a commit-reveal scheme in-subnet to ensure commits and reveals can only be performed between specific blocks in specific epochs. For example, you can require f"commit-{current_epoch}" can only be stored up to the 50% mark of the epoch (the current_epoch and 50% mark can be accessed and calculated from values from the callable function).

At a minimum, a subnet should only allow keys that are required to be stored in the DHT records, and require a maximum expiration time in the storage for as long as it's required.

Having owned records can also be important, depending on the use case, to ensure that records cannot be updated by just anyone (see Signature Validators).


Working Together

Utilizing both an Authorizer and Record Validators together is a very powerful basis for security and expansiveness.


Implementing Authorizers

As previously mentioned, security comes down to securing protocols, where most of the communication takes place. To add an authorizer to the DHT Protocol, we do so from the DHT initialization since the DHT creates the DHT Protocol.

DHT

In this implementation, we start the DHT with a proof-of-stake authorizer that requires a proof of stake for all communications between all nodes.

Authorizers can include any logic and a mix of use cases.

identity_path = kwargs.get('identity_path', None)
pk = get_rsa_private_key(identity_path)
rsa_pos_authorizer = RSAProofOfStakeAuthorizer(
    local_private_key=pk,
    subnet_id=subnet_id,
    hypertensor=Hypertensor()
)
"""
Start DHT with PoS authorizer

- The authorizer is initialized into the DHTProtocol
"""
dht = DHT(
    initial_peers=initial_peers,
    start=True,
    num_workers=DEFAULT_NUM_WORKERS,
    use_relay=use_relay,
    use_auto_relay=use_auto_relay,
    client_mode=reachable_via_relay,
    record_validators=None,
    **dict(kwargs, authorizer=authorizer)
)

Custom Protocol

Start the protocol with an RSA signature protocol. Requiring signing between peers is important so each peer can know who is calling them for each request. Knowing who's requesting and responding to communications allows for more robust logic, such as rate limiting, blacklisting, etc..

identity_path = kwargs.get('identity_path', None)
pk = get_rsa_private_key(identity_path)
rsa_authorizer = TokenRSAAuthorizerBase(pk)
mock_protocol = MockProtocol(
    dht=dht,
    subnet_id=subnet_id,
    hypertensor=Hypertensor(),
    authorizer=rsa_authorizer,
    start=True
)

Implementing Record Validators

Unlike Authorizers, the DHT Protocol can initialize with multiple record validators that validate in order of priority. See Record Validator for more information.

In the following, we are creating two validators, one that creates signed and owned records so we know who owns the record and only they can update it, and a Hypertensor Predicate Validator for a commit-reveal scheme that ensures no record is stored for too long, when specific keys can be stored (tied to an epoch), validates key names, and validates values that ensure is abides from a Pydantic schema.

hypertensor = Hypertensor()
identity_path = kwargs.get('identity_path', None)
pk = get_rsa_private_key(identity_path)

# Create signed and owned records
rsa_signature_validator = RSASignatureValidator(pk)

# Create predicate validator for commit-reveal/key-value validations
predicate = hypertensor_consensus_predicate()
consensus_predicate = HypertensorPredicateValidator(
    hypertensor=hypertensor,
    record_predicate=predicate,
)

record_validators=[rsa_signature_validator, consensus_predicate]

dht = DHT(
    initial_peers=initial_peers,
    start=True,
    num_workers=DEFAULT_NUM_WORKERS,
    use_relay=use_relay,
    use_auto_relay=use_auto_relay,
    client_mode=reachable_via_relay,
    record_validators=record_validators,
    **kwargs,
)

Example of Authorizer and Record Validators Together

hypertensor = Hypertensor()
identity_path = kwargs.get('identity_path', None)
pk = get_rsa_private_key(identity_path)

"""
RECORD VALIDATORS
"""
# Create signed and owned records
rsa_signature_validator = RSASignatureValidator(pk)

# Create predicate validator for commit-reveal/key-value validations
predicate = hypertensor_consensus_predicate()
consensus_predicate = HypertensorPredicateValidator(
    hypertensor=hypertensor,
    record_predicate=predicate,
)

record_validators=[rsa_signature_validator, consensus_predicate]

"""
AUTHORIZER
"""

# DHT
rsa_pos_authorizer = RSAProofOfStakeAuthorizer(
    local_private_key=pk,
    subnet_id=subnet_id,
    hypertensor=hypertensor
)

# PROTOCOL
rsa_authorizer = TokenRSAAuthorizerBase(pk)

# START DHT
dht = DHT(
    initial_peers=initial_peers,
    start=True,
    num_workers=DEFAULT_NUM_WORKERS,
    use_relay=use_relay,
    use_auto_relay=use_auto_relay,
    client_mode=reachable_via_relay,
    record_validators=record_validators,
    **dict(kwargs, authorizer=authorgs,
)

# START PROTOCOL
mock_protocol = MockProtocol(
    dht=dht,
    subnet_id=subnet_id,
    hypertensor=hypertensor,
    authorizer=rsa_authorizer,
    start=True
)

Last updated