Skip to main content

Signed Messages

BlockZero uses cryptographically signed messages to authenticate miner submissions. This prevents validators from accepting forged submissions and ensures that every checkpoint is traceable to a registered miner hotkey.

SignedModelSubmitMessage

The SignedModelSubmitMessage is the data structure miners send to validators when submitting a trained checkpoint.

@dataclass
class SignedModelSubmitMessage:
hotkey: str # miner's SS58-encoded Bittensor hotkey address
expert_group: int # which expert group this submission covers
checkpoint_url: str # URL where the validator can download the checkpoint
block_number: int # the block at which this submission is valid
signature: str # URL-safe Base64 encoded ed25519 signature

Field details:

FieldTypeDescription
hotkeystringSS58-encoded public key (e.g., 5FHneW46...). Identifies the miner uniquely on the Bittensor network.
expert_groupintegerThe expert group ID this checkpoint was trained for. Must match the miner's registered group.
checkpoint_urlstringA publicly accessible URL from which the validator can download the checkpoint binary.
block_numberintegerThe block number at which this submission was created. Validators reject submissions with a block number outside the valid window.
signaturestringURL-safe Base64 encoded ed25519 signature over the canonical serialization of the above fields.

sign_message()

Miners call sign_message() to produce the signature field.

def sign_message(message: SignedModelSubmitMessage, keypair: Keypair) -> str:
"""
Signs the message fields using the miner's Bittensor keypair.
Returns: URL-safe Base64 encoded ed25519 signature string.
"""
canonical = canonicalize(message) # deterministic serialization
signature_bytes = keypair.sign(canonical)
return base64.urlsafe_b64encode(signature_bytes).decode('ascii')

Canonical serialization: The fields are concatenated in a fixed order to produce the bytes that are signed:

def canonicalize(msg: SignedModelSubmitMessage) -> bytes:
return (
msg.hotkey.encode('utf-8') +
b':' +
str(msg.expert_group).encode('utf-8') +
b':' +
msg.checkpoint_url.encode('utf-8') +
b':' +
str(msg.block_number).encode('utf-8')
)

The signing algorithm is ed25519 — the same algorithm Bittensor uses for all on-chain keypair operations. The private key is the miner's hotkey private key; it never leaves the miner's machine.

verify_message()

Validators call verify_message() to authenticate a submission before downloading the checkpoint.

def verify_message(message: SignedModelSubmitMessage, subtensor: Subtensor) -> bool:
"""
Verifies the message signature using the miner's public key from chain.
Returns: True if signature is valid and block number is in window.
"""
# 1. Fetch miner's public key from blockchain using their hotkey address
public_key = subtensor.get_hotkey_public_key(message.hotkey)
if public_key is None:
return False # hotkey not registered on subnet

# 2. Verify block number is within acceptable window
current_block = subtensor.block
if abs(current_block - message.block_number) > BLOCK_WINDOW:
return False # submission is stale or from the future

# 3. Verify the ed25519 signature
canonical = canonicalize(message)
signature_bytes = base64.urlsafe_b64decode(message.signature)
return public_key.verify(canonical, signature_bytes)

A submission that fails verify_message() is rejected immediately — the validator does not download the checkpoint.

URL-Safe Base64

Signatures are encoded using URL-safe Base64 (base64.urlsafe_b64encode in Python's standard library). This encoding:

  • Uses - instead of + and _ instead of /
  • Produces strings safe to use in HTTP headers, JSON values, and URL query parameters
  • Does not require additional percent-encoding
import base64

# Encoding
encoded = base64.urlsafe_b64encode(signature_bytes).decode('ascii')

# Decoding
decoded = base64.urlsafe_b64decode(encoded.encode('ascii'))

Two-Phase Commit Flow

The signed message is the second phase of a two-phase commit protocol. The full flow:

COMMIT PHASE:
1. Miner trains expert group → produces checkpoint_bytes
2. hash = sha256(checkpoint_bytes)
3. Miner calls WorkerChainCommit.commit_hash(hash, block)
4. Hash is recorded on-chain (immutable from this point)

SUBMIT PHASE:
5. Miner constructs SignedModelSubmitMessage with checkpoint_url
6. Miner calls sign_message() → produces signature
7. Miner POSTs the signed message to validator's /submit endpoint

VALIDATION:
8. Validator calls verify_message() → checks signature and block window
9. Validator fetches checkpoint from checkpoint_url
10. Validator computes sha256(checkpoint_bytes) and compares to on-chain hash
11. If hash matches → proceed to Proof-of-Loss evaluation
12. If hash mismatch → reject submission (miner tried to swap checkpoint after commit)

Why this prevents gaming:

Step 4 locks the checkpoint content before the submit phase opens. Step 12 enforces this lock. A miner who wants to observe other miners' submissions and then submit a better one would need to:

  • Commit a hash before seeing others' submissions (step 3), then
  • Submit a different checkpoint after seeing them (step 5), which would fail hash verification (step 12)

The two-phase commit makes front-running cryptographically impossible.

Security note

The private key used in sign_message() is the miner's hotkey private key. Store this key securely. Anyone with access to your hotkey private key can submit on your behalf and drain your rewards.

Use the BZ_WALLET_PASSWORD environment variable to protect your wallet file at rest.