Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bins/design-challenge/tests/resanitize_backfill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ async fn seed_run(store: &MemoryDesignStore, id: &str, created_at_ms: u64) {
.insert_harness(&HarnessRow {
id: format!("h-{id}"),
miner_hotkey: "cd".repeat(32),
miner_coldkey: None,
agent_py: "def run(task, llm, out):\n pass\n".into(),
pyproject_toml: "[project]\nname='x'\nversion='0'\n".into(),
extra_files: BTreeMap::new(),
Expand Down
40 changes: 37 additions & 3 deletions crates/chain-live/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ pub use storage::{
decode_axon_info, decode_bool, decode_double_map_account_k2, decode_double_map_k2,
decode_hotkey, decode_metagraph, decode_u16, decode_u64, decode_vec_u64, decode_vec_vec_u8,
storage_double_map_key_u16_account, storage_double_map_key_u16_u16,
storage_double_map_prefix_u16, storage_key, storage_map_key_identity, storage_map_key_twox64,
storage_map_key_u16, ACCOUNT_ID_LEN,
storage_double_map_prefix_u16, storage_key, storage_map_key_account_blake2,
storage_map_key_identity, storage_map_key_twox64, storage_map_key_u16, ACCOUNT_ID_LEN,
};
pub use tlock::encrypt_commit;

Expand Down Expand Up @@ -181,8 +181,42 @@ impl LiveChainClient {
Some(block_hash)
};
let keys = self.enumerate_hotkeys(netuid, at)?;
let coldkeys = self.fetch_coldkeys_for_hotkeys(&keys, at)?;
let owner = self.read_owner_hotkey(netuid, at)?;
Ok(storage::decode_metagraph(keys, owner, netuid))
Ok(storage::decode_metagraph(keys, coldkeys, owner, netuid))
}

/// Bulk-read `SubtensorModule.Owner(hotkey) → coldkey` for every hotkey.
///
/// Uses batched `state_queryStorageAt` (same path as `Keys`), never
/// per-UID RPCs. Missing / default (all-zero) owners become zero vectors
/// so the UID alignment with `hotkeys` is preserved.
fn fetch_coldkeys_for_hotkeys(
&self,
hotkeys: &[Vec<u8>],
at: Option<&[u8; 32]>,
) -> Result<Vec<Vec<u8>>, ChainError> {
if hotkeys.is_empty() {
return Ok(Vec::new());
}
let mut storage_keys = Vec::with_capacity(hotkeys.len());
let mut index_of: HashMap<Vec<u8>, usize> = HashMap::with_capacity(hotkeys.len());
for (i, hk) in hotkeys.iter().enumerate() {
let account = account_id(hk)?;
let sk = storage::storage_map_key_account_blake2(PALLET_SUBTENSOR, "Owner", &account);
index_of.insert(sk.clone(), i);
storage_keys.push(sk);
}
let mut coldkeys = vec![vec![0_u8; ACCOUNT_ID_LEN]; hotkeys.len()];
for chunk in storage_keys.chunks(256) {
for (key, value) in self.rpc.state_query_storage_at(chunk, at)? {
let Some(&i) = index_of.get(&key) else {
continue;
};
coldkeys[i] = storage::decode_hotkey(&value)?;
}
}
Ok(coldkeys)
}

/// Connect and load a signing key from a file (32 raw bytes or 64 hex chars).
Expand Down
24 changes: 23 additions & 1 deletion crates/chain-live/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,34 @@ pub fn decode_hotkey(bytes: &[u8]) -> Result<Vec<u8>, ChainError> {
}
}

/// Map key with the `Blake2_128Concat` hasher over an `AccountId32`.
///
/// Used by `SubtensorModule.Owner` (hotkey → coldkey). Layout:
/// `Twox128(pallet) ++ Twox128(item) ++ blake2_128(account) ++ account`.
#[must_use]
pub fn storage_map_key_account_blake2(
pallet: &str,
item: &str,
account: &[u8; ACCOUNT_ID_LEN],
) -> Vec<u8> {
let mut k = storage_key(pallet, item);
k.extend_from_slice(&blake2_128(account));
k.extend_from_slice(account);
k
}

/// Build a [`Metagraph`] from decoded storage values.
#[must_use]
pub fn decode_metagraph(keys: Vec<Vec<u8>>, owner: Vec<u8>, netuid: u16) -> Metagraph {
pub fn decode_metagraph(
keys: Vec<Vec<u8>>,
coldkeys: Vec<Vec<u8>>,
owner: Vec<u8>,
netuid: u16,
) -> Metagraph {
Metagraph {
netuid,
hotkeys: keys,
coldkeys,
owner_hotkey: owner,
}
}
9 changes: 7 additions & 2 deletions crates/chain-live/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,12 @@ fn decode_hotkey_option_some() {
#[test]
fn decode_metagraph_builds_correctly() {
let keys = vec![vec![0xAA; 32], vec![0xBB; 32]];
let coldkeys = vec![vec![0x11; 32], vec![0x22; 32]];
let owner = vec![0xCC; 32];
let mg = decode_metagraph(keys.clone(), owner.clone(), 1);
let mg = decode_metagraph(keys.clone(), coldkeys.clone(), owner.clone(), 1);
assert_eq!(mg.netuid, 1);
assert_eq!(mg.hotkeys, keys);
assert_eq!(mg.coldkeys, coldkeys);
assert_eq!(mg.owner_hotkey, owner);
}

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

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

// Two batched reads per refresh: Keys values, then Owner(hotkey) coldkeys.
Mock::given(method("POST"))
.and(body_partial_json(json!({"method": "state_queryStorageAt"})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
Expand All @@ -685,7 +690,7 @@ async fn mount_metagraph_mocks(server: &MockServer, keys_paged_times: u64) {
]
}]
})))
.expect(keys_paged_times)
.expect(keys_paged_times.saturating_mul(2))
.mount(server)
.await;

