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.
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.
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,
)
See the RSA Signature Validator (
RSASignatureValidator(...)
)See the Hypertensor Predicate Validator Class (
HypertensorPredicateValidator(...)
)See the initialization of the Hypertensor Predicate Validator Class (
hypertensor_consensus_predicate()
)
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