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:
| Field | Type | Description |
|---|---|---|
hotkey | string | SS58-encoded public key (e.g., 5FHneW46...). Identifies the miner uniquely on the Bittensor network. |
expert_group | integer | The expert group ID this checkpoint was trained for. Must match the miner's registered group. |
checkpoint_url | string | A publicly accessible URL from which the validator can download the checkpoint binary. |
block_number | integer | The block number at which this submission was created. Validators reject submissions with a block number outside the valid window. |
signature | string | URL-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.
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.