Skip to main content

bittensor_core/pow/
mod.rs

1//! Public registration challenge mining. GPU drivers are optional and loaded at
2//! runtime; no signing material enters the miner or device memory.
3
4mod opencl;
5
6use sha2::{Digest, Sha256};
7use sp_core::{hashing::keccak_256, U256};
8
9pub use opencl::{gpu_devices, GpuDevice, GpuMiner};
10
11pub const PREFIX_LEN: usize = 123;
12pub const MAX_BATCH: u32 = 1_048_576;
13pub type Solution = (u64, [u8; 32]);
14
15pub fn seal(prefix: &[u8], nonce: u64) -> Result<[u8; 32], String> {
16    validate_prefix(prefix)?;
17    let mut hasher = Sha256::new();
18    hasher.update(prefix);
19    hasher.update(nonce.to_le_bytes());
20    Ok(keccak_256(&hasher.finalize()))
21}
22
23fn validate_prefix(prefix: &[u8]) -> Result<(), String> {
24    if prefix.len() != PREFIX_LEN || !prefix.starts_with(b"subtensor-pow-register-v1") {
25        return Err("expected the 123-byte subnet registration challenge prefix".into());
26    }
27    if prefix.get(25..27) == Some(&[0, 0]) {
28        return Err("PoW registration is unavailable on root".into());
29    }
30    Ok(())
31}
32
33fn target(difficulty: u64) -> Result<U256, String> {
34    if difficulty == 0 {
35        return Err("PoW difficulty must be positive".into());
36    }
37    U256::MAX
38        .checked_div(U256::from(difficulty))
39        .ok_or_else(|| "PoW difficulty must be positive".into())
40}
41
42fn validate_batch(prefix: &[u8], difficulty: u64, attempts: u32) -> Result<U256, String> {
43    validate_prefix(prefix)?;
44    if attempts == 0 || attempts > MAX_BATCH {
45        return Err(format!("PoW batch must contain 1..{MAX_BATCH} attempts"));
46    }
47    target(difficulty)
48}
49
50/// Bounded Rust CPU fallback; callers refresh the chain challenge between batches.
51pub fn mine_cpu(
52    prefix: &[u8],
53    difficulty: u64,
54    start: u64,
55    attempts: u32,
56) -> Result<Option<Solution>, String> {
57    let limit = validate_batch(prefix, difficulty, attempts)?;
58    for offset in 0..attempts {
59        let nonce = start.wrapping_add(u64::from(offset));
60        let work = seal(prefix, nonce)?;
61        if U256::from_little_endian(&work) <= limit {
62            return Ok(Some((nonce, work)));
63        }
64    }
65    Ok(None)
66}
67
68#[cfg(test)]
69mod tests {
70    #![allow(
71        clippy::unwrap_used,
72        clippy::expect_used,
73        clippy::indexing_slicing,
74        clippy::arithmetic_side_effects
75    )]
76    use super::*;
77
78    pub(super) fn prefix() -> Vec<u8> {
79        let mut bytes = b"subtensor-pow-register-v1".to_vec();
80        bytes.extend_from_slice(&1u16.to_le_bytes());
81        bytes.extend_from_slice(&[1u8; 32]);
82        bytes.extend_from_slice(&[2u8; 32]);
83        bytes.extend_from_slice(&[3u8; 32]);
84        bytes
85    }
86
87    #[test]
88    fn seal_matches_independent_python_vectors() {
89        let prefix = prefix();
90        for (nonce, expected) in [
91            (
92                0,
93                "7cdaced691a70795a7656ad9b76ef2c16b961686a14a790bcb22eed6226ef103",
94            ),
95            (
96                1,
97                "c2291b632879447b6fe0785b0f25b5151c85a7e07df23a7860cf029e8a03f466",
98            ),
99            (
100                u64::MAX,
101                "69f1aef50783893195365b4fb62a4745085c31fee525a0984bc20301ab377aaf",
102            ),
103        ] {
104            assert_eq!(hex::encode(seal(&prefix, nonce).unwrap()), expected);
105        }
106    }
107
108    #[test]
109    fn cpu_mining_preserves_nonce_and_little_endian_target() {
110        let prefix = prefix();
111        let solution = mine_cpu(&prefix, 1, u64::MAX, 2).unwrap().unwrap();
112        assert_eq!(solution.0, u64::MAX);
113        assert_eq!(solution.1, seal(&prefix, u64::MAX).unwrap());
114        assert_eq!(target(2).unwrap(), U256::MAX / U256::from(2));
115        assert_eq!(target(u64::MAX).unwrap(), U256::MAX / U256::from(u64::MAX));
116    }
117
118    #[test]
119    #[ignore = "requires physical OpenCL GPUs; run explicitly on the GPU test host"]
120    fn gpu_mines_and_revalidates_chain_compatible_work() {
121        let mut miner = GpuMiner::new(None).expect("hardware test requires GPUs");
122        assert!(!miner.devices().is_empty());
123        let prefix = prefix();
124        for start in [0, u64::MAX - 100] {
125            let (nonce, work) = miner.mine(&prefix, 10, start, 4096).unwrap().unwrap();
126            assert_eq!(work, seal(&prefix, nonce).unwrap());
127            assert!(U256::from_little_endian(&work) <= target(10).unwrap());
128        }
129    }
130
131    #[test]
132    fn invalid_challenges_and_unbounded_batches_are_rejected() {
133        let prefix = prefix();
134        assert!(mine_cpu(&prefix, 0, 0, 1).is_err());
135        assert!(mine_cpu(&prefix, 1, 0, 0).is_err());
136        assert!(mine_cpu(&prefix, 1, 0, MAX_BATCH + 1).is_err());
137        assert!(mine_cpu(&prefix[..122], 1, 0, 1).is_err());
138        let mut root = prefix.clone();
139        root[25..27].fill(0);
140        assert!(seal(&root, 1).is_err());
141    }
142}