Skip to content

Commit 392cdfa

Browse files
authored
Merge pull request #82 from BaseIntelligence/fix/similarity-exclude-coldkey
fix(similarity): exclude same-coldkey prior art (Design + Prism)
2 parents 6e159e2 + 99bb2d8 commit 392cdfa

40 files changed

Lines changed: 497 additions & 156 deletions

File tree

bins/design-challenge/tests/resanitize_backfill.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ async fn seed_run(store: &MemoryDesignStore, id: &str, created_at_ms: u64) {
4040
.insert_harness(&HarnessRow {
4141
id: format!("h-{id}"),
4242
miner_hotkey: "cd".repeat(32),
43+
miner_coldkey: None,
4344
agent_py: "def run(task, llm, out):\n pass\n".into(),
4445
pyproject_toml: "[project]\nname='x'\nversion='0'\n".into(),
4546
extra_files: BTreeMap::new(),

crates/chain-live/src/lib.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ pub use storage::{
2525
decode_axon_info, decode_bool, decode_double_map_account_k2, decode_double_map_k2,
2626
decode_hotkey, decode_metagraph, decode_u16, decode_u64, decode_vec_u64, decode_vec_vec_u8,
2727
storage_double_map_key_u16_account, storage_double_map_key_u16_u16,
28-
storage_double_map_prefix_u16, storage_key, storage_map_key_identity, storage_map_key_twox64,
29-
storage_map_key_u16, ACCOUNT_ID_LEN,
28+
storage_double_map_prefix_u16, storage_key, storage_map_key_account_blake2,
29+
storage_map_key_identity, storage_map_key_twox64, storage_map_key_u16, ACCOUNT_ID_LEN,
3030
};
3131
pub use tlock::encrypt_commit;
3232

@@ -181,8 +181,42 @@ impl LiveChainClient {
181181
Some(block_hash)
182182
};
183183
let keys = self.enumerate_hotkeys(netuid, at)?;
184+
let coldkeys = self.fetch_coldkeys_for_hotkeys(&keys, at)?;
184185
let owner = self.read_owner_hotkey(netuid, at)?;
185-
Ok(storage::decode_metagraph(keys, owner, netuid))
186+
Ok(storage::decode_metagraph(keys, coldkeys, owner, netuid))
187+
}
188+
189+
/// Bulk-read `SubtensorModule.Owner(hotkey) → coldkey` for every hotkey.
190+
///
191+
/// Uses batched `state_queryStorageAt` (same path as `Keys`), never
192+
/// per-UID RPCs. Missing / default (all-zero) owners become zero vectors
193+
/// so the UID alignment with `hotkeys` is preserved.
194+
fn fetch_coldkeys_for_hotkeys(
195+
&self,
196+
hotkeys: &[Vec<u8>],
197+
at: Option<&[u8; 32]>,
198+
) -> Result<Vec<Vec<u8>>, ChainError> {
199+
if hotkeys.is_empty() {
200+
return Ok(Vec::new());
201+
}
202+
let mut storage_keys = Vec::with_capacity(hotkeys.len());
203+
let mut index_of: HashMap<Vec<u8>, usize> = HashMap::with_capacity(hotkeys.len());
204+
for (i, hk) in hotkeys.iter().enumerate() {
205+
let account = account_id(hk)?;
206+
let sk = storage::storage_map_key_account_blake2(PALLET_SUBTENSOR, "Owner", &account);
207+
index_of.insert(sk.clone(), i);
208+
storage_keys.push(sk);
209+
}
210+
let mut coldkeys = vec![vec![0_u8; ACCOUNT_ID_LEN]; hotkeys.len()];
211+
for chunk in storage_keys.chunks(256) {
212+
for (key, value) in self.rpc.state_query_storage_at(chunk, at)? {
213+
let Some(&i) = index_of.get(&key) else {
214+
continue;
215+
};
216+
coldkeys[i] = storage::decode_hotkey(&value)?;
217+
}
218+
}
219+
Ok(coldkeys)
186220
}
187221

188222
/// Connect and load a signing key from a file (32 raw bytes or 64 hex chars).

crates/chain-live/src/storage.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,34 @@ pub fn decode_hotkey(bytes: &[u8]) -> Result<Vec<u8>, ChainError> {
218218
}
219219
}
220220

221+
/// Map key with the `Blake2_128Concat` hasher over an `AccountId32`.
222+
///
223+
/// Used by `SubtensorModule.Owner` (hotkey → coldkey). Layout:
224+
/// `Twox128(pallet) ++ Twox128(item) ++ blake2_128(account) ++ account`.
225+
#[must_use]
226+
pub fn storage_map_key_account_blake2(
227+
pallet: &str,
228+
item: &str,
229+
account: &[u8; ACCOUNT_ID_LEN],
230+
) -> Vec<u8> {
231+
let mut k = storage_key(pallet, item);
232+
k.extend_from_slice(&blake2_128(account));
233+
k.extend_from_slice(account);
234+
k
235+
}
236+
221237
/// Build a [`Metagraph`] from decoded storage values.
222238
#[must_use]
223-
pub fn decode_metagraph(keys: Vec<Vec<u8>>, owner: Vec<u8>, netuid: u16) -> Metagraph {
239+
pub fn decode_metagraph(
240+
keys: Vec<Vec<u8>>,
241+
coldkeys: Vec<Vec<u8>>,
242+
owner: Vec<u8>,
243+
netuid: u16,
244+
) -> Metagraph {
224245
Metagraph {
225246
netuid,
226247
hotkeys: keys,
248+
coldkeys,
227249
owner_hotkey: owner,
228250
}
229251
}

crates/chain-live/src/tests.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,12 @@ fn decode_hotkey_option_some() {
132132
#[test]
133133
fn decode_metagraph_builds_correctly() {
134134
let keys = vec![vec![0xAA; 32], vec![0xBB; 32]];
135+
let coldkeys = vec![vec![0x11; 32], vec![0x22; 32]];
135136
let owner = vec![0xCC; 32];
136-
let mg = decode_metagraph(keys.clone(), owner.clone(), 1);
137+
let mg = decode_metagraph(keys.clone(), coldkeys.clone(), owner.clone(), 1);
137138
assert_eq!(mg.netuid, 1);
138139
assert_eq!(mg.hotkeys, keys);
140+
assert_eq!(mg.coldkeys, coldkeys);
139141
assert_eq!(mg.owner_hotkey, owner);
140142
}
141143

@@ -653,6 +655,8 @@ async fn mock_metagraph_at() {
653655
assert_eq!(mg.hotkeys.len(), 2);
654656
assert_eq!(mg.hotkeys[0], vec![0xAA; 32]);
655657
assert_eq!(mg.hotkeys[1], vec![0xBB; 32]);
658+
// Owner mock returns Keys-shaped changes; unmatched Owner keys stay zero.
659+
assert_eq!(mg.coldkeys.len(), 2);
656660
assert_eq!(mg.owner_hotkey, vec![0xCC; 32]);
657661
}
658662

@@ -673,6 +677,7 @@ async fn mount_metagraph_mocks(server: &MockServer, keys_paged_times: u64) {
673677
.mount(server)
674678
.await;
675679

680+
// Two batched reads per refresh: Keys values, then Owner(hotkey) coldkeys.
676681
Mock::given(method("POST"))
677682
.and(body_partial_json(json!({"method": "state_queryStorageAt"})))
678683
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
@@ -685,7 +690,7 @@ async fn mount_metagraph_mocks(server: &MockServer, keys_paged_times: u64) {
685690
]
686691
}]
687692
})))
688-
.expect(keys_paged_times)
693+
.expect(keys_paged_times.saturating_mul(2))
689694
.mount(server)
690695
.await;
691696

