feat: add settlement contract boundary

This commit is contained in:
Dobromir Popov
2026-06-29 02:46:25 +03:00
parent 8900990193
commit e24c2e3cea
7 changed files with 637 additions and 5 deletions

View File

@@ -1,3 +1,317 @@
"""meshnet_contracts — Solana smart contract wrappers for the Distributed Inference Network."""
"""Solana contract boundary for the Distributed Inference Network.
The prototype uses deterministic local state with Solana-shaped wrapper classes.
ADR-0007 keeps the public Python boundary stable so real Solana programs can
replace this adapter later without changing gateway or tracker call sites.
"""
from dataclasses import dataclass, field
__version__ = "0.1.0"
@dataclass(frozen=True)
class RegistryWallet:
"""Stake, strike, and ban state for a node operator wallet."""
stake_balance: int = 0
strike_count: int = 0
banned: bool = False
@dataclass(frozen=True)
class ApiKeyBalance:
"""Client API key payment account balance."""
lamports: int = 0
usdc_micro: int = 0
@dataclass(frozen=True)
class ComputeAttribution:
"""On-chain work attribution recorded by the gateway after inference."""
session_id: str
api_key: str
node_wallet: str
layer_start: int
layer_end: int
tokens: int
speed_score: float = 1.0
settled_epoch: int | None = None
@property
def layer_count(self) -> int:
return self.layer_end - self.layer_start + 1
@property
def work_units(self) -> int:
return self.layer_count * self.tokens
@dataclass(frozen=True)
class SettlementResult:
"""Summary returned by an epoch settlement transaction."""
epoch: int
client_debits: dict[str, int]
node_rewards: dict[str, int]
validator_rewards: dict[str, int]
@dataclass
class _LocalContractState:
registry: dict[str, RegistryWallet] = field(default_factory=dict)
api_keys: dict[str, ApiKeyBalance] = field(default_factory=dict)
attributions: list[ComputeAttribution] = field(default_factory=list)
token_balances: dict[str, int] = field(default_factory=dict)
class RegistryContract:
"""Registry wrapper for node stake, strikes, and bans."""
def __init__(self, state: _LocalContractState, cluster: str) -> None:
self._state = state
self._cluster = cluster
def submit_stake(self, wallet_address: str, amount: int) -> dict:
if amount <= 0:
raise ValueError("stake amount must be positive")
current = self.get_wallet(wallet_address)
self._state.registry[wallet_address] = RegistryWallet(
stake_balance=current.stake_balance + amount,
strike_count=current.strike_count,
banned=current.banned,
)
return {"signature": f"local-stake-{wallet_address}", "cluster": self._cluster}
def get_wallet(self, wallet_address: str) -> RegistryWallet:
return self._state.registry.get(wallet_address, RegistryWallet())
def record_strike(self, wallet_address: str, ban_threshold: int = 3) -> dict:
current = self.get_wallet(wallet_address)
strike_count = current.strike_count + 1
self._state.registry[wallet_address] = RegistryWallet(
stake_balance=current.stake_balance,
strike_count=strike_count,
banned=current.banned or strike_count >= ban_threshold,
)
return {"signature": f"local-strike-{wallet_address}", "cluster": self._cluster}
def ban_wallet(self, wallet_address: str) -> dict:
current = self.get_wallet(wallet_address)
self._state.registry[wallet_address] = RegistryWallet(
stake_balance=current.stake_balance,
strike_count=current.strike_count,
banned=True,
)
return {"signature": f"local-ban-{wallet_address}", "cluster": self._cluster}
def has_minimum_stake(self, wallet_address: str | None, minimum_stake: int) -> bool:
if not wallet_address:
return False
wallet = self.get_wallet(wallet_address)
return wallet.stake_balance >= minimum_stake
class PaymentContract:
"""Payment wrapper for funded API keys and compute attribution."""
def __init__(self, state: _LocalContractState, cluster: str, starting_credit_lamports: int) -> None:
self._state = state
self._cluster = cluster
self._starting_credit_lamports = starting_credit_lamports
def fund_api_key(
self,
api_key: str,
*,
lamports: int = 0,
usdc_micro: int = 0,
) -> dict:
if lamports < 0 or usdc_micro < 0:
raise ValueError("funding amounts must be non-negative")
if lamports == 0 and usdc_micro == 0:
raise ValueError("funding amount must be positive")
current = self.get_balance(api_key)
self._state.api_keys[api_key] = ApiKeyBalance(
lamports=current.lamports + lamports,
usdc_micro=current.usdc_micro + usdc_micro,
)
return {"signature": f"local-fund-{api_key}", "cluster": self._cluster}
def get_balance(self, api_key: str) -> ApiKeyBalance:
return self._state.api_keys.get(
api_key,
ApiKeyBalance(lamports=self._starting_credit_lamports),
)
def record_attribution(
self,
*,
session_id: str,
api_key: str,
node_wallet: str,
layer_start: int,
layer_end: int,
tokens: int,
speed_score: float = 1.0,
) -> dict:
if not api_key:
raise ValueError("api_key is required")
if not node_wallet:
raise ValueError("node_wallet is required")
if layer_start < 0 or layer_end < layer_start:
raise ValueError("layer range must be non-negative and ordered")
if tokens <= 0:
raise ValueError("tokens must be positive")
if speed_score <= 0:
raise ValueError("speed_score must be positive")
self._state.attributions.append(ComputeAttribution(
session_id=session_id,
api_key=api_key,
node_wallet=node_wallet,
layer_start=layer_start,
layer_end=layer_end,
tokens=tokens,
speed_score=speed_score,
))
return {"signature": f"local-attribution-{session_id}", "cluster": self._cluster}
def list_attributions(self, *, include_settled: bool = True) -> list[ComputeAttribution]:
if include_settled:
return list(self._state.attributions)
return [a for a in self._state.attributions if a.settled_epoch is None]
class SettlementContract:
"""Settlement wrapper that debits clients and credits token rewards."""
def __init__(
self,
state: _LocalContractState,
cluster: str,
cost_per_layer_token_lamport: int,
) -> None:
self._state = state
self._cluster = cluster
self._cost_per_layer_token_lamport = cost_per_layer_token_lamport
def settle_epoch(
self,
*,
epoch: int,
validator_wallet: str | None = None,
validator_reward_share_bps: int = 0,
) -> SettlementResult:
if epoch < 0:
raise ValueError("epoch must be non-negative")
if not 0 <= validator_reward_share_bps <= 10_000:
raise ValueError("validator_reward_share_bps must be between 0 and 10000")
unsettled = [a for a in self._state.attributions if a.settled_epoch is None]
client_debits: dict[str, int] = {}
work_by_node: dict[str, int] = {}
for attribution in unsettled:
debit = attribution.work_units * self._cost_per_layer_token_lamport
weighted_work = int(attribution.work_units * attribution.speed_score)
client_debits[attribution.api_key] = client_debits.get(attribution.api_key, 0) + debit
work_by_node[attribution.node_wallet] = (
work_by_node.get(attribution.node_wallet, 0) + weighted_work
)
for api_key, debit in client_debits.items():
current = self._state.api_keys.get(api_key, ApiKeyBalance())
if current.lamports < debit:
raise ValueError(f"insufficient SOL balance for API key: {api_key}")
self._state.api_keys[api_key] = ApiKeyBalance(
lamports=current.lamports - debit,
usdc_micro=current.usdc_micro,
)
total_weighted_work = sum(work_by_node.values())
raw_total_work = sum(a.work_units for a in unsettled)
validator_rewards: dict[str, int] = {}
validator_reward_total = (raw_total_work * validator_reward_share_bps) // 10_000
if validator_wallet and validator_reward_total:
validator_rewards[validator_wallet] = validator_reward_total
node_reward_pool = raw_total_work - validator_reward_total
node_rewards: dict[str, int] = {}
for node_wallet, weighted_work_units in work_by_node.items():
reward = 0 if total_weighted_work == 0 else (node_reward_pool * weighted_work_units) // total_weighted_work
node_rewards[node_wallet] = reward
self._state.token_balances[node_wallet] = (
self._state.token_balances.get(node_wallet, 0) + reward
)
for wallet, reward in validator_rewards.items():
self._state.token_balances[wallet] = self._state.token_balances.get(wallet, 0) + reward
self._state.attributions = [
ComputeAttribution(
session_id=a.session_id,
api_key=a.api_key,
node_wallet=a.node_wallet,
layer_start=a.layer_start,
layer_end=a.layer_end,
tokens=a.tokens,
speed_score=a.speed_score,
settled_epoch=epoch,
)
if a.settled_epoch is None else a
for a in self._state.attributions
]
return SettlementResult(
epoch=epoch,
client_debits=client_debits,
node_rewards=node_rewards,
validator_rewards=validator_rewards,
)
def get_token_balance(self, wallet_address: str) -> int:
return self._state.token_balances.get(wallet_address, 0)
def deployment_plan(self) -> dict:
"""Return the configured manual testnet deployment targets."""
return {
"cluster": self._cluster,
"programs": ["registry", "payment", "settlement"],
}
class LocalSolanaContracts:
"""Facade that exposes all three contract wrappers over local validator state."""
def __init__(
self,
*,
cluster: str = "local-test-validator",
cost_per_layer_token_lamport: int = 1,
starting_credit_lamports: int = 1_000,
) -> None:
if cost_per_layer_token_lamport <= 0:
raise ValueError("cost_per_layer_token_lamport must be positive")
if starting_credit_lamports < 0:
raise ValueError("starting_credit_lamports must be non-negative")
self.cluster = cluster
self._state = _LocalContractState()
self.registry = RegistryContract(self._state, cluster)
self.payment = PaymentContract(self._state, cluster, starting_credit_lamports)
self.settlement = SettlementContract(
self._state,
cluster,
cost_per_layer_token_lamport,
)
__all__ = [
"ApiKeyBalance",
"ComputeAttribution",
"LocalSolanaContracts",
"PaymentContract",
"RegistryContract",
"RegistryWallet",
"SettlementContract",
"SettlementResult",
]