diff --git a/docs/miner_tutorial.md b/docs/miner_tutorial.md index b65c95c0..327730df 100644 --- a/docs/miner_tutorial.md +++ b/docs/miner_tutorial.md @@ -34,6 +34,7 @@ - [4.1. Restrict ingress to validator IPs](#41-restrict-ingress-to-validator-ips) - [4.2. Update and run the blacklist function](#42-update-and-run-the-blacklist-function) - [4.3. Blacklist parameters](#43-blacklist-parameters) +- [5. Set up your public miner profile](#5-set-up-your-public-miner-profile) ### 1. Requirements @@ -451,6 +452,39 @@ The new blacklist function applies a minimum of **65000** stake by default. This - `--blacklist.validator_min_stake 65000` — gates requests by validator stake. The blacklist enforces a minimum of `65000`, even if you configure a lower value. [Back to top ^][table-of-contents] +## 5. Set up your public miner profile + +You can attach public identity metadata to your coldkey — a display name, an avatar image and social handles — shown on your miner profile page on the dashboard. Ownership is proven with an sr25519 signature made with your coldkey: no account or API key is needed. + +Run the one-shot submission script on whatever machine holds your coldkey (the key never leaves the machine; only the signature is sent). It prompts for your wallet password to sign, then submits: + +```shell +python scripts/submit_miner_profile.py \ + --wallet_name my_wallet \ + --display-name "My Mining Co" \ + --avatar-url https://example.com/logo.png \ + --twitter my_handle \ + --discord my_handle \ + --linkedin https://www.linkedin.com/in/my-handle \ + --website https://example.com +``` + +Every submission replaces all six fields, so pass all the values you want shown each time — an omitted field is cleared. To remove your metadata entirely: + +```shell +python scripts/submit_miner_profile.py --wallet_name my_wallet --delete +``` + +Field constraints: + +- `--display-name` — at most 40 printable characters. +- `--twitter` / `--discord` — bare handles (no URL), at most 64 characters from letters, digits and `_ . - #`. +- `--avatar-url` / `--website` / `--linkedin` — `https://` URLs of at most 300 characters; the linkedin URL must be on `linkedin.com` (the website can be any host). + +Your coldkey must be (or have been) registered on the subnet, and the signed message expires after 5 minutes — the script signs and submits in one go, so this only matters if your machine's clock is badly off. + +[Back to top ^][table-of-contents] + [table-of-contents]: #table-of-contents diff --git a/scripts/submit_miner_profile.py b/scripts/submit_miner_profile.py new file mode 100644 index 00000000..78b23d9c --- /dev/null +++ b/scripts/submit_miner_profile.py @@ -0,0 +1,132 @@ +"""Set up (or clear) your public miner profile metadata. + +Signs a metadata payload (display name, avatar, social handles) with your +coldkey — the sr25519 signature is the proof of ownership, no account +needed — and submits it to the miner dashboard API. The metadata is +rendered on your public miner profile page. + +Run this one-shot script on whatever machine holds your coldkey (it never +leaves the machine; only the signature is sent). Each submission replaces +all six fields: an omitted field is cleared. Passing --delete removes +your metadata entirely. + +Field constraints (enforced server-side): +- display name: at most 40 printable characters +- twitter / discord: bare handles (no URL), at most 64 characters from + letters, digits, ``_ . - #`` +- avatar / website / linkedin: https:// URLs of at most 300 characters; + the linkedin URL must be on linkedin.com (website can be any host) +""" + +import argparse +import hashlib +import json +import time + +import bittensor +import requests + +# To skip the interactive decrypt prompt, set WALLET_PASS in your environment, e.g.: +# export WALLET_PASS='...' +# Avoid hardcoding wallet passwords in this script. + + +def canonical_payload_hash(payload: dict) -> str: + canonical = json.dumps( + {key: value for key, value in payload.items() if value}, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ) + return hashlib.sha256(canonical.encode()).hexdigest() + + +def main(args): + if args.delete: + payload = { + "display_name": "", + "avatar_url": "", + "twitter": "", + "discord": "", + "linkedin": "", + "website": "", + } + else: + payload = { + "display_name": args.display_name or "", + "avatar_url": args.avatar_url or "", + "twitter": args.twitter or "", + "discord": args.discord or "", + "linkedin": args.linkedin or "", + "website": args.website or "", + } + if not args.delete and not any(payload.values()): + raise SystemExit( + "Refusing to submit empty metadata. Provide at least one field or use --delete." + ) + if args.wallet_path: + wallet = bittensor.Wallet(name=args.wallet_name, path=args.wallet_path) + else: + wallet = bittensor.Wallet(name=args.wallet_name) + keypair = wallet.coldkey + + timestamp = int(time.time()) + message = ( + f"synth-profile-metadata|v1|{keypair.ss58_address}" + f"|{timestamp}|{canonical_payload_hash(payload)}" + ) + signature = keypair.sign(data=message) + + response = requests.put( + f"{args.api_url}/v1/profiles/{keypair.ss58_address}/metadata", + json={ + "payload": payload, + "timestamp": timestamp, + "signature": signature.hex(), + }, + timeout=30, + ) + print(f"{response.status_code} {response.text}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Sign and submit your public miner profile metadata" + ) + parser.add_argument( + "--wallet_name", type=str, required=True, help="Name of the wallet" + ) + parser.add_argument( + "--wallet_path", + type=str, + help="Path to the wallets directory (default ~/.bittensor/wallets)", + ) + parser.add_argument("--display-name", type=str, help="Public display name") + parser.add_argument( + "--avatar-url", type=str, help="https:// URL of your avatar image" + ) + parser.add_argument("--twitter", type=str, help="Twitter/X handle") + parser.add_argument("--discord", type=str, help="Discord handle") + parser.add_argument( + "--linkedin", type=str, help="https://www.linkedin.com/... URL" + ) + parser.add_argument( + "--website", type=str, help="https:// URL of your website" + ) + parser.add_argument( + "--delete", + action="store_true", + help="Remove your profile metadata entirely", + ) + parser.add_argument( + "--api-url", + type=str, + default="https://monitoring.synthdata.co", + help="Miner dashboard API base URL", + ) + args = parser.parse_args() + + main(args) + +# Example usage: +# python scripts/submit_miner_profile.py --wallet_name my_wallet --display-name "My Mining Co" --avatar-url https://example.com/logo.png --twitter my_handle