Skip to content

Commit c543017

Browse files
committed
Add metagraph hotkeys tracking and RPC endpoints
- Add registered_hotkeys HashSet to ChainState for all metagraph neurons - Update validator_sync to populate all hotkeys during metagraph sync - Add metagraph_hotkeys and metagraph_isRegistered JSON-RPC endpoints - Optimize CI with cargo-nextest and skip coverage on PRs
1 parent 94f72c5 commit c543017

File tree

4 files changed

+127
-100
lines changed

4 files changed

+127
-100
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -16,65 +16,14 @@ env:
1616
REGISTRY: ghcr.io
1717
IMAGE_NAME: ${{ github.repository }}
1818

19-
# Cancel in-progress runs for the same branch
2019
concurrency:
2120
group: ${{ github.workflow }}-${{ github.ref }}
2221
cancel-in-progress: true
2322

2423
jobs:
25-
# Build job - creates shared cache for other jobs (only this job saves cache)
26-
build:
27-
name: Build
28-
runs-on: ubuntu-latest
29-
steps:
30-
- name: Free disk space
31-
run: |
32-
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
33-
sudo docker image prune --all --force
34-
35-
- uses: actions/checkout@v4
36-
37-
- uses: dtolnay/rust-toolchain@stable
38-
39-
- name: Cache cargo
40-
uses: Swatinem/rust-cache@v2
41-
with:
42-
cache-on-failure: true
43-
shared-key: "platform-ci"
44-
save-if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
45-
46-
- name: Build
47-
run: cargo build --release
48-
49-
# Clippy runs in parallel with build (reads cache only)
50-
clippy:
51-
name: Clippy
52-
runs-on: ubuntu-latest
53-
steps:
54-
- name: Free disk space
55-
run: |
56-
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
57-
sudo docker image prune --all --force
58-
59-
- uses: actions/checkout@v4
60-
61-
- uses: dtolnay/rust-toolchain@stable
62-
with:
63-
components: clippy
64-
65-
- name: Cache cargo
66-
uses: Swatinem/rust-cache@v2
67-
with:
68-
cache-on-failure: true
69-
shared-key: "platform-ci"
70-
save-if: false
71-
72-
- name: Run clippy
73-
run: cargo clippy --all-targets --workspace -- -W clippy::all
74-
75-
# Test job - runs faster with nextest (reads cache only)
76-
test:
77-
name: Test
24+
# Single CI job - faster than parallel because cache is restored once
25+
ci:
26+
name: Build & Test
7827
runs-on: ubuntu-latest
7928
permissions:
8029
contents: write
@@ -88,7 +37,7 @@ jobs:
8837

8938
- uses: dtolnay/rust-toolchain@stable
9039
with:
91-
components: llvm-tools-preview
40+
components: clippy,llvm-tools-preview
9241

9342
- name: Install cargo-nextest and cargo-llvm-cov
9443
uses: taiki-e/install-action@v2
@@ -99,21 +48,28 @@ jobs:
9948
uses: Swatinem/rust-cache@v2
10049
with:
10150
cache-on-failure: true
102-
shared-key: "platform-ci"
103-
save-if: false
51+
shared-key: "platform-ci-v1"
52+
53+
# Build first - subsequent steps reuse artifacts
54+
- name: Build
55+
run: cargo build --release --all-targets
56+
57+
# Clippy uses cached build artifacts
58+
- name: Clippy
59+
run: cargo clippy --all-targets --workspace -- -W clippy::all
10460

105-
# Run tests with nextest (faster) - coverage only on main
61+
# Tests - with coverage only on main
10662
- name: Run tests
107-
if: github.ref != format('refs/heads/{0}', github.event.repository.default_branch)
63+
if: github.ref != 'refs/heads/main'
10864
run: cargo nextest run --workspace -E 'not (test(/live/) | test(/integration/))'
10965

11066
- name: Run tests with coverage
111-
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
67+
if: github.ref == 'refs/heads/main'
11268
run: |
11369
cargo llvm-cov nextest --workspace --json --output-path coverage.json -E 'not (test(/live/) | test(/integration/))'
11470
11571
- name: Generate coverage badge
116-
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
72+
if: github.ref == 'refs/heads/main'
11773
run: |
11874
COVERAGE=$(jq '.data[0].totals.lines.percent // 0 | round' coverage.json)
11975
echo "Coverage: $COVERAGE%"
@@ -125,19 +81,19 @@ jobs:
12581
curl -s "https://img.shields.io/badge/coverage-${COVERAGE}%25-${COLOR}" > badges/coverage.svg
12682
12783
- name: Deploy coverage badge
128-
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
84+
if: github.ref == 'refs/heads/main'
12985
uses: peaceiris/actions-gh-pages@v4
13086
with:
13187
github_token: ${{ secrets.GITHUB_TOKEN }}
13288
publish_dir: ./badges
13389
destination_dir: badges
13490
keep_files: true
13591