crates/chain/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ pub struct Metagraph {
6363
pub netuid: u16,
6464
/// Neuron hotkeys in UID order.
6565
pub hotkeys: Vec<Vec<u8>>,
66+
/// Coldkey owning each hotkey (`SubtensorModule.Owner`), UID-aligned with
67+
/// [`Self::hotkeys`]. Empty when the backend did not resolve owners; an
68+
/// all-zero entry means the chain default (unknown / unset).
69+
pub coldkeys: Vec<Vec<u8>>,
6670
/// Owner hotkey for the subnet (may equal first neuron or a dedicated owner).
6771
pub owner_hotkey: Vec<u8>,
6872
}
@@ -404,6 +408,10 @@ pub struct FakeChainConfig {
404408
pub owner_hotkey: Vec<u8>,
405409
/// Neuron hotkeys (UID order).
406410
pub hotkeys: Vec<Vec<u8>>,
411+
/// Optional coldkeys UID-aligned with [`Self::hotkeys`]. Empty → each
412+
/// neuron is treated as self-owned (coldkey == hotkey) so tests that do
413+
/// not care about shared coldkeys keep unique owners.
414+
pub coldkeys: Vec<Vec<u8>>,
407415
/// Published axons as `(hotkey, info)`; hotkeys absent here have never served.
408416
pub axons: Vec<(Vec<u8>, AxonInfo)>,
409417
/// Number of subsequent weight submits that should return [`ChainError::RateLimited`].
@@ -426,6 +434,7 @@ impl Default for FakeChainConfig {
426434
blocks_since_last_step: fake_defaults::BLOCKS_SINCE_LAST_STEP,
427435
owner_hotkey: vec![0xA1; 32],
428436
hotkeys: vec![vec![0xA1; 32], vec![0xB2; 32], vec![0xC3; 32]],
437+
coldkeys: Vec::new(),
429438
axons: Vec::new(),
430439
rate_limit_fails_remaining: 0,
431440
}
@@ -550,9 +559,16 @@ impl ChainClient for FakeChain {
550559
if !found {
551560
return Err(ChainError::UnknownMetagraph);
552561
}
562+
let coldkeys = if self.cfg.coldkeys.is_empty() {
563+
// Default: each hotkey owns itself (no shared-coldkey collisions).
564+
self.cfg.hotkeys.clone()
565+
} else {
566+
self.cfg.coldkeys.clone()
567+
};
553568
Ok(Metagraph {
554569
netuid: self.cfg.netuid,
555570
hotkeys: self.cfg.hotkeys.clone(),
571+
coldkeys,
556572
owner_hotkey: self.cfg.owner_hotkey.clone(),
557573
})
558574
}

crates/challenge-agentic/src/lib.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,22 @@ pub use types::{
3636
pub fn crate_name() -> &'static str {
3737
"challenge-agentic"
3838
}
39+
40+
/// Same economic miner for copy/similarity corpora: matching hotkey, or both
41+
/// coldkeys known and equal (case-insensitive). Used when 1-max gating forces
42+
/// hotkey rotation under one coldkey.
43+
#[must_use]
44+
pub fn same_miner_identity(
45+
hotkey_a: &str,
46+
coldkey_a: Option<&str>,
47+
hotkey_b: &str,
48+
coldkey_b: Option<&str>,
49+
) -> bool {
50+
if hotkey_a.eq_ignore_ascii_case(hotkey_b) {
51+
return true;
52+
}
53+
match (coldkey_a, coldkey_b) {
54+
(Some(a), Some(b)) if !a.is_empty() && !b.is_empty() => a.eq_ignore_ascii_case(b),
55+
_ => false,
56+
}
57+
}

crates/challenge-common/src/expected_set.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,13 +236,19 @@ mod tests {
236236
miner_hk().to_vec(),
237237
validator_hk().to_vec(),
238238
],
239+
coldkeys: vec![
240+
owner_hk().to_vec(),
241+
miner_hk().to_vec(),
242+
validator_hk().to_vec(),
243+
],
239244
owner_hotkey: owner_hk().to_vec(),
240245
}
241246
}
242247

