feat: add settlement contract boundary
This commit is contained in:
61
tests/test_contracts_settlement.py
Normal file
61
tests/test_contracts_settlement.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""US-006 tests: Solana contract boundary for stake, payment, and settlement."""
|
||||
|
||||
from meshnet_contracts import LocalSolanaContracts
|
||||
|
||||
|
||||
def test_node_stake_is_reflected_in_registry():
|
||||
"""A node can submit stake and read the updated registry balance."""
|
||||
contracts = LocalSolanaContracts()
|
||||
|
||||
receipt = contracts.registry.submit_stake("node-wallet-a", 500)
|
||||
|
||||
assert receipt["cluster"] == "local-test-validator"
|
||||
assert contracts.registry.get_wallet("node-wallet-a").stake_balance == 500
|
||||
assert contracts.registry.has_minimum_stake("node-wallet-a", minimum_stake=100)
|
||||
|
||||
|
||||
def test_client_can_fund_api_key_with_testnet_sol():
|
||||
"""A client can fund an API key account and read the available balance."""
|
||||
contracts = LocalSolanaContracts()
|
||||
|
||||
receipt = contracts.payment.fund_api_key("api-key-a", lamports=2_000)
|
||||
|
||||
assert receipt["cluster"] == "local-test-validator"
|
||||
assert contracts.payment.get_balance("api-key-a").lamports == 3_000
|
||||
|
||||
|
||||
def test_epoch_settlement_debits_clients_and_rewards_nodes_and_validator():
|
||||
"""Settlement distributes native token rewards according to recorded work."""
|
||||
contracts = LocalSolanaContracts(cost_per_layer_token_lamport=2)
|
||||
contracts.payment.fund_api_key("api-key-a", lamports=1_000)
|
||||
contracts.payment.record_attribution(
|
||||
session_id="session-a",
|
||||
api_key="api-key-a",
|
||||
node_wallet="node-wallet-a",
|
||||
layer_start=0,
|
||||
layer_end=15,
|
||||
tokens=10,
|
||||
)
|
||||
contracts.payment.record_attribution(
|
||||
session_id="session-a",
|
||||
api_key="api-key-a",
|
||||
node_wallet="node-wallet-b",
|
||||
layer_start=16,
|
||||
layer_end=31,
|
||||
tokens=10,
|
||||
speed_score=2.0,
|
||||
)
|
||||
|
||||
result = contracts.settlement.settle_epoch(
|
||||
epoch=1,
|
||||
validator_wallet="validator-wallet",
|
||||
validator_reward_share_bps=1_000,
|
||||
)
|
||||
|
||||
assert result.client_debits == {"api-key-a": 640}
|
||||
assert result.node_rewards == {"node-wallet-a": 96, "node-wallet-b": 192}
|
||||
assert result.validator_rewards == {"validator-wallet": 32}
|
||||
assert contracts.payment.get_balance("api-key-a").lamports == 1_360
|
||||
assert contracts.settlement.get_token_balance("node-wallet-a") == 96
|
||||
assert contracts.settlement.get_token_balance("node-wallet-b") == 192
|
||||
assert contracts.settlement.get_token_balance("validator-wallet") == 32
|
||||
@@ -9,6 +9,7 @@ import urllib.request
|
||||
|
||||
from meshnet_gateway.server import GatewayServer
|
||||
from meshnet_node.server import StubNodeServer
|
||||
from meshnet_contracts import LocalSolanaContracts
|
||||
from meshnet_tracker.server import TrackerServer
|
||||
|
||||
|
||||
@@ -237,6 +238,150 @@ def test_tracker_registration_rejects_invalid_payload():
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_tracker_excludes_banned_wallets_from_routes():
|
||||
"""Tracker refuses route candidates whose wallet is banned on-chain."""
|
||||
contracts = LocalSolanaContracts()
|
||||
contracts.registry.submit_stake("wallet-a", 1_000)
|
||||
contracts.registry.submit_stake("wallet-b", 1_000)
|
||||
contracts.registry.ban_wallet("wallet-b")
|
||||
tracker = TrackerServer(contracts=contracts, minimum_stake=100)
|
||||
tracker_port = tracker.start()
|
||||
try:
|
||||
_post_json(
|
||||
f"http://127.0.0.1:{tracker_port}/v1/nodes/register",
|
||||
{"endpoint": "http://127.0.0.1:9001", "shard_start": 0, "shard_end": 15,
|
||||
"hardware_profile": {}, "wallet_address": "wallet-a", "score": 1.0},
|
||||
)
|
||||
_post_json(
|
||||
f"http://127.0.0.1:{tracker_port}/v1/nodes/register",
|
||||
{"endpoint": "http://127.0.0.1:9002", "shard_start": 16, "shard_end": 31,
|
||||
"hardware_profile": {}, "wallet_address": "wallet-b", "score": 1.0},
|
||||
)
|
||||
|
||||
try:
|
||||
_get_json(f"http://127.0.0.1:{tracker_port}/v1/route?model=stub-model")
|
||||
raise AssertionError("Expected 503 when only banned wallet covers a layer")
|
||||
except urllib.error.HTTPError as exc:
|
||||
assert exc.code == 503
|
||||
body = json.loads(exc.read())
|
||||
assert "layer 16" in body["error"]
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_gateway_routes_even_when_compute_node_has_low_stake():
|
||||
"""Routing is not based on a compute node's token balance."""
|
||||
contracts = LocalSolanaContracts()
|
||||
contracts.registry.submit_stake("wallet-a", 1_000)
|
||||
contracts.registry.submit_stake("wallet-b", 5)
|
||||
tracker = TrackerServer()
|
||||
tracker_port = tracker.start()
|
||||
tracker_url = f"http://127.0.0.1:{tracker_port}"
|
||||
|
||||
node_a = StubNodeServer(shard_start=0, shard_end=15, is_last_shard=False)
|
||||
port_a = node_a.start()
|
||||
node_b = StubNodeServer(shard_start=16, shard_end=31, is_last_shard=True)
|
||||
port_b = node_b.start()
|
||||
|
||||
_post_json(
|
||||
f"{tracker_url}/v1/nodes/register",
|
||||
{"endpoint": f"http://127.0.0.1:{port_a}", "shard_start": 0, "shard_end": 15,
|
||||
"hardware_profile": {}, "wallet_address": "wallet-a", "score": 1.0},
|
||||
)
|
||||
_post_json(
|
||||
f"{tracker_url}/v1/nodes/register",
|
||||
{"endpoint": f"http://127.0.0.1:{port_b}", "shard_start": 16, "shard_end": 31,
|
||||
"hardware_profile": {}, "wallet_address": "wallet-b", "score": 1.0},
|
||||
)
|
||||
|
||||
gateway = GatewayServer(
|
||||
tracker_url=tracker_url,
|
||||
contracts=contracts,
|
||||
minimum_stake=100,
|
||||
)
|
||||
gateway_port = gateway.start()
|
||||
|
||||
try:
|
||||
payload = json.dumps({
|
||||
"model": "stub-model",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{gateway_port}/v1/chat/completions",
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json", "Authorization": "Bearer api-key-a"},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
assert resp.status == 200
|
||||
assert node_b.received_activations
|
||||
finally:
|
||||
gateway.stop()
|
||||
node_a.stop()
|
||||
node_b.stop()
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_gateway_records_compute_attribution_after_inference_session():
|
||||
"""Gateway records layer and token attribution for every routed node."""
|
||||
contracts = LocalSolanaContracts()
|
||||
contracts.registry.submit_stake("wallet-a", 1_000)
|
||||
contracts.registry.submit_stake("wallet-b", 1_000)
|
||||
contracts.payment.fund_api_key("api-key-a", lamports=10_000)
|
||||
tracker = TrackerServer(contracts=contracts, minimum_stake=100)
|
||||
tracker_port = tracker.start()
|
||||
tracker_url = f"http://127.0.0.1:{tracker_port}"
|
||||
|
||||
node_a = StubNodeServer(shard_start=0, shard_end=15, is_last_shard=False)
|
||||
port_a = node_a.start()
|
||||
node_b = StubNodeServer(shard_start=16, shard_end=31, is_last_shard=True)
|
||||
port_b = node_b.start()
|
||||
|
||||
_post_json(
|
||||
f"{tracker_url}/v1/nodes/register",
|
||||
{"endpoint": f"http://127.0.0.1:{port_a}", "shard_start": 0, "shard_end": 15,
|
||||
"hardware_profile": {}, "wallet_address": "wallet-a", "score": 1.0},
|
||||
)
|
||||
_post_json(
|
||||
f"{tracker_url}/v1/nodes/register",
|
||||
{"endpoint": f"http://127.0.0.1:{port_b}", "shard_start": 16, "shard_end": 31,
|
||||
"hardware_profile": {}, "wallet_address": "wallet-b", "score": 1.0},
|
||||
)
|
||||
|
||||
gateway = GatewayServer(
|
||||
tracker_url=tracker_url,
|
||||
contracts=contracts,
|
||||
minimum_stake=100,
|
||||
)
|
||||
gateway_port = gateway.start()
|
||||
|
||||
try:
|
||||
payload = json.dumps({
|
||||
"model": "stub-model",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{gateway_port}/v1/chat/completions",
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json", "Authorization": "Bearer api-key-a"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
assert resp.status == 200
|
||||
|
||||
records = contracts.payment.list_attributions()
|
||||
assert [(r.api_key, r.node_wallet, r.layer_start, r.layer_end, r.tokens) for r in records] == [
|
||||
("api-key-a", "wallet-a", 0, 15, 1),
|
||||
("api-key-a", "wallet-b", 16, 31, 1),
|
||||
]
|
||||
finally:
|
||||
gateway.stop()
|
||||
node_a.stop()
|
||||
node_b.stop()
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_gateway_returns_json_503_when_tracker_unavailable():
|
||||
"""Tracker-backed gateway failures are reported as JSON HTTP errors."""
|
||||
gateway = GatewayServer(tracker_url="http://127.0.0.1:9")
|
||||
|
||||
Reference in New Issue
Block a user