136-
# Docker build - only after tests pass
92+
# Docker build - only after CI passes
13793
docker:
13894
name: Build & Push Docker
13995
runs-on: ubuntu-latest
140-
needs: [build, test]
96+
needs: [ci]
14197
permissions:
14298
contents: read
14399
packages: write
@@ -187,25 +143,19 @@ jobs:
187143
release:
188144
name: Release
189145
runs-on: ubuntu-latest
190-
needs: [build, test, docker]
146+
needs: [ci, docker]
191147
if: startsWith(github.ref, 'refs/tags/v')
192148
permissions:
193149
contents: write
194150
steps:
195-
- name: Free disk space
196-
run: |
197-
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
198-
sudo docker image prune --all --force
199-
200151
- uses: actions/checkout@v4
201152

202153
- uses: dtolnay/rust-toolchain@stable
203154

204155
- name: Cache cargo
205156
uses: Swatinem/rust-cache@v2
206157
with:
207-
shared-key: "platform-ci"
208-
save-if: false
158+
shared-key: "platform-ci-v1"
209159

210160
- name: Build release binaries
211161
run: cargo build --release

crates/bittensor-integration/src/validator_sync.rs

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,17 @@ impl ValidatorSync {
8888
.await
8989
.map_err(|e| SyncError::ClientError(e.to_string()))?;
9090

91-
// Parse validators from metagraph
92-
let bt_validators = self.parse_metagraph(metagraph)?;
91+
// Parse validators and all hotkeys from metagraph
92+
let (bt_validators, all_hotkeys) = self.parse_metagraph(metagraph)?;
9393
drop(client); // Release lock
9494

95-
// Update state
95+
// Update registered hotkeys in state (all miners + validators)
96+
{
97+
let mut state_guard = state.write();
98+
state_guard.registered_hotkeys = all_hotkeys;
99+
}
100+
101+
// Update state with validators
96102
let result = self.update_state(state, bt_validators, banned_validators);
97103

98104
// Update last sync block
@@ -106,49 +112,58 @@ impl ValidatorSync {
106112
Ok(result)
107113
}
108114

109-
/// Parse metagraph data to extract validators
110-
fn parse_metagraph(&self, metagraph: &Metagraph) -> Result<Vec<MetagraphValidator>, SyncError> {
115+
/// Parse metagraph data to extract validators and all registered hotkeys
116+
fn parse_metagraph(
117+
&self,
118+
metagraph: &Metagraph,
119+
) -> Result<(Vec<MetagraphValidator>, std::collections::HashSet<Hotkey>), SyncError> {
111120
let mut validators = Vec::new();
121+
let mut all_hotkeys = std::collections::HashSet::new();
112122

113123
// Parse neurons from metagraph
114124
for (uid, neuron) in &metagraph.neurons {
115125
// Convert AccountId32 hotkey to our Hotkey type
116126
let hotkey_bytes: &[u8; 32] = neuron.hotkey.as_ref();
117127
let hotkey = Hotkey(*hotkey_bytes);
118128

129+
// Add ALL hotkeys to the registered set (miners + validators)
130+
all_hotkeys.insert(hotkey.clone());
131+
119132
// Get effective stake: alpha stake + root stake (TAO on root subnet)
120133
// This matches how Bittensor calculates validator weight
121134
let alpha_stake = neuron.stake;
122135
let root_stake = neuron.root_stake;
123136
let effective_stake = alpha_stake.saturating_add(root_stake);
124137
let stake = effective_stake.min(u64::MAX as u128) as u64;
125138

126-
// Skip if below minimum stake
127-
if stake < self.min_stake {
128-
continue;
139+
// Only add to validators if above minimum stake
140+
if stake >= self.min_stake {
141+
// Extract normalized scores (u16 -> f64, divide by u16::MAX)
142+
let incentive = neuron.incentive / u16::MAX as f64;
143+
let trust = neuron.trust / u16::MAX as f64;
144+
let consensus = neuron.consensus / u16::MAX as f64;
145+
146+
// Check if active (has stake)
147+
let active = stake > 0;
148+
149+
validators.push(MetagraphValidator {
150+
hotkey,
151+
uid: *uid as u16,
152+
stake,
153+
active,
154+
incentive,
155+
trust,
156+
consensus,
157+
});
129158
}
130-
131-
// Extract normalized scores (u16 -> f64, divide by u16::MAX)
132-
let incentive = neuron.incentive / u16::MAX as f64;
133-
let trust = neuron.trust / u16::MAX as f64;
134-
let consensus = neuron.consensus / u16::MAX as f64;
135-
136-
// Check if active (has stake)
137-
let active = stake > 0;
138-
139-
validators.push(MetagraphValidator {
140-
hotkey,
141-
uid: *uid as u16,
142-
stake,
143-
active,
144-
incentive,
145-
trust,
146-
consensus,
147-
});
148159
}
149160

150-
debug!("Parsed {} validators from metagraph", validators.len());
151-
Ok(validators)
161+
debug!(
162+
"Parsed {} validators and {} total hotkeys from metagraph",
163+
validators.len(),
164+
all_hotkeys.len()
165+
);
166+
Ok((validators, all_hotkeys))
152167
}
153168

154169
/// Update chain state with validators from Bittensor

crates/core/src/state.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ pub struct ChainState {
5959

6060
/// Last update timestamp
6161
pub last_updated: chrono::DateTime<chrono::Utc>,
62+
63+
/// All registered hotkeys from metagraph (miners + validators)
64+
/// Updated during metagraph sync, used for submission verification
65+
#[serde(default)]
66+
pub registered_hotkeys: std::collections::HashSet<Hotkey>,
6267
}
6368

6469
impl ChainState {
@@ -78,6 +83,7 @@ impl ChainState {
7883
pending_jobs: Vec::new(),
7984
state_hash: [0u8; 32],
8085
last_updated: chrono::Utc::now(),
86+
registered_hotkeys: std::collections::HashSet::new(),
8187
};
8288
state.update_hash();
8389
state

crates/rpc-server/src/jsonrpc.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ impl RpcHandler {
349349
["validator", "get"] => self.validator_get(req.id, req.params),
350350
["validator", "count"] => self.validator_count(req.id),
351351

352+
// Metagraph namespace
353+
["metagraph", "hotkeys"] => self.metagraph_hotkeys(req.id),
354+
["metagraph", "isRegistered"] => self.metagraph_is_registered(req.id, req.params),
355+
352356
// Challenge namespace
353357
["challenge", "list"] => self.challenge_list(req.id, req.params),
354358
["challenge", "get"] => self.challenge_get(req.id, req.params),
@@ -891,6 +895,58 @@ impl RpcHandler {
891895
JsonRpcResponse::result(id, json!(chain.validators.len()))
892896
}
893897

898+
// ==================== Metagraph Namespace ====================
899+
900+
/// Get all registered hotkeys from metagraph (miners + validators)
901+
fn metagraph_hotkeys(&self, id: Value) -> JsonRpcResponse {
902+
let chain = self.chain_state.read();
903+
let hotkeys: Vec<String> = chain
904+
.registered_hotkeys
905+
.iter()
906+
.map(|h| h.to_hex())
907+
.collect();
908+
909+
JsonRpcResponse::result(
910+
id,
911+
json!({
912+
"count": hotkeys.len(),
913+
"hotkeys": hotkeys,
914+
}),
915+
)
916+
}
917+
918+
/// Check if a hotkey is registered in the metagraph
919+
fn metagraph_is_registered(&self, id: Value, params: Value) -> JsonRpcResponse {
920+
let hotkey = match self.get_param_str(&params, 0, "hotkey") {
921+
Some(h) => h,
922+
None => {
923+
return JsonRpcResponse::error(id, INVALID_PARAMS, "Missing 'hotkey' parameter")
924+
}
925+
};
926+
927+
let hk = match platform_core::Hotkey::from_hex(&hotkey) {
928+
Some(h) => h,
929+
None => {
930+
// Try SS58 format
931+
match platform_core::Hotkey::from_ss58(&hotkey) {
932+
Some(h) => h,
933+
None => return JsonRpcResponse::error(id, INVALID_PARAMS, "Invalid hotkey format"),
934+
}
935+
}
936+
};
937+
938+
let chain = self.chain_state.read();
939+
let is_registered = chain.registered_hotkeys.contains(&hk);
940+
941+
JsonRpcResponse::result(
942+
id,
943+
json!({
944+
"hotkey": hotkey,
945+
"isRegistered": is_registered,
946+
}),
947+
)
948+
}
949+
894950
// ==================== Challenge Namespace ====================
895951

896952
fn challenge_list(&self, id: Value, params: Value) -> JsonRpcResponse {

0 commit comments

Comments
 (0)