Skip to content

Commit 940bbfd

Browse files
committed
fix non ipv4 ips
1 parent 1c70dc9 commit 940bbfd

4 files changed

Lines changed: 176 additions & 3 deletions

File tree

subvortex/core/metagraph/metagraph.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import subvortex.core.model.neuron.neuron as scmm
1111
import subvortex.core.metagraph.database as scmd
1212
import subvortex.core.metagraph.settings as scms
13+
import subvortex.core.utils as scsu
1314

1415

1516
class MetagraphObserver:
@@ -179,7 +180,7 @@ async def _resync(self) -> dict[str, str]:
179180
else (
180181
# Get the country for the ip if provided
181182
sccc.get_country(new_neuron.ip)
182-
if new_neuron.ip != "0.0.0.0"
183+
if new_neuron.ip != "0.0.0.0" and scsu.is_valid_ipv4(new_neuron.ip)
183184
else None
184185
)
185186
)
@@ -193,6 +194,7 @@ async def _resync(self) -> dict[str, str]:
193194
current_neuron
194195
and current_neuron.country is None
195196
and current_neuron.ip != "0.0.0.0"
197+
and scsu.is_valid_ipv4(new_neuron.ip)
196198
)
197199

198200
if new_neuron == current_neuron and not has_country_none:
@@ -251,8 +253,6 @@ async def _resync(self) -> dict[str, str]:
251253
country is None and new_neuron.ip != "0.0.0.0"
252254
)
253255

254-
print(f"{country} {new_neuron.ip} {has_missing_country}")
255-
256256
updated_neurons.append(new_neuron)
257257

258258
# 🔥 Remove neurons in Redis that are no longer in the metagraph

subvortex/core/tests/src/test_metagraph_observer.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,3 +616,109 @@ async def test_resync_removes_stale_neuron(observer):
616616

