Skip to content

Commit

Permalink
Fiber 2.0.0 (#26)
Browse files Browse the repository at this point in the history
- Made Non-encrypted communication the default, but still support encryption
- Few minor quality of life changes, nothing major
  • Loading branch information
namoray authored Dec 5, 2024
1 parent 199d643 commit 40ef465
Show file tree
Hide file tree
Showing 43 changed files with 1,162 additions and 109 deletions.
File renamed without changes.
59 changes: 59 additions & 0 deletions dev_utils/encrypted/run_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import os

from dotenv import load_dotenv

load_dotenv("dev.env")
import asyncio

import httpx
from cryptography.fernet import Fernet

from fiber.chain import chain_utils
from fiber.logging_utils import get_logger
from fiber.validator import client as vali_client
from fiber.validator import handshake

logger = get_logger(__name__)


async def main():
# Load needed stuff
wallet_name = os.getenv("WALLET_NAME", "default")
hotkey_name = os.getenv("HOTKEY_NAME", "default")
keypair = chain_utils.load_hotkey_keypair(wallet_name, hotkey_name)
httpx_client = httpx.AsyncClient()

# Handshake with miner
miner_address = "http://localhost:7999"
miner_hotkey_ss58_address = "5xyz_some_miner_hotkey"
symmetric_key_str, symmetric_key_uuid = await handshake.perform_handshake(
keypair=keypair,
httpx_client=httpx_client,
server_address=miner_address,
miner_hotkey_ss58_address=miner_hotkey_ss58_address,
)

if symmetric_key_str is None or symmetric_key_uuid is None:
raise ValueError("Symmetric key or UUID is None :-(")
else:
logger.info("Wohoo - handshake worked! :)")

fernet = Fernet(symmetric_key_str)

resp = await vali_client.make_non_streamed_post(
httpx_client=httpx_client,
server_address=miner_address,
fernet=fernet,
keypair=keypair,
symmetric_key_uuid=symmetric_key_uuid,
validator_ss58_address=keypair.ss58_address,
miner_ss58_address=miner_hotkey_ss58_address,
payload={},
endpoint="/example-subnet-request",
)
resp.raise_for_status()
logger.info(f"Example request sent! Response: {resp.text}")


if __name__ == "__main__":
asyncio.run(main())
27 changes: 27 additions & 0 deletions dev_utils/encrypted/start_miner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import os

from dotenv import load_dotenv

load_dotenv("dev.env") # Important to load this before importing anything else!

from fiber.encrypted.miner import server
from fiber.encrypted.miner.endpoints.subnet import factory_router as get_subnet_router
from fiber.encrypted.miner.middleware import configure_extra_logging_middleware
from fiber.logging_utils import get_logger

logger = get_logger(__name__)

app = server.factory_app(debug=True)

app.include_router(get_subnet_router())


if os.getenv("ENV", "dev").lower() == "dev":
configure_extra_logging_middleware(app)

if __name__ == "__main__":
import uvicorn

uvicorn.run(app, host="127.0.0.1", port=7999)

# Remember to fiber-post-ip to whatever testnet you are using!
23 changes: 3 additions & 20 deletions dev_utils/run_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@
import asyncio

import httpx
from cryptography.fernet import Fernet

from fiber.chain import chain_utils
from fiber.logging_utils import get_logger
from fiber.validator import client as vali_client
from fiber.validator import handshake
from fiber.validator import client as validator

logger = get_logger(__name__)

Expand All @@ -26,29 +24,14 @@ async def main():
# Handshake with miner
miner_address = "http://localhost:7999"
miner_hotkey_ss58_address = "5xyz_some_miner_hotkey"
symmetric_key_str, symmetric_key_uuid = await handshake.perform_handshake(
keypair=keypair,
httpx_client=httpx_client,
server_address=miner_address,
miner_hotkey_ss58_address=miner_hotkey_ss58_address,
)

if symmetric_key_str is None or symmetric_key_uuid is None:
raise ValueError("Symmetric key or UUID is None :-(")
else:
logger.info("Wohoo - handshake worked! :)")

fernet = Fernet(symmetric_key_str)

resp = await vali_client.make_non_streamed_post(
resp = await validator.make_non_streamed_post(
httpx_client=httpx_client,
server_address=miner_address,
fernet=fernet,
keypair=keypair,
symmetric_key_uuid=symmetric_key_uuid,
validator_ss58_address=keypair.ss58_address,
miner_ss58_address=miner_hotkey_ss58_address,
payload={},
payload={"hi": "there"},
endpoint="/example-subnet-request",
)
resp.raise_for_status()
Expand Down
2 changes: 1 addition & 1 deletion dev_utils/start_miner.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@
if __name__ == "__main__":
import uvicorn

uvicorn.run(app, host="127.0.0.1", port=7999)
uvicorn.run("start_miner:app", host="127.0.0.1", port=7999, reload=True)

# Remember to fiber-post-ip to whatever testnet you are using!
5 changes: 5 additions & 0 deletions fiber/chain/signatures.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import hashlib

from substrateinterface import Keypair

Expand All @@ -12,6 +13,10 @@ def sign_message(keypair: Keypair, message: str | None) -> str | None:
return f"0x{keypair.sign(message).hex()}"


def get_hash(body: bytes) -> str:
return hashlib.sha256(body).hexdigest()


def verify_signature(message: str | None, signature: str, signer_ss58_address: str) -> bool:
if message is None:
return False
Expand Down
1 change: 1 addition & 0 deletions fiber/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
EXCHANGE_SYMMETRIC_KEY_ENDPOINT = "exchange-symmetric-key"
PUBLIC_ENCRYPTION_KEY_ENDPOINT = "public-encryption-key"
SYMMETRIC_KEY_UUID = "symmetric-key-uuid"
HEADER_HASH = "header-hash"
HOTKEY = "hotkey"
MINER_HOTKEY = "miner-hotkey"
VALIDATOR_HOTKEY = "validator-hotkey"
Expand Down
1 change: 1 addition & 0 deletions fiber/encrypted/miner/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"Just here to help testing"
75 changes: 75 additions & 0 deletions fiber/encrypted/miner/core/configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import base64
import os
from functools import lru_cache
from typing import TypeVar

import httpx
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from dotenv import load_dotenv
from pydantic import BaseModel

from fiber.chain import chain_utils, interface
from fiber.chain.metagraph import Metagraph
from fiber.encrypted.miner.core import miner_constants as mcst
from fiber.encrypted.miner.core.models.config import Config
from fiber.encrypted.miner.security import key_management, nonce_management

T = TypeVar("T", bound=BaseModel)

load_dotenv()


def _derive_key_from_string(input_string: str, salt: bytes = b"salt_") -> str:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000,
)
key = base64.urlsafe_b64encode(kdf.derive(input_string.encode()))
return key.decode()


@lru_cache
def factory_config() -> Config:
nonce_manager = nonce_management.NonceManager()

wallet_name = os.getenv("WALLET_NAME", "default")
hotkey_name = os.getenv("HOTKEY_NAME", "default")
netuid = os.getenv("NETUID")
subtensor_network = os.getenv("SUBTENSOR_NETWORK")
subtensor_address = os.getenv("SUBTENSOR_ADDRESS")
load_old_nodes = bool(os.getenv("LOAD_OLD_NODES", True))
min_stake_threshold = int(os.getenv("MIN_STAKE_THRESHOLD", 1_000))
refresh_nodes = os.getenv("REFRESH_NODES", "true").lower() == "true"

assert netuid is not None, "Must set NETUID env var please!"

if refresh_nodes:
substrate = interface.get_substrate(subtensor_network, subtensor_address)
metagraph = Metagraph(
substrate=substrate,
netuid=netuid,
load_old_nodes=load_old_nodes,
)
else:
metagraph = Metagraph(substrate=None, netuid=netuid, load_old_nodes=load_old_nodes)

keypair = chain_utils.load_hotkey_keypair(wallet_name, hotkey_name)

storage_encryption_key = os.getenv("STORAGE_ENCRYPTION_KEY")
if storage_encryption_key is None:
storage_encryption_key = _derive_key_from_string(mcst.DEFAULT_ENCRYPTION_STRING)

encryption_keys_handler = key_management.EncryptionKeysHandler(
nonce_manager, storage_encryption_key, hotkey=hotkey_name
)

return Config(
encryption_keys_handler=encryption_keys_handler,
keypair=keypair,
metagraph=metagraph,
min_stake_threshold=min_stake_threshold,
httpx_client=httpx.AsyncClient(),
)
3 changes: 3 additions & 0 deletions fiber/encrypted/miner/core/miner_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SYMMETRIC_KEYS_FILENAME = "symmetric_keys.encrypted"
DEFAULT_ENCRYPTION_STRING = "default_encryption"
NONCE_WINDOW_NS = 120_000_000_000 # 2 minutes in nanoseconds
16 changes: 16 additions & 0 deletions fiber/encrypted/miner/core/models/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from dataclasses import dataclass

import httpx
from substrateinterface import Keypair

from fiber.chain.metagraph import Metagraph
from fiber.encrypted.miner.security import key_management


@dataclass
class Config:
encryption_keys_handler: key_management.EncryptionKeysHandler
keypair: Keypair
metagraph: Metagraph
min_stake_threshold: float
httpx_client: httpx.AsyncClient
File renamed without changes.
54 changes: 54 additions & 0 deletions fiber/encrypted/miner/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from fastapi import Depends, Header, HTTPException

from fiber import constants as cst
from fiber.chain import signatures
from fiber.encrypted import utils
from fiber.encrypted.miner.core import configuration
from fiber.encrypted.miner.core.models.config import Config
from fiber.logging_utils import get_logger

logger = get_logger(__name__)


def get_config() -> Config:
return configuration.factory_config()


async def verify_request(
validator_hotkey: str = Header(..., alias=cst.VALIDATOR_HOTKEY),
signature: str = Header(..., alias=cst.SIGNATURE),
miner_hotkey: str = Header(..., alias=cst.MINER_HOTKEY),
nonce: str = Header(..., alias=cst.NONCE),
symmetric_key_uuid: str = Header(..., alias=cst.SYMMETRIC_KEY_UUID),
config: Config = Depends(get_config),
):
if not config.encryption_keys_handler.nonce_manager.nonce_is_valid(nonce):
logger.debug("Nonce is not valid!")
raise HTTPException(
status_code=401,
detail="Oi, that nonce is not valid!",
)

if not signatures.verify_signature(
message=utils.construct_header_signing_message(nonce, miner_hotkey, symmetric_key_uuid),
signer_ss58_address=validator_hotkey,
signature=signature,
):
raise HTTPException(
status_code=401,
detail="Oi, invalid signature, you're not who you said you were!",
)


async def blacklist_low_stake(
validator_hotkey: str = Header(..., alias=cst.VALIDATOR_HOTKEY), config: Config = Depends(get_config)
):
metagraph = config.metagraph

node = metagraph.nodes.get(validator_hotkey)
if not node:
raise HTTPException(status_code=403, detail="Hotkey not found in metagraph")

if node.stake < config.min_stake_threshold:
logger.debug(f"Node {validator_hotkey} has insufficient stake of {node.stake} - minimum is {config.min_stake_threshold}")
raise HTTPException(status_code=403, detail=f"Insufficient stake of {node.stake} ")
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from fastapi import APIRouter, Depends, Header

from fiber import constants as cst
from fiber.encrypted.miner.core.configuration import Config
from fiber.encrypted.miner.core.models.encryption import PublicKeyResponse, SymmetricKeyExchange
from fiber.encrypted.miner.dependencies import blacklist_low_stake, get_config, verify_request
from fiber.encrypted.miner.security.encryption import get_symmetric_key_b64_from_payload
from fiber.logging_utils import get_logger
from fiber.miner.core.configuration import Config
from fiber.miner.core.models.encryption import PublicKeyResponse, SymmetricKeyExchange
from fiber.miner.dependencies import blacklist_low_stake, get_config, verify_request
from fiber.miner.security.encryption import get_symmetric_key_b64_from_payload

logger = get_logger(__name__)

Expand Down
38 changes: 38 additions & 0 deletions fiber/encrypted/miner/endpoints/subnet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
THIS IS AN EXAMPLE FILE OF A SUBNET ENDPOINT!
PLEASE IMPLEMENT YOUR OWN :)
"""

from functools import partial

from fastapi import Depends
from fastapi.routing import APIRouter
from pydantic import BaseModel

from fiber.encrypted.miner.dependencies import blacklist_low_stake, verify_request
from fiber.encrypted.miner.security.encryption import decrypt_general_payload


class ExampleSubnetRequest(BaseModel):
pass


async def example_subnet_request(
decrypted_payload: ExampleSubnetRequest = Depends(
partial(decrypt_general_payload, ExampleSubnetRequest),
),
):
return {"status": "Example request received"}


def factory_router() -> APIRouter:
router = APIRouter()
router.add_api_route(
"/example-subnet-request",
example_subnet_request,
tags=["Example"],
dependencies=[Depends(blacklist_low_stake), Depends(verify_request)],
methods=["POST"],
)
return router
Loading

0 comments on commit 40ef465

Please sign in to comment.