Skip to content

Commit 80103e8

Browse files
MitchellLeeZAmitchell
andauthored
Release/v0.0.38 (#72)
* Reduced refresh interval to 10 minutes instead of 20 minutes. * Reduced immunity of new miners - since requests is lower and we didn't take into account multiple validators. * Updated snippet fetcher to not be seen as a bot (#71) Co-authored-by: mitchell <[email protected]> * Miner Weight Update (#70) * Refactored code. * Updated .gitignore to exclude output and results. * Added burn_weights_by_ranking. * Added unit tests. * Updated .gitignore. * Removed unwanted files. * Fixed as per PR comments. --------- Co-authored-by: mitchell <[email protected]> * Updated domain_is_recently_registered to fix error validating domain: can't subtract offset-naive and offset-aware datetimes. --------- Co-authored-by: mitchell <[email protected]>
1 parent b28eb76 commit 80103e8

File tree

9 files changed

+625
-41
lines changed

9 files changed

+625
-41
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@ __pycache__/
22
env-veridex
33
env/
44
miner/openai/
5+
miner/best_miner/
6+
output/
7+
results/
8+
.idea/
9+
.env

shared/environment_variables.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717
INITIAL_WEIGHT = 0.7
1818

1919
NEUTRAL_SCORE=10
20-
IMMUNITY_PERIOD = 350 # Ensures new miners have a full day to prove themselves, even if other miners have been idle.
20+
IMMUNITY_PERIOD = 100 # Ensures new miners have a full day to prove themselves, even if other miners have been idle.
2121
IMMUNITY_WEIGHT = 0.5

tests/manual/test_snippet_fetcher.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@
1010
from shared.veridex_protocol import SourceEvidence
1111
from validator.snippet_fetcher import SnippetFetcher
1212

13+
# Array of hard-coded test URLs for snippet fetching
14+
TEST_URLS = [
15+
"https://nmaahc.si.edu/explore/stories/unforgettable-nat-king-cole-flip-wilson-american-television",
16+
"https://www.pbs.org/wnet/nature/blog/killer-whale-fact-sheet/",
17+
"https://hms.harvard.edu/news/screen-time-brain"
18+
]
19+
1320
def get_config():
1421
parser = argparse.ArgumentParser()
1522
parser.add_argument("--custom", default="my_custom_value", help="Custom value")
@@ -52,10 +59,61 @@ async def main(url):
5259
duration = time.perf_counter() - start
5360
print(f"Time taken for {url}: {duration:.4f} seconds")
5461

62+
async def main_all_urls():
63+
"""Fetch all URLs in the TEST_URLS array"""
64+
miner_uid = 1
65+
snippet_fetcher = SnippetFetcher()
66+
67+
try:
68+
for i, url in enumerate(TEST_URLS):
69+
print(f"\n{'='*80}")
70+
print(f"Fetching URL {i+1}/{len(TEST_URLS)}: {url}")
71+
print(f"{'='*80}\n")
72+
73+
start = time.perf_counter()
74+
try:
75+
page_text = await snippet_fetcher.fetch_entire_page(f"test-{i}", miner_uid, url)
76+
duration = time.perf_counter() - start
77+
78+
print(f"\n{'='*80}")
79+
print(f"Result for {url}:")
80+
print(f"Length: {len(page_text)} characters")
81+
print(f"Time taken: {duration:.4f} seconds")
82+
print(f"{'='*80}\n")
83+
print(page_text[:500] + "..." if len(page_text) > 500 else page_text)
84+
print("\n")
85+
except Exception as e:
86+
duration = time.perf_counter() - start
87+
print(f"Error fetching {url}: {e} (took {duration:.4f} seconds)")
88+
finally:
89+
await snippet_fetcher.client.aclose()
90+
5591
# Entry point
5692
if __name__ == "__main__":
5793
parser = argparse.ArgumentParser(description="Run Snippet Fetcher for url.")
58-
parser.add_argument("--url", type=str, default="https://studyfinds.org/content-overload-streaming", help="Page to fetch")
94+
parser.add_argument(
95+
"--url",
96+
type=str,
97+
default=TEST_URLS[0],
98+
help="Page to fetch (default: first URL in TEST_URLS array)"
99+
)
100+
parser.add_argument(
101+
"--all",
102+
action="store_true",
103+
help="Fetch all URLs in TEST_URLS array"
104+
)
105+
parser.add_argument(
106+
"--list",
107+
action="store_true",
108+
help="List all URLs in TEST_URLS array"
109+
)
59110
args = parser.parse_args()
60111

61-
asyncio.run(main(args.url))
112+
if args.list:
113+
print("Available test URLs:")
114+
for i, url in enumerate(TEST_URLS, 1):
115+
print(f" {i}. {url}")
116+
elif args.all:
117+
asyncio.run(main_all_urls())
118+
else:
119+
asyncio.run(main(args.url))

tests/unit_tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import unittest
2+
import sys
3+
import os
4+
5+
# Add the parent directory to the path to import the validator module
6+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
7+
8+
# Create a comprehensive mock bittensor module before importing validator_daemon
9+
class MockBt:
10+
class logging:
11+
@staticmethod
12+
def set_trace():
13+
pass # Mock for debugging
14+
@staticmethod
15+
def warning(msg):
16+
print(f"WARNING: {msg}")
17+
18+
@staticmethod
19+
def info(msg):
20+
print(f"INFO: {msg}")
21+
22+
@staticmethod
23+
def error(msg):
24+
print(f"ERROR: {msg}")
25+
26+
@staticmethod
27+
def config(*args, **kwargs):
28+
pass
29+
30+
@staticmethod
31+
def add_args(*args, **kwargs):
32+
pass
33+
34+
class subtensor:
35+
@staticmethod
36+
def add_args(*args, **kwargs):
37+
pass
38+
39+
class wallet:
40+
@staticmethod
41+
def add_args(*args, **kwargs):
42+
pass
43+
44+
# Patch the bittensor module before importing
45+
sys.modules['bittensor'] = MockBt()
46+
47+
# Import the actual function and constants from validator_daemon
48+
from validator.validator_daemon import distribute_weights_by_ranking, DEFAULT_TOTAL_WEIGHT, RANKING_EMISSION_TOP_PERC
49+
50+
51+
class TestDistributeWeightsByRanking(unittest.TestCase):
52+
"""Test suite for distribute_weights_by_ranking function"""
53+
54+
def test_empty_scores(self):
55+
"""Test with empty scores array"""
56+
moving_scores = []
57+
result = distribute_weights_by_ranking(moving_scores)
58+
self.assertEqual(result, [])
59+
60+
def test_single_score(self):
61+
"""Test with single score"""
62+
moving_scores = [10.0]
63+
result = distribute_weights_by_ranking(moving_scores)
64+
65+
self.assertEqual(len(result), 1)
66+
self.assertEqual(result[0], int(DEFAULT_TOTAL_WEIGHT))
67+
self.assertEqual(sum(result), int(DEFAULT_TOTAL_WEIGHT))
68+
69+
def test_two_scores_descending(self):
70+
"""Test with two scores in descending order"""
71+
moving_scores = [10.0, 5.0]
72+
result = distribute_weights_by_ranking(moving_scores)
73+
74+
self.assertEqual(len(result), 2)
75+
# Top should get 50% of total_weight
76+
expected_top = int(DEFAULT_TOTAL_WEIGHT * RANKING_EMISSION_TOP_PERC)
77+
# Second should get 25% of total_weight
78+
expected_second = int(DEFAULT_TOTAL_WEIGHT * 0.25)
79+
# Remainder goes to top
80+
remainder = int(DEFAULT_TOTAL_WEIGHT) - expected_top - expected_second
81+
expected_top += remainder
82+
83+
self.assertEqual(result[0], expected_top) # Highest score gets top allocation
84+
self.assertEqual(result[1], expected_second)
85+
self.assertEqual(sum(result), int(DEFAULT_TOTAL_WEIGHT))
86+
87+
def test_two_scores_ascending(self):
88+
"""Test with two scores in ascending order - should still rank correctly"""
89+
moving_scores = [5.0, 10.0]
90+
result = distribute_weights_by_ranking(moving_scores)
91+
92+
self.assertEqual(len(result), 2)
93+
# Index 1 has higher score, so should get top allocation
94+
self.assertGreater(result[1], result[0])
95+
self.assertEqual(sum(result), int(DEFAULT_TOTAL_WEIGHT))
96+
97+
def test_geometric_progression(self):
98+
"""Test that weights follow geometric progression (50%, 25%, 12.5%, etc.)"""
99+
moving_scores = [10.0, 8.0, 6.0, 4.0, 2.0]
100+
result = distribute_weights_by_ranking(moving_scores)
101+
102+
self.assertEqual(len(result), 5)
103+
self.assertEqual(sum(result), int(DEFAULT_TOTAL_WEIGHT))
104+
105+
# Verify ranking order (higher scores get more weight)
106+
self.assertGreater(result[0], result[1]) # Top > second
107+
self.assertGreater(result[1], result[2]) # Second > third
108+
self.assertGreater(result[2], result[3]) # Third > fourth
109+
self.assertGreater(result[3], result[4]) # Fourth > fifth
110+
111+
# Verify approximate percentages (allowing for rounding)
112+
total = float(sum(result))
113+
self.assertAlmostEqual(result[0] / total, RANKING_EMISSION_TOP_PERC, delta=0.01)
114+
self.assertAlmostEqual(result[1] / total, 0.25, delta=0.01)
115+
self.assertAlmostEqual(result[2] / total, 0.125, delta=0.01)
116+
117+
def test_equal_scores(self):
118+
"""Test with equal scores - first one should get top allocation"""
119+
moving_scores = [5.0, 5.0, 5.0]
120+
result = distribute_weights_by_ranking(moving_scores)
121+
122+
self.assertEqual(len(result), 3)
123+
self.assertEqual(sum(result), int(DEFAULT_TOTAL_WEIGHT))
124+
# First index gets top allocation (stable sort)
125+
self.assertGreaterEqual(result[0], result[1])
126+
self.assertGreaterEqual(result[1], result[2])
127+
128+
def test_zero_scores(self):
129+
"""Test with all zero scores"""
130+
moving_scores = [0.0, 0.0, 0.0]
131+
result = distribute_weights_by_ranking(moving_scores)
132+
133+
self.assertEqual(len(result), 3)
134+
self.assertEqual(sum(result), int(DEFAULT_TOTAL_WEIGHT))
135+
# All should get equal allocation when scores are equal
136+
self.assertGreaterEqual(min(result), 0)
137+
138+
def test_mixed_positive_negative(self):
139+
"""Test with mixed positive and negative scores"""
140+
moving_scores = [10.0, -5.0, 3.0]
141+
result = distribute_weights_by_ranking(moving_scores)
142+
143+
self.assertEqual(len(result), 3)
144+
self.assertEqual(sum(result), int(DEFAULT_TOTAL_WEIGHT))
145+
# Highest score (10.0) should get most weight
146+
self.assertGreater(result[0], result[2]) # 10.0 > 3.0
147+
self.assertGreater(result[2], result[1]) # 3.0 > -5.0
148+
149+
def test_custom_total_weight(self):
150+
"""Test with custom total_weight"""
151+
moving_scores = [10.0, 5.0]
152+
custom_total = 1000.0
153+
result = distribute_weights_by_ranking(moving_scores, total_weight=custom_total)
154+
155+
self.assertEqual(len(result), 2)
156+
self.assertEqual(sum(result), int(custom_total))
157+
# Top should get 50% of custom_total
158+
expected_top = int(custom_total * RANKING_EMISSION_TOP_PERC)
159+
remainder = int(custom_total) - expected_top - int(custom_total * 0.25)
160+
expected_top += remainder
161+
self.assertEqual(result[0], expected_top)
162+
163+
def test_custom_top_percentage(self):
164+
"""Test with custom top_percentage"""
165+
moving_scores = [10.0, 5.0, 3.0]
166+
custom_percentage = 0.6 # 60% for top
167+
result = distribute_weights_by_ranking(moving_scores, top_percentage=custom_percentage)
168+
169+
self.assertEqual(len(result), 3)
170+
self.assertEqual(sum(result), int(DEFAULT_TOTAL_WEIGHT))
171+
172+
# Verify top gets approximately 60%
173+
total = float(sum(result))
174+
self.assertAlmostEqual(result[0] / total, custom_percentage, delta=0.01)
175+
# Second should get 30% (60% * 0.5)
176+
self.assertAlmostEqual(result[1] / total, 0.30, delta=0.01)
177+
178+
def test_large_number_of_scores(self):
179+
"""Test with many scores"""
180+
moving_scores = [float(i) for i in range(20, 0, -1)] # 20 down to 1
181+
result = distribute_weights_by_ranking(moving_scores)
182+
183+
self.assertEqual(len(result), 20)
184+
self.assertEqual(sum(result), int(DEFAULT_TOTAL_WEIGHT))
185+
186+
# Verify descending order
187+
for i in range(len(result) - 1):
188+
self.assertGreaterEqual(result[i], result[i + 1])
189+
190+
def test_very_small_scores(self):
191+
"""Test with very small score values"""
192+
moving_scores = [0.001, 0.002, 0.003]
193+
result = distribute_weights_by_ranking(moving_scores)
194+
195+
self.assertEqual(len(result), 3)
196+
self.assertEqual(sum(result), int(DEFAULT_TOTAL_WEIGHT))
197+
# Highest score should get most weight
198+
self.assertGreater(result[2], result[1])
199+
self.assertGreater(result[1], result[0])
200+
201+
def test_very_large_scores(self):
202+
"""Test with very large score values"""
203+
moving_scores = [1e10, 2e10, 3e10]
204+
result = distribute_weights_by_ranking(moving_scores)
205+
206+
self.assertEqual(len(result), 3)
207+
self.assertEqual(sum(result), int(DEFAULT_TOTAL_WEIGHT))
208+
# Highest score should get most weight
209+
self.assertGreater(result[2], result[1])
210+
self.assertGreater(result[1], result[0])
211+
212+
def test_sum_equals_total_weight(self):
213+
"""Test that sum always equals total_weight exactly"""
214+
test_cases = [
215+
[1.0],
216+
[1.0, 2.0],
217+
[1.0, 2.0, 3.0, 4.0, 5.0],
218+
[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
219+
]
220+
221+
for moving_scores in test_cases:
222+
with self.subTest(moving_scores=moving_scores):
223+
result = distribute_weights_by_ranking(moving_scores)
224+
self.assertEqual(sum(result), int(DEFAULT_TOTAL_WEIGHT),
225+
f"Failed for scores: {moving_scores}")
226+
227+
def test_ranking_preserved(self):
228+
"""Test that ranking is preserved (higher scores get more weight)"""
229+
moving_scores = [1.0, 5.0, 3.0, 7.0, 2.0]
230+
result = distribute_weights_by_ranking(moving_scores)
231+
232+
# Expected ranking: index 3 (7.0) > index 1 (5.0) > index 2 (3.0) > index 4 (2.0) > index 0 (1.0)
233+
self.assertGreater(result[3], result[1]) # 7.0 > 5.0
234+
self.assertGreater(result[1], result[2]) # 5.0 > 3.0
235+
self.assertGreater(result[2], result[4]) # 3.0 > 2.0
236+
self.assertGreater(result[4], result[0]) # 2.0 > 1.0
237+
238+
def test_all_weights_non_negative(self):
239+
"""Test that all weights are non-negative"""
240+
moving_scores = [-10.0, -5.0, 0.0, 5.0, 10.0]
241+
result = distribute_weights_by_ranking(moving_scores)
242+
243+
for weight in result:
244+
self.assertGreaterEqual(weight, 0, "All weights should be non-negative")
245+
246+
def test_returns_integers(self):
247+
"""Test that function returns list of integers"""
248+
moving_scores = [1.0, 2.0, 3.0]
249+
result = distribute_weights_by_ranking(moving_scores)
250+
251+
self.assertIsInstance(result, list)
252+
for weight in result:
253+
self.assertIsInstance(weight, int, "All weights should be integers")
254+
255+
256+
if __name__ == '__main__':
257+
unittest.main()
258+

validator/api_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444

4545
load_dotenv()
4646

47-
REFRESH_INTERVAL_SECONDS = 60 * 20
47+
REFRESH_INTERVAL_SECONDS = 60 * 10
4848
NUMBER_OF_MINERS = 3
4949

5050
semaphore = asyncio.Semaphore(5) # Limit to 10 threads at a time

validator/domain_validator.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import bittensor as bt
33
import sys
44
import asyncio
5-
from datetime import datetime
5+
from datetime import datetime, timezone
66

77
domain_limiter = asyncio.Semaphore(5) # Max 5 concurrent threads
88

@@ -26,7 +26,13 @@ async def domain_is_recently_registered(domain) -> bool:
2626
# Give benefit of doubt if error happened: return False
2727
return False
2828

29-
return (datetime.now() - creation_date).days <= 30 # Adjust the days threshold
29+
# Ensure both datetimes are timezone-aware for comparison
30+
now = datetime.now(timezone.utc)
31+
if creation_date.tzinfo is None:
32+
# If creation_date is naive, assume it's UTC
33+
creation_date = creation_date.replace(tzinfo=timezone.utc)
34+
35+
return (now - creation_date).days <= 30 # Adjust the days threshold
3036
except Exception as e:
3137
# Give benefit of doubt if error happened: return False
3238
bt.logging.error(f"Error validating domain: {e}")

0 commit comments

Comments
 (0)