617617
observer.database.remove_neurons.assert_called_once_with([stored])
618618
observer.database.update_neurons.assert_called_once_with([new])
619+
620+
621+
@pytest.mark.asyncio
622+
async def test_non_ipv4_address_not_considered_change(observer):
623+
fake_proto = MagicMock()
624+
fake_proto.uid = 10
625+
fake_proto.axon_info.ip = "::1" # Not an IPv4
626+
fake_proto.hotkey = "ipv6_hotkey"
627+
628+
# Original neuron had the same IP and no country
629+
stored = scmm.Neuron(uid=10, ip="::1", hotkey="ipv6_hotkey", country=None)
630+
observer.metagraph = AsyncMock()
631+
observer.metagraph.neurons = [fake_proto]
632+
observer.database.get_neurons = AsyncMock(return_value={"ipv6_hotkey": stored})
633+
observer.database.update_neurons = AsyncMock()
634+
observer.database.remove_neurons = AsyncMock()
635+
636+
with patch(
637+
"subvortex.core.model.neuron.neuron.Neuron.from_proto", return_value=stored
638+
), patch("subvortex.core.country.country.get_country", return_value=None):
639+
axons, has_missing_country = await observer._resync()
640+
641+
observer.database.update_neurons.assert_not_called()
642+
observer.database.remove_neurons.assert_not_called()
643+
assert axons["ipv6_hotkey"] == "::1"
644+
assert has_missing_country is False
645+
646+
647+
@pytest.mark.asyncio
648+
async def test_non_ipv4_address_but_other_change_triggers_update(observer):
649+
fake_proto = MagicMock()
650+
fake_proto.uid = 11
651+
fake_proto.axon_info.ip = "abcd" # Not a valid IPv4
652+
fake_proto.hotkey = "weird_ip_hotkey"
653+
654+
stored = scmm.Neuron(uid=11, ip="abcd", hotkey="weird_ip_hotkey", country=None)
655+
updated = scmm.Neuron(
656+
uid=11, ip="abcd", hotkey="weird_ip_hotkey", country=None, incentive=0.1
657+
)
658+
659+
observer.metagraph = AsyncMock()
660+
observer.metagraph.neurons = [fake_proto]
661+
observer.database.get_neurons = AsyncMock(return_value={"weird_ip_hotkey": stored})
662+
observer.database.update_neurons = AsyncMock()
663+
observer.database.remove_neurons = AsyncMock()
664+
665+
with patch(
666+
"subvortex.core.model.neuron.neuron.Neuron.from_proto", return_value=updated
667+
), patch("subvortex.core.country.country.get_country", return_value=None):
668+
axons, has_missing_country = await observer._resync()
669+
670+
observer.database.update_neurons.assert_called_once_with([updated])
671+
observer.database.remove_neurons.assert_not_called()
672+
assert axons["weird_ip_hotkey"] == "abcd"
673+
assert has_missing_country is True
674+
675+
676+
@pytest.mark.asyncio
677+
async def test_change_from_non_ipv4_to_ipv4(observer):
678+
fake_proto = MagicMock()
679+
fake_proto.uid = 12
680+
fake_proto.axon_info.ip = "8.8.8.8" # valid IPv4
681+
fake_proto.hotkey = "ipv4_later"
682+
683+
old = scmm.Neuron(uid=12, ip="not-an-ip", hotkey="ipv4_later", country=None)
684+
new = scmm.Neuron(uid=12, ip="8.8.8.8", hotkey="ipv4_later", country="US")
685+
686+
observer.metagraph = AsyncMock()
687+
observer.metagraph.neurons = [fake_proto]
688+
observer.database.get_neurons = AsyncMock(return_value={"ipv4_later": old})
689+
observer.database.update_neurons = AsyncMock()
690+
observer.database.remove_neurons = AsyncMock()
691+
692+
with patch(
693+
"subvortex.core.model.neuron.neuron.Neuron.from_proto", return_value=new
694+
), patch("subvortex.core.country.country.get_country", return_value="US"):
695+
axons, has_missing_country = await observer._resync()
696+
697+
observer.database.remove_neurons.assert_not_called()
698+
observer.database.update_neurons.assert_called_once_with([new])
699+
assert axons["ipv4_later"] == "8.8.8.8"
700+
assert has_missing_country is False
701+
702+
703+
@pytest.mark.asyncio
704+
async def test_zero_ip_and_non_ipv4_not_considered_missing_country(observer):
705+
fake_proto = MagicMock()
706+
fake_proto.uid = 13
707+
fake_proto.axon_info.ip = "0.0.0.0" # should be skipped
708+
fake_proto.hotkey = "zero_ip"
709+
710+
stored = scmm.Neuron(uid=13, ip="0.0.0.0", hotkey="zero_ip", country=None)
711+
712+
observer.metagraph = AsyncMock()
713+
observer.metagraph.neurons = [fake_proto]
714+
observer.database.get_neurons = AsyncMock(return_value={"zero_ip": stored})
715+
observer.database.update_neurons = AsyncMock()
716+
717+
with patch(
718+
"subvortex.core.model.neuron.neuron.Neuron.from_proto", return_value=stored
719+
):
720+
axons, has_missing_country = await observer._resync()
721+
722+
observer.database.remove_neurons.assert_not_called()
723+
observer.database.update_neurons.assert_not_called()
724+
assert has_missing_country is False
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import pytest
2+
3+
from subvortex.core.utils import is_valid_ipv4
4+
5+
6+
@pytest.mark.parametrize(
7+
"ip",
8+
[
9+
"192.168.1.1",
10+
"8.8.8.8",
11+
"127.0.0.1",
12+
"0.0.0.0",
13+
"255.255.255.255",
14+
],
15+
)
16+
def test_valid_ipv4_addresses(ip):
17+
assert is_valid_ipv4(ip), f"❌ {ip} should be recognized as valid IPv4"
18+
19+
20+
@pytest.mark.parametrize(
21+
"ip",
22+
[
23+
"::1",
24+
"2001:db8::1",
25+
"fe80::",
26+
"::ffff:192.0.2.128", # IPv4-mapped IPv6
27+
"abcd::1234",
28+
],
29+
)
30+
def test_valid_ipv6_addresses(ip):
31+
assert not is_valid_ipv4(ip), f"❌ {ip} should NOT be recognized as IPv4"
32+
33+
34+
@pytest.mark.parametrize(
35+
"ip",
36+
[
37+
"",
38+
" ",
39+
"not.an.ip",
40+
"256.256.256.256",
41+
"192.168.1.999",
42+
"1234.123.123.123",
43+
"....",
44+
"::",
45+
"::360:c054:1f9b",
46+
],
47+
)
48+
def test_invalid_ips(ip):
49+
assert not is_valid_ipv4(ip), f"❌ {ip} should NOT be recognized as valid IPv4"
50+
51+
52+
def test_is_valid_ipv4_type_safety():
53+
# Non-string inputs
54+
assert not is_valid_ipv4(None)
55+
assert not is_valid_ipv4(1234)
56+
assert not is_valid_ipv4(["192.168.1.1"])

subvortex/core/utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import ipaddress
2+
3+
4+
def is_valid_ipv4(ip: str):
5+
if not isinstance(ip, str):
6+
return False
7+
8+
try:
9+
return isinstance(ipaddress.ip_address(ip), ipaddress.IPv4Address)
10+
except ValueError:
11+
return False

0 commit comments

Comments
 (0)