243248
fn meta_after_late_registration() -> Metagraph {
244249
let mut m = meta_at_block_b();
245250
m.hotkeys.push(late_miner_hk().to_vec());
251+
m.coldkeys.push(late_miner_hk().to_vec());
246252
m
247253
}
248254

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
-- Persist miner coldkey (SubtensorModule.Owner) at intake so similarity /
2+
-- copy corpora can exclude same-coldkey prior art after a miner iterates via
3+
-- a new hotkey (1-max gating forces hotkey rotation under one coldkey).
4+
-- Nullable for legacy rows; new intakes fill it from the metagraph cache.
5+
6+
ALTER TABLE design_harness
7+
ADD COLUMN miner_coldkey TEXT;
8+
9+
ALTER TABLE design_harness
10+
ADD CONSTRAINT design_harness_miner_coldkey_hex
11+
CHECK (miner_coldkey IS NULL OR miner_coldkey ~ '^[0-9a-f]{64}$');
12+
13+
CREATE INDEX ix_design_harness_coldkey
14+
ON design_harness (miner_coldkey)
15+
WHERE miner_coldkey IS NOT NULL;
16+
17+
ALTER TABLE prism_submission
18+
ADD COLUMN miner_coldkey TEXT;
19+
20+
ALTER TABLE prism_submission
21+
ADD CONSTRAINT prism_submission_miner_coldkey_hex
22+
CHECK (miner_coldkey IS NULL OR miner_coldkey ~ '^[0-9a-f]{64}$');
23+
24+
CREATE INDEX ix_prism_submission_coldkey
25+
ON prism_submission (miner_coldkey)
26+
WHERE miner_coldkey IS NOT NULL;

