Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Developer/BACK_LOG.md

Large diffs are not rendered by default.

553 changes: 553 additions & 0 deletions dincli/abis/DinEmission.json

Large diffs are not rendered by default.

67 changes: 65 additions & 2 deletions dincli/cli/aggregator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
from rich.table import Table
from web3 import Web3
from eth_abi import encode as abi_encode
from dincli.cli.dintoken import (buy_dintokens, read_din_per_eth_rate,
read_dintoken_stake, stake_dintokens)
from dincli.cli.dintoken import (approve_din, buy_dintokens,
read_din_per_eth_rate, read_dintoken_stake,
stake_dintokens)
from dincli.cli.utils import (CACHE_DIR, MIN_STAKE, build_and_send_tx,
get_manifest_key, lock_batch_seed_if_pending,
parse_tier_kind,
require_custom_manifest_service,
resolve_manifest_path)
from dincli.cli.worker import (
Expand Down Expand Up @@ -688,3 +690,64 @@ def reveal_t2(
)
except Exception as e:
console.print(f"[bold red]✗ Error revealing T2 aggregation: {e}[/bold red]")


@app.command("dispute", help="Dispute a finalized T1/T2 batch's CID (posts the DIN bond; any active validator)")
def dispute(
ctx: typer.Context,
model_id: int = typer.Argument(..., help="Model ID"),
gi: int = typer.Option(None, "--gi", help="GI of the batch (default: current GI)"),
tier: str = typer.Option(..., "--tier", help="t1 or t2"),
batch_id: int = typer.Option(..., "--batch", help="Batch ID within its tier"),
):
tier_kind = parse_tier_kind(tier)
effective_network, w3, account, console = ctx.obj.get_en_w3_account_console(model_id)

task_coordinator_contract = ctx.obj.get_deployed_din_task_coordinator_contract(True, model_id)

curr_GI, _ = ctx.obj.get_current_gi_and_state(task_coordinator_contract)
ref_gi = ctx.obj.validate_gi_LTE_curr_GI(gi, curr_GI)

# disputes getter: (challenger, bond, openedAt, ...). One dispute per batch, ever.
if task_coordinator_contract.functions.disputes(ref_gi, tier_kind, batch_id).call()[0] != "0x0000000000000000000000000000000000000000":
console.print(f"[bold red]✗ GI {ref_gi} {tier.upper()} batch {batch_id} has already been disputed.[/bold red]")
raise typer.Exit(1)

din_token_address = task_coordinator_contract.functions.dinToken().call()
if din_token_address == "0x0000000000000000000000000000000000000000":
console.print("[bold red]✗ DINTaskCoordinator has no DIN token set, so it can't take a bond. The model owner must call setDinToken first.[/bold red]")
raise typer.Exit(1)

bond = task_coordinator_contract.functions.disputeBond().call()
approve_din(ctx, task_coordinator_contract.address, bond, purpose="the dispute bond")

build_and_send_tx(
ctx,
task_coordinator_contract.functions.openDispute(ref_gi, tier_kind, batch_id),
f"Disputing GI {ref_gi} {tier.upper()} batch {batch_id} (bond {Web3.from_wei(bond, 'ether')} DIN)",
"Dispute opened. The model owner resolves it.",
"Could not open the dispute (TC_BatchNotFinalized / TC_DisputeWindowClosed if the batch isn't in its dispute window)",
)


@app.command("claim-dispute-bond", help="Withdraw dispute bonds returned to you (DINTaskCoordinator.claimDisputeBond)")
def claim_dispute_bond(
ctx: typer.Context,
model_id: int = typer.Argument(..., help="Model ID"),
):
effective_network, w3, account, console = ctx.obj.get_en_w3_account_console(model_id)

task_coordinator_contract = ctx.obj.get_deployed_din_task_coordinator_contract(True, model_id)

amount = task_coordinator_contract.functions.disputeBondClaimable(account.address).call()
if amount == 0:
console.print("[bold red]✗ No dispute bond to claim.[/bold red]")
raise typer.Exit(1)

build_and_send_tx(
ctx,
task_coordinator_contract.functions.claimDisputeBond(),
f"Claiming {Web3.from_wei(amount, 'ether')} DIN of dispute bond",
"Dispute bond claimed.",
"Could not claim the dispute bond",
)
92 changes: 87 additions & 5 deletions dincli/cli/auditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from eth_account.messages import encode_defunct
from eth_account import Account
from dincli.cli.dintoken import (buy_dintokens, read_din_per_eth_rate,
read_dintoken_stake, stake_dintokens)
from dincli.cli.dintoken import (approve_din, buy_dintokens,
read_din_per_eth_rate, read_dintoken_stake,
stake_dintokens)
from dincli.cli.utils import (CACHE_DIR, CONFIG_DIR, MIN_STAKE, build_and_send_tx,
get_manifest_key, lock_batch_seed_if_pending,
require_custom_manifest_service,
Expand All @@ -31,8 +32,15 @@
from dincli.services.cid_utils import get_cid_from_bytes32
from dincli.services.ipfs import retrieve_from_ipfs

def _load_auditor_x25519_key() -> PrivateKey:
key_path = Path(CONFIG_DIR) / "auditor_x25519.key"
# One X25519 key per wallet: a single shared file broke several auditors
# (e.g. --demokey accounts) on one machine, since each registers its own
# public key on DinValidatorStake.
def _auditor_x25519_key_path(address: str) -> Path:
return Path(CONFIG_DIR) / f"auditor_x25519_{Web3.to_checksum_address(address)}.key"


def _load_auditor_x25519_key(address: str) -> PrivateKey:
key_path = _auditor_x25519_key_path(address)
if not key_path.exists():
raise FileNotFoundError(
f"Auditor X25519 private key not found at {key_path}. "
Expand Down Expand Up @@ -165,6 +173,80 @@ def register(
console.print(f"[bold red]✗ Could not register auditor. {e}[/bold red]")
raise typer.Exit()

@app.command("register-encryption-key", help="Generate this wallet's X25519 key and register its public key on DinValidatorStake")
def register_encryption_key(ctx: typer.Context):
effective_network, w3, account, console = ctx.obj.get_en_w3_account_console()

stake_contract = ctx.obj.get_deployed_din_stake_contract()

key_path = _auditor_x25519_key_path(account.address)
if key_path.exists():
privkey = PrivateKey(bytes.fromhex(key_path.read_text().strip()))
console.print(f"[dim]Using existing X25519 key {key_path}[/dim]")
else:
privkey = PrivateKey.generate()
key_path.parent.mkdir(parents=True, exist_ok=True)
key_path.write_text(privkey.encode(encoder=nacl.encoding.HexEncoder).decode())
console.print(f"[yellow]Generated X25519 key → {key_path}[/yellow]")
key_path.chmod(0o600)

pubkey = bytes(privkey.public_key)
if bytes(stake_contract.functions.getEncryptionKey(account.address).call()) == pubkey:
console.print("[bold green]✓ This key is already registered on-chain.[/bold green]")
return

build_and_send_tx(
ctx,
stake_contract.functions.registerEncryptionKey(pubkey),
"Registering encryption key",
"Encryption key registered.",
"Could not register encryption key",
)


@app.command("dispute-test-data", help="Dispute your batch's test data (posts the DIN bond; batch auditors only)")
def dispute_test_data(
ctx: typer.Context,
model_id: int = typer.Argument(..., help="Model ID"),
gi: int = typer.Option(None, "--gi", help="Global iteration number"),
batch_id: int = typer.Option(..., "--batch", help="Your auditor batch ID"),
):
effective_network, w3, account, console = ctx.obj.get_en_w3_account_console(model_id)

task_coordinator_contract = ctx.obj.get_deployed_din_task_coordinator_contract(True, model_id)
task_auditor_contract = ctx.obj.get_deployed_din_task_auditor_contract(True, model_id)

curr_GI, _ = ctx.obj.get_current_gi_and_state(task_coordinator_contract)
ref_gi = ctx.obj.validate_gi_LTE_curr_GI(gi, curr_GI)

# Mirrors openTestDataDispute's checks, so a doomed dispute costs no approval.
if not task_auditor_contract.functions.isBatchAuditor(ref_gi, batch_id, account.address).call():
console.print(f"[bold red]✗ You aren't an auditor of GI {ref_gi} batch {batch_id}.[/bold red]")
raise typer.Exit(1)
if task_auditor_contract.functions.giRewardSnapshot(ref_gi).call()[4]:
console.print(f"[bold red]✗ GI {ref_gi}'s rewards are already settled; it can't be disputed.[/bold red]")
raise typer.Exit(1)
if task_auditor_contract.functions.testDataCommitments(ref_gi, batch_id).call() == b"\x00" * 32:
console.print(f"[bold red]✗ No test data has been assigned to batch {batch_id} yet.[/bold red]")
raise typer.Exit(1)
d = task_auditor_contract.functions.testDataDisputes(ref_gi, batch_id).call()
if d[3] or d[4]:
console.print(f"[bold red]✗ Batch {batch_id} already has an active dispute or is pending reassignment.[/bold red]")
raise typer.Exit(1)

bond = task_auditor_contract.functions.disputeBondAmount().call()
if bond > 0:
approve_din(ctx, task_auditor_contract.address, bond, purpose="the dispute bond")

build_and_send_tx(
ctx,
task_auditor_contract.functions.openTestDataDispute(ref_gi, batch_id),
f"Opening a test-data dispute on GI {ref_gi} batch {batch_id} (bond {Web3.from_wei(bond, 'ether')} DIN)",
"Dispute opened. The model owner must answer within the window, or anyone can close it as upheld.",
"Could not open the dispute",
)


@app.command("lock-seed", help="Lock the auditor batch-assignment seed for the current GI (permissionless)")
def lock_seed(
ctx: typer.Context,
Expand Down Expand Up @@ -470,7 +552,7 @@ def evaluate_lms(
# fetch ciphertext IPFS(rawCID) → AES-GCM-decrypt(K) → plaintext .pt
raw_cid = None
try:
auditor_privkey = _load_auditor_x25519_key()
auditor_privkey = _load_auditor_x25519_key(account.address)
owner_pubkey = PublicKey(bytes.fromhex(
get_manifest_key(effective_network, "owner_encryption_pubkey", model_id)
))
Expand Down
9 changes: 9 additions & 0 deletions dincli/cli/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,15 @@ def get_deployed_din_registry_contract(self, verbose: bool = True):
artifact_path = files("dincli").joinpath("abis", "DINModelRegistry.json")
return get_contract_instance(str(artifact_path), self.network, dinregistry_address)

def get_deployed_din_emission_contract(self, verbose: bool = True):
dinemission_address = load_din_info()[self.network].get("emission")
if not dinemission_address:
raise ValueError(f"No DinEmission address in din_info for network '{self.network}'")
if verbose:
self.console.print("[bold green]✓ DIN Emission contract address:[/bold green] ", dinemission_address)
artifact_path = files("dincli").joinpath("abis", "DinEmission.json")
return get_contract_instance(str(artifact_path), self.network, dinemission_address)

def _get_task_manifest_context(
self,
model_id: Optional[int],
Expand Down
38 changes: 37 additions & 1 deletion dincli/cli/dinrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,45 @@ def total_models(ctx: typer.Context,
console.print(f"[bold green]Total models: {models_length}[/bold green]")


def _task_contract_model_ids(network: str, req) -> dict:
"""modelId() of a registration request's task contracts, keyed by name."""
abis = files("dincli").joinpath("abis")
return {
name: get_contract_instance(str(abis.joinpath(abi)), network, address).functions.modelId().call()
for name, abi, address in (
("DINTaskCoordinator", "DINTaskCoordinator.json", req[3]),
("DINTaskAuditor", "DINTaskAuditor.json", req[4]),
)
}


@registry_app.command("approve-registration-request")
def approve_registration_request(ctx: typer.Context, request_id: int = typer.Argument(..., help="Model request ID to approve")):
def approve_registration_request(
ctx: typer.Context,
request_id: int = typer.Argument(..., help="Model request ID to approve"),
force: bool = typer.Option(False, "--force", help="Approve even if the task contracts' modelId() doesn't match the ID this approval assigns"),
):
effective_network, w3, account, console = ctx.obj.get_en_w3_account_console()
DINModelRegistry_Contract = ctx.obj.get_deployed_din_registry_contract()

# The task contracts bake modelId into their constructors before the
# registry assigns one, and approveModel assigns totalModels(). A
# mismatch breaks per-model stake enforcement for the model's lifetime.
req = DINModelRegistry_Contract.functions.modelRequests(request_id).call()
assigned_model_id = DINModelRegistry_Contract.functions.totalModels().call()
mismatched = {
name: model_id
for name, model_id in _task_contract_model_ids(effective_network, req).items()
if model_id != assigned_model_id
}
if mismatched:
for name, model_id in mismatched.items():
console.print(f"[bold red]✗ {name} has modelId() {model_id}, but this approval assigns model ID {assigned_model_id}.[/bold red]")
if not force:
console.print("[yellow]Refusing to approve. The model owner should redeploy with the right --model-id; pass --force to approve anyway.[/yellow]")
raise typer.Exit(1)
console.print("[yellow]--force given: approving despite the mismatch.[/yellow]")

build_and_send_tx(
ctx,
DINModelRegistry_Contract.functions.approveModel(request_id),
Expand Down
31 changes: 21 additions & 10 deletions dincli/cli/dintoken.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@
app = typer.Typer(help="Commands for DIN Token in DIN.")


def approve_din(ctx: typer.Context, spender: str, amount: int, purpose: str = "this action"):
"""Approves `spender` to pull `amount` (wei) DIN from the active wallet.

Shared by every command that has a contract pull DIN with
safeTransferFrom: staking, reward deposits and dispute bonds. Exits on a
failed approval, since the call that follows would only revert.
"""
din_token_contract = ctx.obj.get_deployed_din_token_contract(verbose=False)
build_and_send_tx(
ctx,
din_token_contract.functions.approve(spender, amount),
f"Approving DINTokens for {purpose}",
f"DINTokens approved for {purpose}.",
f"Could not approve DINTokens for {purpose}.",
exit_on_failure=True,
)
# Give a lagging RPC time to see the allowance before the pull.
time.sleep(5)


def buy_dintokens(
ctx: typer.Context,
amount: float,
Expand Down Expand Up @@ -106,16 +126,7 @@ def stake_dintokens(
console.print(f"[bold green]Staking {amount} DINTokens...[/bold green]")

try:
build_and_send_tx(
ctx,
din_token_contract.functions.approve(din_stake_contract.address, stake_amount),
"Approving DINTokens for staking",
"DINTokens approved for staking.",
"Could not approve DINTokens for staking.",
exit_on_failure=True,
)

time.sleep(5)
approve_din(ctx, din_stake_contract.address, stake_amount, purpose="staking")

build_and_send_tx(
ctx,
Expand Down
4 changes: 4 additions & 0 deletions dincli/cli/modelowner.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from dincli.cli.modelownerd.aggregation import aggregation_app
from dincli.cli.modelownerd.auditor_batches import auditor_batches_app
from dincli.cli.modelownerd.deploy import deploy_app
from dincli.cli.modelownerd.disputes import disputes_app
from dincli.cli.modelownerd.gi import gi_app
from dincli.cli.modelownerd.lms import lms_app
from dincli.cli.modelownerd.lms_evaluation import lms_evaluation_app
from dincli.cli.modelownerd.model import model_app
from dincli.cli.modelownerd.rewards import rewards_app
from dincli.cli.modelownerd.setup import (add_slasher)
from dincli.cli.modelownerd.slash import slash_app
from dincli.cli.modelownerd.task import task_app
Expand All @@ -23,6 +25,8 @@
app.add_typer(slash_app, name="slash")
app.command("add-slasher")(add_slasher)
app.add_typer(task_app, name="task")
app.add_typer(rewards_app, name="rewards")
app.add_typer(disputes_app, name="disputes")

if __name__ == "__main__":
app()
Loading
Loading