Expand Down
16 changes: 16 additions & 0 deletions crates/chain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ pub struct Metagraph {
pub netuid: u16,
/// Neuron hotkeys in UID order.
pub hotkeys: Vec<Vec<u8>>,
/// Coldkey owning each hotkey (`SubtensorModule.Owner`), UID-aligned with
/// [`Self::hotkeys`]. Empty when the backend did not resolve owners; an
/// all-zero entry means the chain default (unknown / unset).
pub coldkeys: Vec<Vec<u8>>,
/// Owner hotkey for the subnet (may equal first neuron or a dedicated owner).
pub owner_hotkey: Vec<u8>,
}
Expand Down Expand Up @@ -404,6 +408,10 @@ pub struct FakeChainConfig {
pub owner_hotkey: Vec<u8>,
/// Neuron hotkeys (UID order).
pub hotkeys: Vec<Vec<u8>>,
/// Optional coldkeys UID-aligned with [`Self::hotkeys`]. Empty → each
/// neuron is treated as self-owned (coldkey == hotkey) so tests that do
/// not care about shared coldkeys keep unique owners.
pub coldkeys: Vec<Vec<u8>>,
/// Published axons as `(hotkey, info)`; hotkeys absent here have never served.
pub axons: Vec<(Vec<u8>, AxonInfo)>,
/// Number of subsequent weight submits that should return [`ChainError::RateLimited`].
Expand All @@ -426,6 +434,7 @@ impl Default for FakeChainConfig {
blocks_since_last_step: fake_defaults::BLOCKS_SINCE_LAST_STEP,
owner_hotkey: vec![0xA1; 32],
hotkeys: vec![vec![0xA1; 32], vec![0xB2; 32], vec![0xC3; 32]],
coldkeys: Vec::new(),
axons: Vec::new(),
rate_limit_fails_remaining: 0,
}
Expand Down Expand Up @@ -550,9 +559,16 @@ impl ChainClient for FakeChain {
if !found {
return Err(ChainError::UnknownMetagraph);
}
let coldkeys = if self.cfg.coldkeys.is_empty() {
// Default: each hotkey owns itself (no shared-coldkey collisions).
self.cfg.hotkeys.clone()
} else {
self.cfg.coldkeys.clone()
};
Ok(Metagraph {
netuid: self.cfg.netuid,
hotkeys: self.cfg.hotkeys.clone(),
coldkeys,
owner_hotkey: self.cfg.owner_hotkey.clone(),
})
}
Expand Down
19 changes: 19 additions & 0 deletions crates/challenge-agentic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,22 @@ pub use types::{
pub fn crate_name() -> &'static str {
"challenge-agentic"
}

/// Same economic miner for copy/similarity corpora: matching hotkey, or both
/// coldkeys known and equal (case-insensitive). Used when 1-max gating forces
/// hotkey rotation under one coldkey.
#[must_use]
pub fn same_miner_identity(
hotkey_a: &str,
coldkey_a: Option<&str>,
hotkey_b: &str,
coldkey_b: Option<&str>,
) -> bool {
if hotkey_a.eq_ignore_ascii_case(hotkey_b) {
return true;
}
match (coldkey_a, coldkey_b) {
(Some(a), Some(b)) if !a.is_empty() && !b.is_empty() => a.eq_ignore_ascii_case(b),
_ => false,
}
}
6 changes: 6 additions & 0 deletions crates/challenge-common/src/expected_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,13 +236,19 @@ mod tests {
miner_hk().to_vec(),
validator_hk().to_vec(),
],
coldkeys: vec![
owner_hk().to_vec(),
miner_hk().to_vec(),
validator_hk().to_vec(),
],
owner_hotkey: owner_hk().to_vec(),
}
}