crates/db/src/prism_store.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pub struct PrismSubmissionRow {
1616
pub id: String,
1717
/// Miner hotkey (lowercase 64 hex).
1818
pub miner_hotkey: String,
19+
/// Owning coldkey (lowercase 64 hex), when known at intake.
20+
pub miner_coldkey: Option<String>,
1921
/// Epoch at acceptance.
2022
pub epoch: i64,
2123
/// Netuid.
@@ -61,6 +63,8 @@ pub struct NewPrismSubmission<'a> {
6163
pub id: &'a str,
6264
/// miner hotkey.
6365
pub miner_hotkey: &'a str,
66+
/// owning coldkey (optional).
67+
pub miner_coldkey: Option<&'a str>,
6468
/// epoch.
6569
pub epoch: i64,
6670
/// netuid.
@@ -85,9 +89,9 @@ pub struct NewPrismStageEvent<'a> {
8589
}
8690

8791
/// Column list shared by all row reads.
88-
const COLS: &str = "id, miner_hotkey, epoch, netuid, status, label, architecture_py, training_py, \
89-
pod_id, pod_provider, receipt_json, metrics_json, bpb, review_json, similarity_json, kind, \
90-
score, absence_reason, retry_count, error_detail";
92+
const COLS: &str = "id, miner_hotkey, miner_coldkey, epoch, netuid, status, label, \
93+
architecture_py, training_py, pod_id, pod_provider, receipt_json, metrics_json, bpb, \
94+
review_json, similarity_json, kind, score, absence_reason, retry_count, error_detail";
9195

9296
/// Insert the queued row.
9397
///
@@ -98,11 +102,13 @@ pub async fn insert_prism_submission(
98102
n: &NewPrismSubmission<'_>,
99103
) -> Result<(), DbError> {
100104
sqlx::query(
101-
"INSERT INTO prism_submission (id, miner_hotkey, epoch, netuid, status, label, architecture_py, training_py) \
102-
VALUES ($1, $2, $3, $4, 'queued', $5, $6, $7)",
105+
"INSERT INTO prism_submission \
106+
(id, miner_hotkey, miner_coldkey, epoch, netuid, status, label, architecture_py, training_py) \
107+
VALUES ($1, $2, $3, $4, $5, 'queued', $6, $7, $8)",
103108
)
104109
.bind(n.id)
105110
.bind(n.miner_hotkey)
111+
.bind(n.miner_coldkey)
106112
.bind(n.epoch)
107113
.bind(n.netuid)
108114
.bind(n.label)

0 commit comments

Comments
 (0)