fn meta_after_late_registration() -> Metagraph {
let mut m = meta_at_block_b();
m.hotkeys.push(late_miner_hk().to_vec());
m.coldkeys.push(late_miner_hk().to_vec());
m
}

Expand Down
26 changes: 26 additions & 0 deletions crates/db/migrations/0014_miner_coldkey.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- Persist miner coldkey (SubtensorModule.Owner) at intake so similarity /
-- copy corpora can exclude same-coldkey prior art after a miner iterates via
-- a new hotkey (1-max gating forces hotkey rotation under one coldkey).
-- Nullable for legacy rows; new intakes fill it from the metagraph cache.

ALTER TABLE design_harness
ADD COLUMN miner_coldkey TEXT;

ALTER TABLE design_harness
ADD CONSTRAINT design_harness_miner_coldkey_hex
CHECK (miner_coldkey IS NULL OR miner_coldkey ~ '^[0-9a-f]{64}$');

CREATE INDEX ix_design_harness_coldkey
ON design_harness (miner_coldkey)
WHERE miner_coldkey IS NOT NULL;
Comment on lines +9 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n 'sqlx::migrate|Migrator|migrate!|no_transaction|no-tx|CREATE INDEX' \
  --glob '*.rs' --glob '*.toml' --glob '*.sql' .

Repository: BaseIntelligence/base

Length of output: 5550


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- candidate files ---\n'
fd -a 'lib.rs|0014_miner_coldkey.sql' .

printf '\n--- crates/db/src/lib.rs relevant section ---\n'
sed -n '90,115p' crates/db/src/lib.rs

printf '\n--- 0014_miner_coldkey.sql line-numbered ---\n'
cat -n crates/db/migrations/0014_miner_coldkey.sql

printf '\n--- migrations containing BEGIN/COMMIT/LOCK/CREATE INDEX NOT CONCURRENTLY/ALTER TABLE ... VALIDATE CONSTRAINT ---\n'
python3 - <<'PY'
import subprocess, re, pathlib
sql_files = sorted(pathlib.Path('crates/db/migrations').glob('*.sql'))
for f in sql_files:
    text = f.read_text()
    if any(tok in text.upper() for tok in ['BEGIN WORK', 'BEGIN TRANSACTION', 'CREATE INDEX', 'LOCK TABLE', 'ALTER TABLE', 'NO VALIDATE', 'VALIDATE CONSTRAINT', 'ON DATABASE', 'ON ROLE', 'ROLLBACK']):
        print(f'-- {f}')
        for i,line in enumerate(text.splitlines(),1):
            if re.search(r'(^|\s)(BEGIN WORK|BEGIN TRANSACTION|COMMIT|ROLLBACK|CREATE INDEX|LOCK TABLE|ALTER TABLE|ON TABLE|NOT VALID|VALIDATE CONSTRAINT|ON DATABASE|ON ROLE)\b', line.strip().upper()):
                print(f'{i}: {line}')
PY

printf '\n--- repo config for migrations/deploy docs mentioning migration outage/lock/concurrent ---\n'
rg -n 'migration|concurrent|CREATE INDEX|VALIDATE CONSTRAINT|LOCK|outage|CREATE INDEX CONCURRENTLY|sqlx' \
  --glob '!target/**' --glob '!node_modules/**' \
  README.md docs crates Cargo.toml 2>/dev/null || true

Repository: BaseIntelligence/base

Length of output: 250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
fd 'lib.rs|0014_miner_coldkey.sql' .

printf '%s\n' ''
printf '%s\n' '--- crates/db/src/lib.rs relevant section ---'
sed -n '90,115p' crates/db/src/lib.rs

printf '%s\n' ''
printf '%s\n' '--- 0014_miner_coldkey.sql line-numbered ---'
cat -n crates/db/migrations/0014_miner_coldkey.sql

printf '%s\n' ''
printf '%s\n' '--- migrations containing relevant SQL statements ---'
python3 - <<'PY'
import re
from pathlib import Path
for f in sorted(Path('crates/db/migrations').glob('*.sql')):
    text = f.read_text()
    tokens = ['BEGIN WORK', 'BEGIN TRANSACTION', 'COMMIT', 'ROLLBACK', 'CREATE INDEX', 'LOCK TABLE', 'ALTER TABLE', 'NOT VALID', 'VALIDATE CONSTRAINT']
    if any(tok in text.upper() for tok in tokens):
        print(f'-- {f}')
        for i, line in enumerate(text.splitlines(), 1):
            stripped = line.strip().upper()
            if any(tok in stripped for tok in tokens) or re.search(r'ALTER TABLE\s+\w+\s+ADD\s+CONSTRAINT\s+\w+\s+CHECK', stripped) or re.search(r'CREATE\s+INDEX\s+(CONCURRENTLY\s+)?\w+', stripped):
                print(f'{i}: {line}')
PY

printf '%s\n' ''
printf '%s\n' '--- deployment/migration guidance mentions ---'
rg -n 'migration|concurrent|CREATE INDEX|VALIDATE CONSTRAINT|LOCK|outage|CREATE INDEX CONCURRENTLY|sqlx|deploy|deployment plan' \
  --glob '!target/**' --glob '!node_modules/**' \
  README.md docs crates Cargo.toml 2>/dev/null || true

Repository: BaseIntelligence/base

Length of output: 50377


Use an online deployment plan for 0014_miner_coldkey.sql.

sqlx::migrate!("./migrations").run(pool) runs this as a transaction. The added CHECK constraints scan existing rows during ADD CONSTRAINT, and the two new indexes acquire table writes while they build. Split the migration into non-transactional online steps: use NOT VALID for both constraints, add the indexes with CREATE INDEX CONCURRENTLY, then schedule a separate VALIDATE CONSTRAINT step. If that cannot be automated, document the required write outage before deploying this migration.

🧰 Tools
🪛 Squawk (2.61.0)

[warning] 10-11: By default new constraints require a table scan and block writes to the table while that scan occurs. Use NOT VALID with a later VALIDATE CONSTRAINT call.

(constraint-missing-not-valid)


[warning] 13-15: During normal index creation, table updates are blocked, but reads are still allowed. Use concurrently to avoid blocking writes.

(require-concurrent-index-creation)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/db/migrations/0014_miner_coldkey.sql` around lines 9 - 15, Revise
migration 0014_miner_coldkey.sql for the transactional sqlx::migrate!
deployment: add the CHECK constraint as NOT VALID, create the partial index with
CREATE INDEX CONCURRENTLY, and move constraint validation into a separate
non-transactional deployment step using VALIDATE CONSTRAINT. If the migration
framework cannot execute these steps online, document the required write outage
instead.

Source: Linters/SAST tools


ALTER TABLE prism_submission
ADD COLUMN miner_coldkey TEXT;

ALTER TABLE prism_submission
ADD CONSTRAINT prism_submission_miner_coldkey_hex
CHECK (miner_coldkey IS NULL OR miner_coldkey ~ '^[0-9a-f]{64}$');

CREATE INDEX ix_prism_submission_coldkey
ON prism_submission (miner_coldkey)
WHERE miner_coldkey IS NOT NULL;
16 changes: 11 additions & 5 deletions crates/db/src/prism_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub struct PrismSubmissionRow {
pub id: String,
/// Miner hotkey (lowercase 64 hex).
pub miner_hotkey: String,
/// Owning coldkey (lowercase 64 hex), when known at intake.
pub miner_coldkey: Option<String>,
/// Epoch at acceptance.
pub epoch: i64,
/// Netuid.
Expand Down Expand Up @@ -61,6 +63,8 @@ pub struct NewPrismSubmission<'a> {
pub id: &'a str,
/// miner hotkey.
pub miner_hotkey: &'a str,
/// owning coldkey (optional).
pub miner_coldkey: Option<&'a str>,
/// epoch.
pub epoch: i64,
/// netuid.
Expand All @@ -85,9 +89,9 @@ pub struct NewPrismStageEvent<'a> {
}

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

/// Insert the queued row.
///
Expand All @@ -98,11 +102,13 @@ pub async fn insert_prism_submission(
n: &NewPrismSubmission<'_>,
) -> Result<(), DbError> {
sqlx::query(
"INSERT INTO prism_submission (id, miner_hotkey, epoch, netuid, status, label, architecture_py, training_py) \
VALUES ($1, $2, $3, $4, 'queued', $5, $6, $7)",
"INSERT INTO prism_submission \
(id, miner_hotkey, miner_coldkey, epoch, netuid, status, label, architecture_py, training_py) \
VALUES ($1, $2, $3, $4, $5, 'queued', $6, $7, $8)",
)
.bind(n.id)
.bind(n.miner_hotkey)
.bind(n.miner_coldkey)
.bind(n.epoch)
.bind(n.netuid)
.bind(n.label)
Expand Down
Loading
Loading