From 5e53420dfbf695af3d72a3419190b87070726e25 Mon Sep 17 00:00:00 2001 From: Pedro Werneck Date: Wed, 30 Sep 2026 09:01:40 -0300 Subject: [PATCH 1/4] feat: validate peer setup before a demo runs on A wrong peer email showed up only halfway through a notebook, and a connection left over from an earlier run looked live. validate_peer() fails unless the peer is approved and either connected in this session or logged in within max_age (Drive's modified time of its version file). With a timeout it waits for a late approval or login, one Drive request per poll. The demo notebooks call it after peering. --- .../1. DO-benchmark-owner-dbe.ipynb | 12 + .../2. DO-model-owner-dbe.ipynb | 12 + .../1. DO-model-owner-gemma-restrict.ipynb | 13 + ...2. DO-benchmark-owner-gemma-restrict.ipynb | 13 + .../3. DS-researcher-gemma-restrict.ipynb | 14 + .../1. DO-model-owner-gemma-shield.ipynb | 13 + .../2. DO-benchmark-owner-gemma-shield.ipynb | 13 + .../1. DO-model-owner-gemma-restrict.ipynb | 13 + ...2. DO-benchmark-owner-gemma-restrict.ipynb | 13 + .../nanolm/2. enclave_eval_localenc.ipynb | 18 ++ .../enclave/nanolm/3. enclave_eval_e2e.ipynb | 18 ++ .../nbsplit/1. DO-model-owner-nanolm.ipynb | 13 + .../2. DO-benchmark-owner-nanolm.ipynb | 13 + .../nbsplit/3. DS-researcher-nanolm.ipynb | 14 + .../syft-enclave/src/syft_enclaves/client.py | 19 +- .../syft-enclave/tests/test_enclave_client.py | 12 + packages/syft-rds/src/syft_rds/client.py | 3 + syft/sync/connections/connection_router.py | 8 + .../connections/drive/gdrive_transport.py | 28 +- syft/sync/peers/peer.py | 8 + syft/sync/syftbox_manager.py | 57 +++- syft/sync/version/peer_manager.py | 107 ++++++- tests/unit/test_validate_peer.py | 260 ++++++++++++++++++ 23 files changed, 687 insertions(+), 7 deletions(-) create mode 100644 tests/unit/test_validate_peer.py diff --git a/notebooks/enclave/double-blind-eval-w-receipts/1. DO-benchmark-owner-dbe.ipynb b/notebooks/enclave/double-blind-eval-w-receipts/1. DO-benchmark-owner-dbe.ipynb index 06c7e4b2893..5a9de61ac54 100644 --- a/notebooks/enclave/double-blind-eval-w-receipts/1. DO-benchmark-owner-dbe.ipynb +++ b/notebooks/enclave/double-blind-eval-w-receipts/1. DO-benchmark-owner-dbe.ipynb @@ -162,6 +162,18 @@ "benchmark_owner.peers" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", + "# left over from an earlier run.\n", + "benchmark_owner.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", + "print(f\" Connected to {MODEL_OWNER_EMAIL}\")" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/notebooks/enclave/double-blind-eval-w-receipts/2. DO-model-owner-dbe.ipynb b/notebooks/enclave/double-blind-eval-w-receipts/2. DO-model-owner-dbe.ipynb index cc816f6d5d9..a945ed21c3b 100644 --- a/notebooks/enclave/double-blind-eval-w-receipts/2. DO-model-owner-dbe.ipynb +++ b/notebooks/enclave/double-blind-eval-w-receipts/2. DO-model-owner-dbe.ipynb @@ -170,6 +170,18 @@ "print(\" Benchmark owner approved\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", + "# left over from an earlier run.\n", + "model_owner.validate_peer(BENCHMARK_OWNER_EMAIL, timeout=300)\n", + "print(f\" Connected to {BENCHMARK_OWNER_EMAIL}\")" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb index 3c14adfeaff..527ecf02ffc 100644 --- a/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb @@ -191,6 +191,19 @@ "print(\" Researcher peer approved\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "validate-peers", + "metadata": {}, + "outputs": [], + "source": [ + "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", + "# left over from an earlier run.\n", + "model_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", + "print(f\" Connected to {RESEARCHER_EMAIL}\")" + ] + }, { "cell_type": "markdown", "id": "15", diff --git a/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb index 57e114c91b7..bf6272334ba 100644 --- a/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb @@ -164,6 +164,19 @@ "print(\" Researcher peer approved\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "validate-peers", + "metadata": {}, + "outputs": [], + "source": [ + "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", + "# left over from an earlier run.\n", + "benchmark_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", + "print(f\" Connected to {RESEARCHER_EMAIL}\")" + ] + }, { "cell_type": "markdown", "id": "14", diff --git a/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb index a6e5ec797da..d695cd17288 100644 --- a/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb @@ -151,6 +151,20 @@ "researcher.peers" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "validate-peers", + "metadata": {}, + "outputs": [], + "source": [ + "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", + "# left over from an earlier run.\n", + "for peer_email in [MODEL_OWNER_EMAIL, BENCHMARK_OWNER_EMAIL]:\n", + " researcher.validate_peer(peer_email, timeout=300)\n", + " print(f\" Connected to {peer_email}\")" + ] + }, { "cell_type": "markdown", "id": "13", diff --git a/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb b/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb index eaa6fad021d..e8596c55782 100644 --- a/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb +++ b/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb @@ -242,6 +242,19 @@ "print(\" Benchmark owner peer approved\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "validate-peers", + "metadata": {}, + "outputs": [], + "source": [ + "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", + "# left over from an earlier run.\n", + "model_owner.validate_peer(BENCHMARK_OWNER_EMAIL, timeout=300)\n", + "print(f\" Connected to {BENCHMARK_OWNER_EMAIL}\")" + ] + }, { "cell_type": "markdown", "id": "17", diff --git a/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb b/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb index 9119fdd6b10..90e8cb670f4 100644 --- a/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb +++ b/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb @@ -184,6 +184,19 @@ "benchmark_owner.peers" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "validate-peers", + "metadata": {}, + "outputs": [], + "source": [ + "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", + "# left over from an earlier run.\n", + "benchmark_owner.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", + "print(f\" Connected to {MODEL_OWNER_EMAIL}\")" + ] + }, { "cell_type": "markdown", "id": "14", diff --git a/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb index a9a23ab5ea7..4bbd55c2809 100644 --- a/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb @@ -194,6 +194,19 @@ "print(\" Benchmark owner peer approved\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "validate-peers", + "metadata": {}, + "outputs": [], + "source": [ + "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", + "# left over from an earlier run.\n", + "model_owner.validate_peer(BENCHMARK_OWNER_EMAIL, timeout=300)\n", + "print(f\" Connected to {BENCHMARK_OWNER_EMAIL}\")" + ] + }, { "cell_type": "markdown", "id": "15", diff --git a/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb index 1a3bb211e48..79a397127e2 100644 --- a/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb @@ -173,6 +173,19 @@ "benchmark_owner.peers" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "validate-peers", + "metadata": {}, + "outputs": [], + "source": [ + "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", + "# left over from an earlier run.\n", + "benchmark_owner.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", + "print(f\" Connected to {MODEL_OWNER_EMAIL}\")" + ] + }, { "cell_type": "markdown", "id": "14", diff --git a/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb b/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb index 0291dfdf8e7..17160748c21 100644 --- a/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb +++ b/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb @@ -345,6 +345,24 @@ "print(\" Peer topology established\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "validate-peers", + "metadata": {}, + "outputs": [], + "source": [ + "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", + "# left over from an earlier run.\n", + "# Each call waits at most 300 s, so the worst case for this cell is\n", + "# 20 minutes (4 x 300 s).\n", + "researcher.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", + "researcher.validate_peer(BENCHMARK_OWNER_EMAIL, timeout=300)\n", + "model_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", + "benchmark_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", + "print(\" Peers validated\")" + ] + }, { "cell_type": "markdown", "id": "16", diff --git a/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb b/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb index e44b0dc72a5..057313afec0 100644 --- a/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb +++ b/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb @@ -217,6 +217,24 @@ "print(\" Peer topology established\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "validate-peers", + "metadata": {}, + "outputs": [], + "source": [ + "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", + "# left over from an earlier run.\n", + "# Each call waits at most 300 s, so the worst case for this cell is\n", + "# 20 minutes (4 x 300 s).\n", + "researcher.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", + "researcher.validate_peer(BENCHMARK_OWNER_EMAIL, timeout=300)\n", + "model_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", + "benchmark_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", + "print(\" Peers validated\")" + ] + }, { "cell_type": "markdown", "id": "12", diff --git a/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb b/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb index 20fa803b7e9..599fcfff10d 100644 --- a/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb +++ b/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb @@ -159,6 +159,19 @@ "print(\" Researcher peer approved\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "validate-peers", + "metadata": {}, + "outputs": [], + "source": [ + "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", + "# left over from an earlier run.\n", + "model_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", + "print(f\" Connected to {RESEARCHER_EMAIL}\")" + ] + }, { "cell_type": "markdown", "id": "14", diff --git a/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb b/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb index dd8ead10fee..09f54205e1f 100644 --- a/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb +++ b/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb @@ -159,6 +159,19 @@ "print(\" Researcher peer approved\")" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "validate-peers", + "metadata": {}, + "outputs": [], + "source": [ + "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", + "# left over from an earlier run.\n", + "benchmark_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", + "print(f\" Connected to {RESEARCHER_EMAIL}\")" + ] + }, { "cell_type": "markdown", "id": "14", diff --git a/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb b/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb index 392d41beec7..2ad64d951a1 100644 --- a/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb +++ b/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb @@ -151,6 +151,20 @@ "researcher.peers" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "validate-peers", + "metadata": {}, + "outputs": [], + "source": [ + "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", + "# left over from an earlier run.\n", + "for peer_email in [MODEL_OWNER_EMAIL, BENCHMARK_OWNER_EMAIL]:\n", + " researcher.validate_peer(peer_email, timeout=300)\n", + " print(f\" Connected to {peer_email}\")" + ] + }, { "cell_type": "markdown", "id": "13", diff --git a/packages/syft-enclave/src/syft_enclaves/client.py b/packages/syft-enclave/src/syft_enclaves/client.py index 4d08d5bf9c3..460e9d1afee 100644 --- a/packages/syft-enclave/src/syft_enclaves/client.py +++ b/packages/syft-enclave/src/syft_enclaves/client.py @@ -1,7 +1,7 @@ import json import logging import os -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import TYPE_CHECKING, Optional @@ -15,7 +15,11 @@ from syft.sync.peers.peer import Peer from syft.sync.peers.peer_list import PeerList -from syft.sync.version.peer_manager import CompatAction +from syft.sync.version.peer_manager import ( + DEFAULT_PEER_LOGIN_MAX_AGE, + DEFAULT_PEER_POLL_INTERVAL, + CompatAction, +) from syft_enclaves.attestation.dispatch import policy_for, verify_evidence from syft_enclaves.attestation.envelope import AttestationEvidence from syft_enclaves.enclave_job_client import EnclaveJobClient @@ -110,6 +114,17 @@ def approve_peer_request( def reject_peer_request(self, email_or_peer: str | Peer): self._rds.reject_peer_request(email_or_peer) + def validate_peer( + self, + peer_email: str, + max_age: timedelta = DEFAULT_PEER_LOGIN_MAX_AGE, + timeout: float = 0, + poll_interval: float = DEFAULT_PEER_POLL_INTERVAL, + ) -> Peer: + return self._rds.validate_peer( + peer_email, max_age=max_age, timeout=timeout, poll_interval=poll_interval + ) + def attest_peer( self, peer_email: str, diff --git a/packages/syft-enclave/tests/test_enclave_client.py b/packages/syft-enclave/tests/test_enclave_client.py index 909630906df..3771ae1f5e1 100644 --- a/packages/syft-enclave/tests/test_enclave_client.py +++ b/packages/syft-enclave/tests/test_enclave_client.py @@ -3,6 +3,9 @@ os.environ["PRE_SYNC"] = "false" +import pytest + +from syft.sync.peers.peer import PeerSetupError from syft_enclaves import SyftEnclaveClient @@ -42,3 +45,12 @@ def approved_emails(client): # DS: all peers are accepted (both sides created folders) assert approved_emails(ds) == {do1.email, do2.email, enclave.email} + + +def test_validate_peer_accepts_live_peer_and_rejects_wrong_email(): + _, do1, _, ds = SyftEnclaveClient.quad_with_mock_drive_service_connection() + + assert ds.validate_peer(do1.email).email == do1.email + assert do1.validate_peer(ds.email).email == ds.email + with pytest.raises(PeerSetupError, match="not a peer"): + ds.validate_peer("wrong@test.com") diff --git a/packages/syft-rds/src/syft_rds/client.py b/packages/syft-rds/src/syft_rds/client.py index 398aa523ef4..5f9cb7d5153 100644 --- a/packages/syft-rds/src/syft_rds/client.py +++ b/packages/syft-rds/src/syft_rds/client.py @@ -260,6 +260,9 @@ def reject_peer_request(self, *args: Any, **kwargs: Any) -> Any: def load_peers(self, *args: Any, **kwargs: Any) -> Any: return self.sync_engine.load_peers(*args, **kwargs) + def validate_peer(self, *args: Any, **kwargs: Any) -> Any: + return self.sync_engine.validate_peer(*args, **kwargs) + def delete_syftbox(self, *args: Any, **kwargs: Any) -> Any: return self.sync_engine.delete_syftbox(*args, **kwargs) diff --git a/syft/sync/connections/connection_router.py b/syft/sync/connections/connection_router.py index e45fc214476..01c3ce86315 100644 --- a/syft/sync/connections/connection_router.py +++ b/syft/sync/connections/connection_router.py @@ -1,4 +1,5 @@ import logging +from datetime import datetime from typing import TYPE_CHECKING, Dict, List, Optional from pydantic import BaseModel, PrivateAttr @@ -430,6 +431,13 @@ def read_peer_version_file(self, peer_email: str) -> Optional["VersionInfo"]: connection = self.connection_for_datasite_watcher() return connection.read_peer_version_file(peer_email) + def read_peer_version_file_modified_time( + self, peer_email: str + ) -> Optional[datetime]: + """Drive's last-modified time of a peer's version file, or None.""" + connection = self.connection_for_datasite_watcher() + return connection.read_peer_version_file_modified_time(peer_email) + def share_version_file_with_peer(self, peer_email: str) -> None: """Share version file with a peer so they can read it.""" connection = self.connection_for_own_syftbox() diff --git a/syft/sync/connections/drive/gdrive_transport.py b/syft/sync/connections/drive/gdrive_transport.py index 30b22fd878a..5c95c524e24 100644 --- a/syft/sync/connections/drive/gdrive_transport.py +++ b/syft/sync/connections/drive/gdrive_transport.py @@ -5,6 +5,7 @@ import logging import pickle import re +from datetime import datetime from functools import lru_cache from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, List, Optional @@ -321,6 +322,11 @@ def _ordered(entries: list[_VersionedFolder]) -> list[tuple[str, str]]: return _ordered(compatible), _ordered(older), _ordered(newer) +def _peer_version_file_query(peer_email: str) -> str: + """Drive query for the version file that ``peer_email`` owns.""" + return f"name='{SYFT_VERSION_FILE}' and trashed=false and '{peer_email}' in owners" + + class GDriveConnection(SyftboxPlatformConnection): """Google Drive Files API transport layer""" @@ -1892,9 +1898,7 @@ def write_version_file(self, version_info: "VersionInfo") -> None: def _get_peer_version_file_id(self, peer_email: str) -> Optional[str]: """Find SYFT_version.json file in a peer's /SyftBox folder""" # Find the peer's SyftBox folder - query = ( - f"name='{SYFT_VERSION_FILE}' and trashed=false and '{peer_email}' in owners" - ) + query = _peer_version_file_query(peer_email) results = execute_with_retries( self.drive_service.files().list(q=query, fields="files(id)") ) @@ -1939,6 +1943,24 @@ def read_peer_version_file(self, peer_email: str) -> Optional["VersionInfo"]: print(f"Warning: could not read the version file of {peer_email}: {e}") return None + def read_peer_version_file_modified_time( + self, peer_email: str + ) -> Optional[datetime]: + """Drive's last-modified time of a peer's version file, or None without one. + + The peer writes the file at each login. Drive sets this time, not the + peer's clock. + """ + query = _peer_version_file_query(peer_email) + results = execute_with_retries( + self.drive_service.files().list(q=query, fields="files(id,modifiedTime)") + ) + items = results.get("files", []) + if not items: + return None + # Python 3.10 fromisoformat() does not accept the "Z" suffix. + return datetime.fromisoformat(items[0]["modifiedTime"].replace("Z", "+00:00")) + def share_version_file_with_peer(self, peer_email: str) -> None: """Share the version file with a peer so they can read it.""" file_id = self._get_version_file_id() diff --git a/syft/sync/peers/peer.py b/syft/sync/peers/peer.py index 3b53e1a6163..82201a9c3e8 100644 --- a/syft/sync/peers/peer.py +++ b/syft/sync/peers/peer.py @@ -6,6 +6,14 @@ from syft.sync.version.version_info import VersionInfo +class PeerSetupError(ValueError): + """A peer is unknown, not approved, or not shown to be a live connection.""" + + +class PeerNotReadyError(PeerSetupError): + """The peer can still become valid: it did not approve yet, or log in recently.""" + + class PeerState(str, Enum): ACCEPTED = "accepted" REQUESTED_BY_PEER = "requested_by_peer" diff --git a/syft/sync/syftbox_manager.py b/syft/sync/syftbox_manager.py index 2fe36d09378..fa7038c6375 100644 --- a/syft/sync/syftbox_manager.py +++ b/syft/sync/syftbox_manager.py @@ -5,6 +5,7 @@ import time from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager +from datetime import timedelta from pathlib import Path from typing import List, Optional, cast @@ -24,7 +25,7 @@ ) from syft.sync.file_writer import FileWriter from syft.sync.job_file_change_handler import JobFileChangeHandler -from syft.sync.peers.peer import Peer +from syft.sync.peers.peer import Peer, PeerNotReadyError from syft.sync.peers.peer_list import PeerList from syft.sync.peers.peer_store import PeerStore from syft.sync.platforms.base_platform import BasePlatform @@ -50,6 +51,8 @@ random_syftbox_folder_for_testing, ) from syft.sync.version.peer_manager import ( + DEFAULT_PEER_LOGIN_MAX_AGE, + DEFAULT_PEER_POLL_INTERVAL, PeerManager, PeerManagerConfig, ) @@ -462,6 +465,7 @@ class SyftboxManager(BaseModelCallbackMixin): "load_peers", "approve_peer_request", "reject_peer_request", + "validate_peer", "sync", "create_checkpoint", "should_create_checkpoint", @@ -1009,6 +1013,57 @@ def reject_peer_request(self, email_or_peer: str | Peer): """Reject a pending peer request. Delegates to PeerManager.""" self.peer_manager.reject_peer_request(email_or_peer) + def validate_peer( + self, + peer_email: str, + max_age: timedelta = DEFAULT_PEER_LOGIN_MAX_AGE, + timeout: float = 0, + poll_interval: float = DEFAULT_PEER_POLL_INTERVAL, + ) -> Peer: + """Load the peers, then return ``peer_email`` if it is a live connection. + + Use it after peering, before the rest of a notebook runs. The rules are + in ``PeerManager.validate_peer``. With ``timeout`` (seconds), wait for + the peer to approve or log in. Each poll, every ``poll_interval`` + seconds, is one Drive request; the peers load again only when that + request shows a change. A peer that cannot become valid by waiting + fails at once. + + Raises: + PeerSetupError: ``peer_email`` is not a live connection. + """ + deadline = time.monotonic() + timeout + self.load_peers() + while True: + try: + return self.peer_manager.validate_peer(peer_email, max_age=max_age) + except PeerNotReadyError as e: + self._wait_until_peer_may_be_valid( + peer_email, max_age, deadline, poll_interval, e + ) + self.load_peers() + + def _wait_until_peer_may_be_valid( + self, + peer_email: str, + max_age: timedelta, + deadline: float, + poll_interval: float, + error: PeerNotReadyError, + ) -> None: + """Poll until ``peer_may_be_valid`` is True; raise ``error`` at ``deadline``. + + The last sleep is cut to end at ``deadline``. + """ + if deadline - time.monotonic() <= 0: + raise error + print(f"Waiting for {peer_email}: {error}") + while (remaining := deadline - time.monotonic()) > 0: + time.sleep(min(poll_interval, remaining)) + if self.peer_manager.peer_may_be_valid(peer_email, max_age=max_age): + return + raise error + # ========== Encryption key fingerprints ========== @property diff --git a/syft/sync/version/peer_manager.py b/syft/sync/version/peer_manager.py index f14dbd1c9ca..9246e8d1064 100644 --- a/syft/sync/version/peer_manager.py +++ b/syft/sync/version/peer_manager.py @@ -6,6 +6,7 @@ import logging import warnings from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timedelta, timezone from enum import Enum from pathlib import Path from typing import Dict, List, Optional @@ -25,7 +26,7 @@ format_fingerprint, normalize_fingerprint, ) -from syft.sync.peers.peer import Peer, PeerState +from syft.sync.peers.peer import Peer, PeerNotReadyError, PeerSetupError, PeerState from syft.sync.peers.peer_store import PeerStore, datasite_crypto_keys_path from syft.sync.utils.print_utils import ( print_peer_already_connected, @@ -43,6 +44,21 @@ logger = logging.getLogger(__name__) +# How recent a peer login must be for validate_peer() to accept a connection +# that was already approved at start. It covers a demo day; connections left +# over from earlier runs are usually older. +DEFAULT_PEER_LOGIN_MAX_AGE = timedelta(hours=12) + +# Seconds between polls when validate_peer() waits. Each poll is one Drive +# request; a peer approval takes longer than this anyway. +DEFAULT_PEER_POLL_INTERVAL = 15 + + +def _is_within(moment: Optional[datetime], max_age: timedelta) -> bool: + """True when ``moment`` is known and not older than ``max_age``.""" + return moment is not None and moment >= datetime.now(timezone.utc) - max_age + + # Key wrapping the DID document inside a published encryption bundle file. BUNDLE_FILE_KEY = "public_encryption_bundle" @@ -173,6 +189,9 @@ class PeerManager(BaseModel): _loaded_peer_versions: Dict[str, Optional[VersionInfo]] = PrivateAttr( default_factory=dict ) + # Emails of the peers that were approved at the first load_peers(). None + # until that load. + _approved_at_start: Optional[set[str]] = PrivateAttr(default=None) # ========== Peer List Properties ========== @@ -843,6 +862,11 @@ def load_peers(self, force_download: bool = False): email, PeerState.ACCEPTED.value ) + if self._approved_at_start is None: + self._approved_at_start = { + p.email for p in peers if p.state == PeerState.ACCEPTED + } + # set_peers keeps the locally pinned bundle over whatever the Drive # copy of SYFT_peers.json holds, and validates any bundle it adopts. self.peer_store.set_peers(peers) @@ -873,6 +897,87 @@ def _pin_missing_peer_bundles(self, peers: List[Peer]) -> None: peer.email, peer.state.value, public_encryption_bundle=adopted ) + def validate_peer( + self, peer_email: str, max_age: timedelta = DEFAULT_PEER_LOGIN_MAX_AGE + ) -> Peer: + """Return the peer ``peer_email`` if it is a live connection, else raise. + + A peer is valid when it is approved, and one of these is true: + - It was not approved yet at the first ``load_peers()`` of this client. + - It logged in within ``max_age``. The Drive modified time of its + version file gives the time. + + A connection that was approved at start can be left over from an + earlier run, with an account that is not in use now. Thus it does not + show that ``peer_email`` is the correct peer. This is a setup check, + not authentication. + + Raises: + PeerNotReadyError: the peer did not approve the request yet, or it + was approved at start and did not log in within ``max_age``. + PeerSetupError: the peer is unknown, rejected, or waits for this + client to approve its request. + """ + peer = self.get_cached_peer(peer_email) + if peer is None: + raise PeerSetupError( + f"{peer_email} is not a peer. Check the email, or add it with " + f"client.add_peer({peer_email!r})." + ) + if peer.is_requested_by_peer: + raise PeerSetupError( + f"{peer_email} is not approved yet: approve it with " + f"client.approve_peer_request({peer_email!r})." + ) + if peer.is_requested_by_me: + raise PeerNotReadyError( + f"{peer_email} has not approved the request yet. Check the email, " + "and make sure that the peer approved the request." + ) + if not peer.is_approved: + raise PeerSetupError( + f"{peer_email} is not approved (state: {peer.state.value})." + ) + if peer_email in (self._approved_at_start or set()): + self._require_recent_login(peer_email, max_age) + return peer + + def _require_recent_login(self, peer_email: str, max_age: timedelta) -> None: + """Raise ``PeerNotReadyError`` unless ``peer_email`` logged in within ``max_age``.""" + last_login = self.connection_router.read_peer_version_file_modified_time( + peer_email + ) + if _is_within(last_login, max_age): + return + raise PeerNotReadyError( + f"{peer_email} was already connected when this client started, and " + f"did not log in within {max_age} (last login: {last_login or 'unknown'}). " + "The connection can be left over from an earlier run. Check the " + "email, and make sure that the peer is logged in." + ) + + def peer_may_be_valid( + self, peer_email: str, max_age: timedelta = DEFAULT_PEER_LOGIN_MAX_AGE + ) -> bool: + """Tell if ``validate_peer`` can pass after the next ``load_peers()``. + + Use it between polls, after ``validate_peer`` raised + ``PeerNotReadyError``. It makes one Drive request and does not load + the peers. It is True when the peer created its folders for this + datasite (it approved our request), or when it logged in within + ``max_age``. + """ + peer = self.get_cached_peer(peer_email) + if peer is not None and peer.is_requested_by_me: + return any( + p.email == peer_email + for p in self.connection_router.get_peer_requests() + ) + last_login = self.connection_router.read_peer_version_file_modified_time( + peer_email + ) + return _is_within(last_login, max_age) + def check_peer_request_exists(self, email: str) -> bool: """Check if a peer request exists for the given email.""" return any(p.email == email for p in self.requested_by_peer_peers) diff --git a/tests/unit/test_validate_peer.py b/tests/unit/test_validate_peer.py new file mode 100644 index 00000000000..0db9f06e854 --- /dev/null +++ b/tests/unit/test_validate_peer.py @@ -0,0 +1,260 @@ +import inspect +from datetime import datetime, timedelta, timezone + +import pytest + +import syft.sync.syftbox_manager as syftbox_manager_module +from syft.sync.connections.drive import mock_drive_service +from syft.sync.peers.peer import PeerNotReadyError, PeerSetupError +from syft.sync.syftbox_manager import SyftboxManager + + +def _pair(add_peers: bool): + return SyftboxManager.pair_with_mock_drive_service_connection( + email1="do@test.com", email2="ds@test.com", add_peers=add_peers + ) + + +def _set_last_login(manager: SyftboxManager, when: datetime) -> None: + """Make Drive report that ``manager`` last wrote its version file at ``when``.""" + connection = manager._connection_router.connection_for_own_syftbox() + store = connection.drive_service._backing_store + store.files[connection._get_version_file_id()].modifiedTime = ( + when.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" + ) + + +def _no_sleep(_seconds): + raise AssertionError("validate_peer must not wait here") + + +def test_peer_connected_in_this_session_is_valid(): + ds_manager, do_manager = _pair(add_peers=False) + ds_manager.add_peer(do_manager.email) + do_manager.load_peers() + do_manager.approve_peer_request(ds_manager.email) + + assert ds_manager.validate_peer(do_manager.email).email == do_manager.email + assert do_manager.validate_peer(ds_manager.email).email == ds_manager.email + + +def test_unknown_peer_is_invalid(): + ds_manager, _ = _pair(add_peers=True) + + with pytest.raises(PeerSetupError, match="not a peer"): + ds_manager.validate_peer("wrong@test.com") + + +def test_unapproved_peer_is_invalid(): + ds_manager, do_manager = _pair(add_peers=False) + ds_manager.add_peer(do_manager.email) + + with pytest.raises(PeerNotReadyError, match="not approved"): + ds_manager.validate_peer(do_manager.email) + + +def test_peer_request_not_approved_by_me_is_invalid(): + ds_manager, do_manager = _pair(add_peers=False) + ds_manager.add_peer(do_manager.email) + + with pytest.raises(PeerSetupError, match="approve_peer_request"): + do_manager.validate_peer(ds_manager.email) + + +def test_peer_approved_without_request_is_invalid_until_peer_connects(): + # approve_peer_request(..., peer_must_exist=False) with a wrong email only + # sends a request; the peer is not connected. + ds_manager, do_manager = _pair(add_peers=False) + do_manager.approve_peer_request(ds_manager.email, peer_must_exist=False) + + with pytest.raises(PeerNotReadyError, match="not approved"): + do_manager.validate_peer(ds_manager.email) + + ds_manager.add_peer(do_manager.email) + assert do_manager.validate_peer(ds_manager.email).is_approved + + +def test_leftover_connection_with_stale_peer_is_invalid(): + ds_manager, do_manager = _pair(add_peers=True) + # The DS loaded peers first after the DO approved: the connection was + # already there when the DS client started. + _set_last_login(do_manager, datetime.now(timezone.utc) - timedelta(days=2)) + + with pytest.raises(PeerNotReadyError, match="already connected"): + ds_manager.validate_peer(do_manager.email) + + +def test_leftover_connection_with_same_day_peer_login_is_valid(): + ds_manager, do_manager = _pair(add_peers=True) + _set_last_login(do_manager, datetime.now(timezone.utc) - timedelta(hours=2)) + + assert ds_manager.validate_peer(do_manager.email).is_approved + + +def test_login_time_comes_from_drive_not_from_the_peer_clock(): + ds_manager, do_manager = _pair(add_peers=True) + # The peer's clock is two days behind; Drive stamps the upload with its own. + do_manager.peer_manager.get_own_version().updated_at = datetime.now( + timezone.utc + ) - timedelta(days=2) + do_manager.peer_manager.write_own_version() + + assert ds_manager.validate_peer(do_manager.email).is_approved + + +def test_max_age_sets_how_recent_the_peer_login_must_be(): + ds_manager, do_manager = _pair(add_peers=True) + _set_last_login(do_manager, datetime.now(timezone.utc) - timedelta(hours=2)) + + assert ds_manager.validate_peer(do_manager.email, max_age=timedelta(hours=3)) + with pytest.raises(PeerNotReadyError, match="already connected"): + ds_manager.validate_peer(do_manager.email, max_age=timedelta(hours=1)) + + +def test_timeout_waits_for_a_late_approval(monkeypatch): + ds_manager, do_manager = _pair(add_peers=False) + ds_manager.add_peer(do_manager.email) + do_manager.load_peers() + waits = [] + + def approve_during_wait(seconds): + waits.append(seconds) + do_manager.approve_peer_request(ds_manager.email) + + monkeypatch.setattr(syftbox_manager_module.time, "sleep", approve_during_wait) + + peer = ds_manager.validate_peer(do_manager.email, timeout=60, poll_interval=1) + assert peer.is_approved + assert waits == [1] + + +def test_timeout_expires_when_the_peer_never_approves(): + ds_manager, do_manager = _pair(add_peers=False) + ds_manager.add_peer(do_manager.email) + + with pytest.raises(PeerNotReadyError, match="not approved"): + ds_manager.validate_peer(do_manager.email, timeout=0.05, poll_interval=0.01) + + +def test_timeout_does_not_wait_when_waiting_cannot_help(monkeypatch): + ds_manager, do_manager = _pair(add_peers=False) + ds_manager.add_peer(do_manager.email) + monkeypatch.setattr(syftbox_manager_module.time, "sleep", _no_sleep) + + with pytest.raises(PeerSetupError, match="not a peer"): + ds_manager.validate_peer("wrong@test.com", timeout=60) + # A request that only this client can approve. + with pytest.raises(PeerSetupError, match="approve_peer_request"): + do_manager.validate_peer(ds_manager.email, timeout=60) + + +def _count_drive_requests(monkeypatch) -> list[int]: + """Count mock Drive requests; the returned one-item list holds the count.""" + count = [0] + for request_class in ( + mock_drive_service.MockListRequest, + mock_drive_service.MockGetRequest, + ): + original = request_class.execute + + def counted(self, *args, _original=original, **kwargs): + count[0] += 1 + return _original(self, *args, **kwargs) + + monkeypatch.setattr(request_class, "execute", counted) + # Downloads go through get_media(), not execute(). + original_get_media = mock_drive_service.MockFilesResource.get_media + + def counted_get_media(self, *args, **kwargs): + count[0] += 1 + return original_get_media(self, *args, **kwargs) + + monkeypatch.setattr( + mock_drive_service.MockFilesResource, "get_media", counted_get_media + ) + return count + + +def _count_load_peers(monkeypatch) -> list[int]: + count = [0] + original = SyftboxManager.load_peers + + def counted(self, *args, **kwargs): + count[0] += 1 + return original(self, *args, **kwargs) + + monkeypatch.setattr(SyftboxManager, "load_peers", counted) + return count + + +def test_wait_for_approval_polls_with_one_drive_request(monkeypatch): + ds_manager, do_manager = _pair(add_peers=False) + ds_manager.add_peer(do_manager.email) + drive_requests = _count_drive_requests(monkeypatch) + loads = _count_load_peers(monkeypatch) + clock = [0.0] + per_poll = [] + + def record_poll(seconds): + clock[0] += seconds + per_poll.append(drive_requests[0]) + drive_requests[0] = 0 + + monkeypatch.setattr(syftbox_manager_module.time, "sleep", record_poll) + monkeypatch.setattr(syftbox_manager_module.time, "monotonic", lambda: clock[0]) + + with pytest.raises(PeerNotReadyError, match="not approved"): + ds_manager.validate_peer(do_manager.email, timeout=4, poll_interval=1) + # Four polls in four seconds. Before the first sleep: the initial full + # load. After it: one Drive request per poll, and no other load. + assert len(per_poll) == 4 + assert per_poll[1:] == [1, 1, 1] + assert drive_requests[0] == 1 + assert loads[0] == 1 + + +def test_wait_for_login_reloads_once_the_peer_logs_in(monkeypatch): + ds_manager, do_manager = _pair(add_peers=True) + _set_last_login(do_manager, datetime.now(timezone.utc) - timedelta(days=2)) + loads = _count_load_peers(monkeypatch) + polls = [] + + def log_in_on_second_poll(_seconds): + polls.append(_seconds) + if len(polls) == 2: + do_manager.peer_manager.write_own_version() + + monkeypatch.setattr(syftbox_manager_module.time, "sleep", log_in_on_second_poll) + + assert ds_manager.validate_peer(do_manager.email, timeout=60, poll_interval=1) + assert len(polls) == 2 + assert loads[0] == 2 + + +def test_default_poll_interval_is_at_least_15s(): + default = ( + inspect.signature(SyftboxManager.validate_peer) + .parameters["poll_interval"] + .default + ) + assert default >= 15 + + +def test_timeout_shorter_than_poll_interval_still_waits(monkeypatch): + ds_manager, do_manager = _pair(add_peers=False) + ds_manager.add_peer(do_manager.email) + do_manager.load_peers() + clock = [0.0] + sleeps = [] + + def approve_during_wait(seconds): + clock[0] += seconds + sleeps.append(seconds) + do_manager.approve_peer_request(ds_manager.email) + + monkeypatch.setattr(syftbox_manager_module.time, "sleep", approve_during_wait) + monkeypatch.setattr(syftbox_manager_module.time, "monotonic", lambda: clock[0]) + + peer = ds_manager.validate_peer(do_manager.email, timeout=10, poll_interval=15) + assert peer.is_approved + assert sleeps == [10] From de96c54571d0ba1eb9338bb7c509975e78d19213 Mon Sep 17 00:00:00 2001 From: Pedro Werneck Date: Wed, 30 Sep 2026 10:04:31 -0300 Subject: [PATCH 2/4] pre-commit fixes --- .../1. DO-model-owner-gemma-restrict.ipynb | 46 ++++++------- ...2. DO-benchmark-owner-gemma-restrict.ipynb | 36 +++++------ .../3. DS-researcher-gemma-restrict.ipynb | 30 ++++----- .../1. DO-model-owner-gemma-shield.ipynb | 54 ++++++++-------- .../2. DO-benchmark-owner-gemma-shield.ipynb | 64 +++++++++---------- .../1. DO-model-owner-gemma-restrict.ipynb | 50 +++++++-------- ...2. DO-benchmark-owner-gemma-restrict.ipynb | 60 ++++++++--------- .../nanolm/2. enclave_eval_localenc.ipynb | 40 ++++++------ .../enclave/nanolm/3. enclave_eval_e2e.ipynb | 46 ++++++------- .../nbsplit/1. DO-model-owner-nanolm.ipynb | 26 ++++---- .../2. DO-benchmark-owner-nanolm.ipynb | 26 ++++---- .../nbsplit/3. DS-researcher-nanolm.ipynb | 30 ++++----- 12 files changed, 254 insertions(+), 254 deletions(-) diff --git a/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb index 527ecf02ffc..f2060c26b07 100644 --- a/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb @@ -194,7 +194,7 @@ { "cell_type": "code", "execution_count": null, - "id": "validate-peers", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -206,7 +206,7 @@ }, { "cell_type": "markdown", - "id": "15", + "id": "16", "metadata": {}, "source": [ "### Step 1.2 — Attest the enclave's identity\n", @@ -217,7 +217,7 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -227,7 +227,7 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "18", "metadata": {}, "source": [ "## Step 2 — Get the Gemma 3 model\n", @@ -238,7 +238,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -249,7 +249,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -265,7 +265,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -274,7 +274,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "22", "metadata": {}, "source": [ "## Step 3 — Prepare the files for upload\n", @@ -285,7 +285,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -335,7 +335,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -351,7 +351,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "25", "metadata": {}, "source": [ "## Step 4 — Upload the private model\n", @@ -362,7 +362,7 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -381,7 +381,7 @@ }, { "cell_type": "markdown", - "id": "26", + "id": "27", "metadata": {}, "source": [ "## Step 5 — Run `syft-restrict` on the inference code\n", @@ -395,7 +395,7 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -455,7 +455,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -476,7 +476,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "30", "metadata": {}, "source": [ "### Step 5.1 — Approve the job\n", @@ -487,7 +487,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -499,7 +499,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -509,7 +509,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -520,7 +520,7 @@ }, { "cell_type": "markdown", - "id": "33", + "id": "34", "metadata": {}, "source": [ "## Step 6 — Approve the final inference job\n", @@ -531,7 +531,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -544,7 +544,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -554,7 +554,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "37", "metadata": {}, "outputs": [], "source": [ diff --git a/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb index bf6272334ba..9c8c4ee0768 100644 --- a/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb @@ -167,7 +167,7 @@ { "cell_type": "code", "execution_count": null, - "id": "validate-peers", + "id": "14", "metadata": {}, "outputs": [], "source": [ @@ -179,7 +179,7 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "15", "metadata": {}, "source": [ "### Step 1.2 — Attest the enclave's identity\n", @@ -190,7 +190,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -200,7 +200,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "17", "metadata": {}, "source": [ "## Step 2 — Prepare the benchmark\n", @@ -211,7 +211,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -245,7 +245,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -258,7 +258,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "20", "metadata": {}, "source": [ "## Step 3 — Upload the benchmark\n", @@ -269,7 +269,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -288,7 +288,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "22", "metadata": {}, "source": [ "## Step 4 — Review the `syft-restrict` job\n", @@ -304,7 +304,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -317,7 +317,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -327,7 +327,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -338,7 +338,7 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "26", "metadata": {}, "source": [ "### Step 4.1 — Read the certificate\n", @@ -349,7 +349,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -366,7 +366,7 @@ }, { "cell_type": "markdown", - "id": "27", + "id": "28", "metadata": {}, "source": [ "## Step 5 — Approve the inference job\n", @@ -377,7 +377,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -390,7 +390,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -400,7 +400,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "31", "metadata": {}, "outputs": [], "source": [ diff --git a/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb index d695cd17288..175c1114da9 100644 --- a/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb @@ -154,7 +154,7 @@ { "cell_type": "code", "execution_count": null, - "id": "validate-peers", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -167,7 +167,7 @@ }, { "cell_type": "markdown", - "id": "13", + "id": "14", "metadata": {}, "source": [ "### Step 1.2 — Attest enclave's identity" @@ -176,7 +176,7 @@ { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -186,7 +186,7 @@ }, { "cell_type": "markdown", - "id": "15", + "id": "16", "metadata": {}, "source": [ "## Step 2 — Browse the mock datasets" @@ -195,7 +195,7 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -205,7 +205,7 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "18", "metadata": {}, "source": [ "## Step 3 — Job helpers" @@ -214,7 +214,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -228,7 +228,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "20", "metadata": {}, "source": [ "## Step 4 — Define the inference job\n", @@ -239,7 +239,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -303,7 +303,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "22", "metadata": {}, "source": [ "## Step 5 — Submit the job to the enclave" @@ -312,7 +312,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -335,7 +335,7 @@ }, { "cell_type": "markdown", - "id": "23", + "id": "24", "metadata": {}, "source": [ "## Step 6 — Wait for both DOs to approve and the enclave to run\n", @@ -346,7 +346,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -361,7 +361,7 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "26", "metadata": {}, "source": [ "## Step 7 — View the results" @@ -370,7 +370,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "27", "metadata": {}, "outputs": [], "source": [ diff --git a/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb b/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb index e8596c55782..77f65d906b8 100644 --- a/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb +++ b/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb @@ -245,7 +245,7 @@ { "cell_type": "code", "execution_count": null, - "id": "validate-peers", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -257,7 +257,7 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "18", "metadata": {}, "source": [ "### Step 1.2 — Attest the enclave's identity\n", @@ -268,7 +268,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -278,7 +278,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "20", "metadata": {}, "source": [ "## Step 2 — Get the two models\n", @@ -295,7 +295,7 @@ }, { "cell_type": "markdown", - "id": "20", + "id": "21", "metadata": {}, "source": [ "### Step 2.1 — Gemma 3 from Kaggle\n" @@ -304,7 +304,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -323,7 +323,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -338,7 +338,7 @@ }, { "cell_type": "markdown", - "id": "23", + "id": "24", "metadata": {}, "source": [ "### Step 2.2 — ShieldGemma 2B from Hugging Face\n", @@ -351,7 +351,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -376,7 +376,7 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "26", "metadata": {}, "source": [ "## Step 3 — Prepare the files for upload\n", @@ -390,7 +390,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -468,7 +468,7 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -487,7 +487,7 @@ }, { "cell_type": "markdown", - "id": "28", + "id": "29", "metadata": {}, "source": [ "## Step 4 — Upload the two private models\n", @@ -498,7 +498,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -524,7 +524,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -535,7 +535,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "32", "metadata": {}, "source": [ "## Step 5 — Run `syft-restrict` on the inference code\n", @@ -549,7 +549,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -609,7 +609,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -629,7 +629,7 @@ }, { "cell_type": "markdown", - "id": "34", + "id": "35", "metadata": {}, "source": [ "### Step 5.1 — Approve the job\n", @@ -640,7 +640,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -655,7 +655,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -665,7 +665,7 @@ { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -675,7 +675,7 @@ }, { "cell_type": "markdown", - "id": "38", + "id": "39", "metadata": {}, "source": [ "\"Wait\n" @@ -683,7 +683,7 @@ }, { "cell_type": "markdown", - "id": "39", + "id": "40", "metadata": {}, "source": [ "## Step 6 — Approve the final inference job\n", @@ -694,7 +694,7 @@ { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "41", "metadata": {}, "outputs": [], "source": [ @@ -710,7 +710,7 @@ { "cell_type": "code", "execution_count": null, - "id": "41", + "id": "42", "metadata": {}, "outputs": [], "source": [ @@ -720,7 +720,7 @@ { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "43", "metadata": {}, "outputs": [], "source": [ diff --git a/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb b/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb index 90e8cb670f4..0364425290c 100644 --- a/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb +++ b/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb @@ -187,7 +187,7 @@ { "cell_type": "code", "execution_count": null, - "id": "validate-peers", + "id": "14", "metadata": {}, "outputs": [], "source": [ @@ -199,7 +199,7 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "15", "metadata": {}, "source": [ "### Step 1.2 — Attest the enclave's identity\n", @@ -210,7 +210,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -220,7 +220,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "17", "metadata": {}, "source": [ "## Step 2 — Prepare the benchmark\n", @@ -235,7 +235,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -301,7 +301,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -312,7 +312,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -325,7 +325,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -343,7 +343,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "22", "metadata": {}, "source": [ "## Step 3 — Upload the benchmark\n", @@ -354,7 +354,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -371,7 +371,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -381,7 +381,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "25", "metadata": {}, "source": [ "\"Wait\n" @@ -389,7 +389,7 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "26", "metadata": {}, "source": [ "## Step 4 — Review the `syft-restrict` job\n", @@ -405,7 +405,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -421,7 +421,7 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -431,7 +431,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -441,7 +441,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "30", "metadata": {}, "source": [ "### Step 4.1 — Read the certificate\n", @@ -452,7 +452,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -472,7 +472,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "32", "metadata": {}, "source": [ "## Step 5 — Submit the inference job\n", @@ -489,7 +489,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -498,7 +498,7 @@ }, { "cell_type": "markdown", - "id": "33", + "id": "34", "metadata": {}, "source": [ "### Step 5.1 — Read the model owner's guardrail\n", @@ -509,7 +509,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -527,7 +527,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -659,7 +659,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -687,7 +687,7 @@ }, { "cell_type": "markdown", - "id": "37", + "id": "38", "metadata": {}, "source": [ "## Step 6 — Approve the inference job\n", @@ -698,7 +698,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "39", "metadata": {}, "outputs": [], "source": [ @@ -710,7 +710,7 @@ { "cell_type": "code", "execution_count": null, - "id": "39", + "id": "40", "metadata": {}, "outputs": [], "source": [ @@ -720,7 +720,7 @@ { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "41", "metadata": {}, "outputs": [], "source": [ @@ -730,7 +730,7 @@ }, { "cell_type": "markdown", - "id": "41", + "id": "42", "metadata": {}, "source": [ "\"Wait\n" @@ -738,7 +738,7 @@ }, { "cell_type": "markdown", - "id": "42", + "id": "43", "metadata": {}, "source": [ "## Step 7 — View the results\n", @@ -751,7 +751,7 @@ { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "44", "metadata": {}, "outputs": [], "source": [ @@ -767,7 +767,7 @@ { "cell_type": "code", "execution_count": null, - "id": "44", + "id": "45", "metadata": {}, "outputs": [], "source": [ diff --git a/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb index 4bbd55c2809..b7fa8ac6eb6 100644 --- a/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb @@ -197,7 +197,7 @@ { "cell_type": "code", "execution_count": null, - "id": "validate-peers", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -209,7 +209,7 @@ }, { "cell_type": "markdown", - "id": "15", + "id": "16", "metadata": {}, "source": [ "### Step 1.2 — Attest the enclave's identity\n", @@ -220,7 +220,7 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -230,7 +230,7 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "18", "metadata": {}, "source": [ "## Step 2 — Get the Gemma 3 model\n", @@ -240,7 +240,7 @@ }, { "cell_type": "markdown", - "id": "18", + "id": "19", "metadata": {}, "source": [ "The Notebooks assumes, that gemma-3 model is present in your Google Drive, kindly upload the model to your Google Drive. You could use the kaggle API to download the model from Kaggle and upload it to your Google Drive. The command to download the model from Kaggle is below.\n", @@ -251,7 +251,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -270,7 +270,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -287,7 +287,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "22", "metadata": {}, "source": [ "## Step 3 — Prepare the files for upload\n", @@ -298,7 +298,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -340,7 +340,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -356,7 +356,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "25", "metadata": {}, "source": [ "## Step 4 — Upload the private model\n", @@ -367,7 +367,7 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -384,7 +384,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -394,7 +394,7 @@ }, { "cell_type": "markdown", - "id": "27", + "id": "28", "metadata": {}, "source": [ "## Step 5 — Run `syft-restrict` on the inference code\n", @@ -408,7 +408,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -468,7 +468,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -488,7 +488,7 @@ }, { "cell_type": "markdown", - "id": "30", + "id": "31", "metadata": {}, "source": [ "### Step 5.1 — Approve the job\n", @@ -499,7 +499,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -514,7 +514,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -524,7 +524,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -534,7 +534,7 @@ }, { "cell_type": "markdown", - "id": "34", + "id": "35", "metadata": {}, "source": [ "\"Wait\n" @@ -542,7 +542,7 @@ }, { "cell_type": "markdown", - "id": "35", + "id": "36", "metadata": {}, "source": [ "## Step 6 — Approve the final inference job\n", @@ -553,7 +553,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -569,7 +569,7 @@ { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -579,7 +579,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "39", "metadata": {}, "outputs": [], "source": [ diff --git a/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb index 79a397127e2..7b4f5f5f74e 100644 --- a/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb @@ -176,7 +176,7 @@ { "cell_type": "code", "execution_count": null, - "id": "validate-peers", + "id": "14", "metadata": {}, "outputs": [], "source": [ @@ -188,7 +188,7 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "15", "metadata": {}, "source": [ "### Step 1.2 — Attest the enclave's identity\n", @@ -199,7 +199,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -209,7 +209,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "17", "metadata": {}, "source": [ "## Step 2 — Prepare the benchmark\n", @@ -224,7 +224,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -290,7 +290,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -301,7 +301,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -314,7 +314,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -332,7 +332,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "22", "metadata": {}, "source": [ "## Step 3 — Upload the benchmark\n", @@ -343,7 +343,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -360,7 +360,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -370,7 +370,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "25", "metadata": {}, "source": [ "\"Wait\n" @@ -378,7 +378,7 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "26", "metadata": {}, "source": [ "## Step 4 — Review the `syft-restrict` job\n", @@ -394,7 +394,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -410,7 +410,7 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -420,7 +420,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -430,7 +430,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "30", "metadata": {}, "source": [ "### Step 4.1 — Read the certificate\n", @@ -441,7 +441,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -461,7 +461,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "32", "metadata": {}, "source": [ "## Step 5 — Submit the inference job\n", @@ -475,7 +475,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -485,7 +485,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -563,7 +563,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -588,7 +588,7 @@ }, { "cell_type": "markdown", - "id": "35", + "id": "36", "metadata": {}, "source": [ "## Step 6 — Approve the inference job\n", @@ -599,7 +599,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -611,7 +611,7 @@ { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -621,7 +621,7 @@ { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "39", "metadata": {}, "outputs": [], "source": [ @@ -631,7 +631,7 @@ }, { "cell_type": "markdown", - "id": "39", + "id": "40", "metadata": {}, "source": [ "\"Wait\n" @@ -639,7 +639,7 @@ }, { "cell_type": "markdown", - "id": "40", + "id": "41", "metadata": {}, "source": [ "## Step 7 — View the results\n", @@ -650,7 +650,7 @@ { "cell_type": "code", "execution_count": null, - "id": "41", + "id": "42", "metadata": {}, "outputs": [], "source": [ @@ -666,7 +666,7 @@ { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "43", "metadata": {}, "outputs": [], "source": [ diff --git a/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb b/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb index 17160748c21..43a144ee19e 100644 --- a/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb +++ b/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb @@ -348,7 +348,7 @@ { "cell_type": "code", "execution_count": null, - "id": "validate-peers", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -365,7 +365,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "17", "metadata": {}, "source": [ "## Step 3 — Model owner uploads the Gemma model\n", @@ -377,7 +377,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -397,7 +397,7 @@ }, { "cell_type": "markdown", - "id": "18", + "id": "19", "metadata": {}, "source": [ "## Step 4 — Benchmark owner uploads the evaluation benchmark" @@ -406,7 +406,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -426,7 +426,7 @@ }, { "cell_type": "markdown", - "id": "20", + "id": "21", "metadata": {}, "source": [ "## Step 5 — Researcher browses the mock datasets" @@ -435,7 +435,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -445,7 +445,7 @@ }, { "cell_type": "markdown", - "id": "22", + "id": "23", "metadata": {}, "source": [ "## Step 6 — Researcher writes and submits the inference job\n", @@ -458,7 +458,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -514,7 +514,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -533,7 +533,7 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "26", "metadata": {}, "source": [ "## Step 7 — Enclave receives and distributes the job\n", @@ -545,7 +545,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -562,7 +562,7 @@ }, { "cell_type": "markdown", - "id": "27", + "id": "28", "metadata": {}, "source": [ "## Step 8 — Model owner and Benchmark owner approve\n", @@ -574,7 +574,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -585,7 +585,7 @@ }, { "cell_type": "markdown", - "id": "29", + "id": "30", "metadata": {}, "source": [ "## Step 9 — Enclave runs the job and distributes results\n", @@ -597,7 +597,7 @@ { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "31", "metadata": {}, "outputs": [], "source": [ @@ -614,7 +614,7 @@ }, { "cell_type": "markdown", - "id": "31", + "id": "32", "metadata": {}, "source": [ "## Step 10 — Researcher retrieves the result" @@ -623,7 +623,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -642,7 +642,7 @@ }, { "cell_type": "markdown", - "id": "33", + "id": "34", "metadata": {}, "source": [ "## Step 11 — Model owner and Benchmark owner also receive the result\n", @@ -653,7 +653,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "35", "metadata": {}, "outputs": [], "source": [ diff --git a/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb b/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb index 057313afec0..50fa4fc4b62 100644 --- a/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb +++ b/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb @@ -220,7 +220,7 @@ { "cell_type": "code", "execution_count": null, - "id": "validate-peers", + "id": "12", "metadata": {}, "outputs": [], "source": [ @@ -237,7 +237,7 @@ }, { "cell_type": "markdown", - "id": "12", + "id": "13", "metadata": {}, "source": [ "## Step 2.1 Dataset Helpers " @@ -246,7 +246,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "14", "metadata": {}, "outputs": [], "source": [ @@ -314,7 +314,7 @@ { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -348,7 +348,7 @@ }, { "cell_type": "markdown", - "id": "15", + "id": "16", "metadata": {}, "source": [ "## Step 2.2 — Model owner uploads the model weights\n", @@ -360,7 +360,7 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -380,7 +380,7 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "18", "metadata": {}, "source": [ "## Step 3 — Benchmark owner uploads the evaluation benchmark" @@ -389,7 +389,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -409,7 +409,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "20", "metadata": {}, "source": [ "## Step 4 — Researcher browses the mock datasets" @@ -418,7 +418,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -428,7 +428,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "22", "metadata": {}, "source": [ "## Step 5 — Researcher writes and submits the inference job\n", @@ -440,7 +440,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -497,7 +497,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -516,7 +516,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "25", "metadata": {}, "source": [ "## Step 6 — Enclave receives and distributes the job\n", @@ -528,7 +528,7 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -543,7 +543,7 @@ }, { "cell_type": "markdown", - "id": "26", + "id": "27", "metadata": {}, "source": [ "## Step 7 — Model owner and Benchmark owner approve\n", @@ -555,7 +555,7 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -566,7 +566,7 @@ }, { "cell_type": "markdown", - "id": "28", + "id": "29", "metadata": {}, "source": [ "## Step 8 — Enclave runs the job and distributes results\n", @@ -578,7 +578,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -593,7 +593,7 @@ }, { "cell_type": "markdown", - "id": "30", + "id": "31", "metadata": {}, "source": [ "## Step 9 — Researcher retrieves the result" @@ -602,7 +602,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -621,7 +621,7 @@ }, { "cell_type": "markdown", - "id": "32", + "id": "33", "metadata": {}, "source": [ "## Step 10 — Model owner and Benchmark owner also receive the result\n", @@ -632,7 +632,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "34", "metadata": {}, "outputs": [], "source": [ diff --git a/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb b/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb index 599fcfff10d..662e303ebff 100644 --- a/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb +++ b/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb @@ -162,7 +162,7 @@ { "cell_type": "code", "execution_count": null, - "id": "validate-peers", + "id": "14", "metadata": {}, "outputs": [], "source": [ @@ -174,7 +174,7 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "15", "metadata": {}, "source": [ "### Step 1.2 — Attest enclave's identity" @@ -183,7 +183,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -193,7 +193,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "17", "metadata": {}, "source": [ "## Step 2.1 — Dataset helpers" @@ -202,7 +202,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -251,7 +251,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -266,7 +266,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "20", "metadata": {}, "source": [ "## Step 2.2 — Upload the model weights\n", @@ -278,7 +278,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -298,7 +298,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "22", "metadata": {}, "source": [ "## Step 3 — Wait for the Researcher to submit the job, then approve\n", @@ -309,7 +309,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -322,7 +322,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -333,7 +333,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "25", "metadata": {}, "source": [ "## Step 4 — Receive results\n", @@ -344,7 +344,7 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "26", "metadata": {}, "outputs": [], "source": [ diff --git a/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb b/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb index 09f54205e1f..5652781f1d4 100644 --- a/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb +++ b/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb @@ -162,7 +162,7 @@ { "cell_type": "code", "execution_count": null, - "id": "validate-peers", + "id": "14", "metadata": {}, "outputs": [], "source": [ @@ -174,7 +174,7 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "15", "metadata": {}, "source": [ "### Step 1.2 — Attest enclave's identity" @@ -183,7 +183,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -193,7 +193,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "17", "metadata": {}, "source": [ "## Step 2.1 — Dataset helpers" @@ -202,7 +202,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -229,7 +229,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -246,7 +246,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "20", "metadata": {}, "source": [ "## Step 2.2 — Upload the evaluation benchmark\n", @@ -257,7 +257,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -277,7 +277,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "22", "metadata": {}, "source": [ "## Step 3 — Wait for the Researcher to submit the job, then approve\n", @@ -288,7 +288,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -301,7 +301,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -312,7 +312,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "25", "metadata": {}, "source": [ "## Step 4 — Receive results\n", @@ -323,7 +323,7 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "26", "metadata": {}, "outputs": [], "source": [ diff --git a/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb b/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb index 2ad64d951a1..05d574255b3 100644 --- a/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb +++ b/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb @@ -154,7 +154,7 @@ { "cell_type": "code", "execution_count": null, - "id": "validate-peers", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -167,7 +167,7 @@ }, { "cell_type": "markdown", - "id": "13", + "id": "14", "metadata": {}, "source": [ "### Step 1.2 — Attest enclave's identity" @@ -176,7 +176,7 @@ { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -186,7 +186,7 @@ }, { "cell_type": "markdown", - "id": "15", + "id": "16", "metadata": {}, "source": [ "## Step 2 — Browse the mock datasets" @@ -195,7 +195,7 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -205,7 +205,7 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "18", "metadata": {}, "source": [ "## Step 3 — Job helpers" @@ -214,7 +214,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -228,7 +228,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "20", "metadata": {}, "source": [ "## Step 4 — Define the inference job\n", @@ -239,7 +239,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -295,7 +295,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "22", "metadata": {}, "source": [ "## Step 5 — Submit the job to the enclave" @@ -304,7 +304,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -324,7 +324,7 @@ }, { "cell_type": "markdown", - "id": "23", + "id": "24", "metadata": {}, "source": [ "## Step 6 — Wait for both DOs to approve and the enclave to run\n", @@ -335,7 +335,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -350,7 +350,7 @@ }, { "cell_type": "markdown", - "id": "25", + "id": "26", "metadata": {}, "source": [ "## Step 7 — View the results" @@ -359,7 +359,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "27", "metadata": {}, "outputs": [], "source": [ From 8c9289dda89efc098639f4af14e4d381796bc5e9 Mon Sep 17 00:00:00 2001 From: Pedro Werneck Date: Wed, 30 Sep 2026 17:40:03 -0300 Subject: [PATCH 3/4] fix: warn about peers that were connected or waiting at login validate_peer() now warns about a peer that was already connected, or already waiting for our approval, at the first load_peers(). Either can be left over from an earlier run. The login-time check and its Drive read are removed. delete_syftbox() clears the record, so a reset client does not warn. The notebooks leave the enclave to attest_peer(). --- .../1. DO-benchmark-owner-dbe.ipynb | 5 +- .../2. DO-model-owner-dbe.ipynb | 5 +- .../1. DO-model-owner-gemma-restrict.ipynb | 5 +- ...2. DO-benchmark-owner-gemma-restrict.ipynb | 5 +- .../3. DS-researcher-gemma-restrict.ipynb | 5 +- .../1. DO-model-owner-gemma-shield.ipynb | 5 +- .../2. DO-benchmark-owner-gemma-shield.ipynb | 5 +- .../1. DO-model-owner-gemma-restrict.ipynb | 5 +- ...2. DO-benchmark-owner-gemma-restrict.ipynb | 5 +- .../nanolm/2. enclave_eval_localenc.ipynb | 5 +- .../enclave/nanolm/3. enclave_eval_e2e.ipynb | 5 +- .../nbsplit/1. DO-model-owner-nanolm.ipynb | 5 +- .../2. DO-benchmark-owner-nanolm.ipynb | 5 +- .../nbsplit/3. DS-researcher-nanolm.ipynb | 5 +- .../syft-enclave/src/syft_enclaves/client.py | 11 +- .../syft-enclave/tests/test_enclave_client.py | 1 - syft/sync/connections/connection_router.py | 8 -- .../connections/drive/gdrive_transport.py | 28 +--- syft/sync/syftbox_manager.py | 13 +- syft/sync/version/peer_manager.py | 103 ++++++-------- tests/unit/test_validate_peer.py | 130 +++++------------- 21 files changed, 132 insertions(+), 232 deletions(-) diff --git a/notebooks/enclave/double-blind-eval-w-receipts/1. DO-benchmark-owner-dbe.ipynb b/notebooks/enclave/double-blind-eval-w-receipts/1. DO-benchmark-owner-dbe.ipynb index 5a9de61ac54..1cf121a0df1 100644 --- a/notebooks/enclave/double-blind-eval-w-receipts/1. DO-benchmark-owner-dbe.ipynb +++ b/notebooks/enclave/double-blind-eval-w-receipts/1. DO-benchmark-owner-dbe.ipynb @@ -168,8 +168,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", - "# left over from an earlier run.\n", + "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", + "# Warn if the peer was already there before this notebook logged in.\n", + "# attest_peer() below checks the enclave.\n", "benchmark_owner.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", "print(f\" Connected to {MODEL_OWNER_EMAIL}\")" ] diff --git a/notebooks/enclave/double-blind-eval-w-receipts/2. DO-model-owner-dbe.ipynb b/notebooks/enclave/double-blind-eval-w-receipts/2. DO-model-owner-dbe.ipynb index a945ed21c3b..ebb59963e94 100644 --- a/notebooks/enclave/double-blind-eval-w-receipts/2. DO-model-owner-dbe.ipynb +++ b/notebooks/enclave/double-blind-eval-w-receipts/2. DO-model-owner-dbe.ipynb @@ -176,8 +176,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", - "# left over from an earlier run.\n", + "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", + "# Warn if the peer was already there before this notebook logged in.\n", + "# attest_peer() below checks the enclave.\n", "model_owner.validate_peer(BENCHMARK_OWNER_EMAIL, timeout=300)\n", "print(f\" Connected to {BENCHMARK_OWNER_EMAIL}\")" ] diff --git a/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb index f2060c26b07..c100dab22e8 100644 --- a/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb @@ -198,8 +198,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", - "# left over from an earlier run.\n", + "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", + "# Warn if the peer was already there before this notebook logged in.\n", + "# attest_peer() below checks the enclave.\n", "model_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", "print(f\" Connected to {RESEARCHER_EMAIL}\")" ] diff --git a/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb index 9c8c4ee0768..5142d00eaf2 100644 --- a/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb @@ -171,8 +171,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", - "# left over from an earlier run.\n", + "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", + "# Warn if the peer was already there before this notebook logged in.\n", + "# attest_peer() below checks the enclave.\n", "benchmark_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", "print(f\" Connected to {RESEARCHER_EMAIL}\")" ] diff --git a/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb index 175c1114da9..fd789d864b6 100644 --- a/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb @@ -158,8 +158,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", - "# left over from an earlier run.\n", + "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", + "# Warn if the peer was already there before this notebook logged in.\n", + "# attest_peer() below checks the enclave.\n", "for peer_email in [MODEL_OWNER_EMAIL, BENCHMARK_OWNER_EMAIL]:\n", " researcher.validate_peer(peer_email, timeout=300)\n", " print(f\" Connected to {peer_email}\")" diff --git a/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb b/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb index 77f65d906b8..d4707d8b37a 100644 --- a/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb +++ b/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb @@ -249,8 +249,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", - "# left over from an earlier run.\n", + "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", + "# Warn if the peer was already there before this notebook logged in.\n", + "# attest_peer() below checks the enclave.\n", "model_owner.validate_peer(BENCHMARK_OWNER_EMAIL, timeout=300)\n", "print(f\" Connected to {BENCHMARK_OWNER_EMAIL}\")" ] diff --git a/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb b/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb index 0364425290c..da66d8a57c3 100644 --- a/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb +++ b/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb @@ -191,8 +191,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", - "# left over from an earlier run.\n", + "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", + "# Warn if the peer was already there before this notebook logged in.\n", + "# attest_peer() below checks the enclave.\n", "benchmark_owner.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", "print(f\" Connected to {MODEL_OWNER_EMAIL}\")" ] diff --git a/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb index b7fa8ac6eb6..468f8ca7350 100644 --- a/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb @@ -201,8 +201,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", - "# left over from an earlier run.\n", + "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", + "# Warn if the peer was already there before this notebook logged in.\n", + "# attest_peer() below checks the enclave.\n", "model_owner.validate_peer(BENCHMARK_OWNER_EMAIL, timeout=300)\n", "print(f\" Connected to {BENCHMARK_OWNER_EMAIL}\")" ] diff --git a/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb index 7b4f5f5f74e..1f7f49cee3b 100644 --- a/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb @@ -180,8 +180,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", - "# left over from an earlier run.\n", + "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", + "# Warn if the peer was already there before this notebook logged in.\n", + "# attest_peer() below checks the enclave.\n", "benchmark_owner.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", "print(f\" Connected to {MODEL_OWNER_EMAIL}\")" ] diff --git a/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb b/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb index 43a144ee19e..2eb57b9bab1 100644 --- a/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb +++ b/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb @@ -352,8 +352,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", - "# left over from an earlier run.\n", + "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", + "# Warn if the peer was already there before this notebook logged in.\n", + "# Nothing in this notebook checks the enclave address.\n", "# Each call waits at most 300 s, so the worst case for this cell is\n", "# 20 minutes (4 x 300 s).\n", "researcher.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", diff --git a/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb b/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb index 50fa4fc4b62..7c69038eb2f 100644 --- a/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb +++ b/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb @@ -224,8 +224,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", - "# left over from an earlier run.\n", + "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", + "# Warn if the peer was already there before this notebook logged in.\n", + "# Nothing in this notebook checks the enclave address.\n", "# Each call waits at most 300 s, so the worst case for this cell is\n", "# 20 minutes (4 x 300 s).\n", "researcher.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", diff --git a/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb b/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb index 662e303ebff..7f1d47098bc 100644 --- a/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb +++ b/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb @@ -166,8 +166,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", - "# left over from an earlier run.\n", + "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", + "# Warn if the peer was already there before this notebook logged in.\n", + "# attest_peer() below checks the enclave.\n", "model_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", "print(f\" Connected to {RESEARCHER_EMAIL}\")" ] diff --git a/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb b/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb index 5652781f1d4..542cc5cc84b 100644 --- a/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb +++ b/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb @@ -166,8 +166,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", - "# left over from an earlier run.\n", + "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", + "# Warn if the peer was already there before this notebook logged in.\n", + "# attest_peer() below checks the enclave.\n", "benchmark_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", "print(f\" Connected to {RESEARCHER_EMAIL}\")" ] diff --git a/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb b/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb index 05d574255b3..856d5f4035b 100644 --- a/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb +++ b/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb @@ -158,8 +158,9 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for the peer. Fail on a wrong email, or on a connection\n", - "# left over from an earlier run.\n", + "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", + "# Warn if the peer was already there before this notebook logged in.\n", + "# attest_peer() below checks the enclave.\n", "for peer_email in [MODEL_OWNER_EMAIL, BENCHMARK_OWNER_EMAIL]:\n", " researcher.validate_peer(peer_email, timeout=300)\n", " print(f\" Connected to {peer_email}\")" diff --git a/packages/syft-enclave/src/syft_enclaves/client.py b/packages/syft-enclave/src/syft_enclaves/client.py index 460e9d1afee..097f7923479 100644 --- a/packages/syft-enclave/src/syft_enclaves/client.py +++ b/packages/syft-enclave/src/syft_enclaves/client.py @@ -1,7 +1,7 @@ import json import logging import os -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone from pathlib import Path from typing import TYPE_CHECKING, Optional @@ -15,11 +15,7 @@ from syft.sync.peers.peer import Peer from syft.sync.peers.peer_list import PeerList -from syft.sync.version.peer_manager import ( - DEFAULT_PEER_LOGIN_MAX_AGE, - DEFAULT_PEER_POLL_INTERVAL, - CompatAction, -) +from syft.sync.version.peer_manager import DEFAULT_PEER_POLL_INTERVAL, CompatAction from syft_enclaves.attestation.dispatch import policy_for, verify_evidence from syft_enclaves.attestation.envelope import AttestationEvidence from syft_enclaves.enclave_job_client import EnclaveJobClient @@ -117,12 +113,11 @@ def reject_peer_request(self, email_or_peer: str | Peer): def validate_peer( self, peer_email: str, - max_age: timedelta = DEFAULT_PEER_LOGIN_MAX_AGE, timeout: float = 0, poll_interval: float = DEFAULT_PEER_POLL_INTERVAL, ) -> Peer: return self._rds.validate_peer( - peer_email, max_age=max_age, timeout=timeout, poll_interval=poll_interval + peer_email, timeout=timeout, poll_interval=poll_interval ) def attest_peer( diff --git a/packages/syft-enclave/tests/test_enclave_client.py b/packages/syft-enclave/tests/test_enclave_client.py index 3771ae1f5e1..24ec47bc34f 100644 --- a/packages/syft-enclave/tests/test_enclave_client.py +++ b/packages/syft-enclave/tests/test_enclave_client.py @@ -51,6 +51,5 @@ def test_validate_peer_accepts_live_peer_and_rejects_wrong_email(): _, do1, _, ds = SyftEnclaveClient.quad_with_mock_drive_service_connection() assert ds.validate_peer(do1.email).email == do1.email - assert do1.validate_peer(ds.email).email == ds.email with pytest.raises(PeerSetupError, match="not a peer"): ds.validate_peer("wrong@test.com") diff --git a/syft/sync/connections/connection_router.py b/syft/sync/connections/connection_router.py index 01c3ce86315..e45fc214476 100644 --- a/syft/sync/connections/connection_router.py +++ b/syft/sync/connections/connection_router.py @@ -1,5 +1,4 @@ import logging -from datetime import datetime from typing import TYPE_CHECKING, Dict, List, Optional from pydantic import BaseModel, PrivateAttr @@ -431,13 +430,6 @@ def read_peer_version_file(self, peer_email: str) -> Optional["VersionInfo"]: connection = self.connection_for_datasite_watcher() return connection.read_peer_version_file(peer_email) - def read_peer_version_file_modified_time( - self, peer_email: str - ) -> Optional[datetime]: - """Drive's last-modified time of a peer's version file, or None.""" - connection = self.connection_for_datasite_watcher() - return connection.read_peer_version_file_modified_time(peer_email) - def share_version_file_with_peer(self, peer_email: str) -> None: """Share version file with a peer so they can read it.""" connection = self.connection_for_own_syftbox() diff --git a/syft/sync/connections/drive/gdrive_transport.py b/syft/sync/connections/drive/gdrive_transport.py index 5c95c524e24..30b22fd878a 100644 --- a/syft/sync/connections/drive/gdrive_transport.py +++ b/syft/sync/connections/drive/gdrive_transport.py @@ -5,7 +5,6 @@ import logging import pickle import re -from datetime import datetime from functools import lru_cache from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, List, Optional @@ -322,11 +321,6 @@ def _ordered(entries: list[_VersionedFolder]) -> list[tuple[str, str]]: return _ordered(compatible), _ordered(older), _ordered(newer) -def _peer_version_file_query(peer_email: str) -> str: - """Drive query for the version file that ``peer_email`` owns.""" - return f"name='{SYFT_VERSION_FILE}' and trashed=false and '{peer_email}' in owners" - - class GDriveConnection(SyftboxPlatformConnection): """Google Drive Files API transport layer""" @@ -1898,7 +1892,9 @@ def write_version_file(self, version_info: "VersionInfo") -> None: def _get_peer_version_file_id(self, peer_email: str) -> Optional[str]: """Find SYFT_version.json file in a peer's /SyftBox folder""" # Find the peer's SyftBox folder - query = _peer_version_file_query(peer_email) + query = ( + f"name='{SYFT_VERSION_FILE}' and trashed=false and '{peer_email}' in owners" + ) results = execute_with_retries( self.drive_service.files().list(q=query, fields="files(id)") ) @@ -1943,24 +1939,6 @@ def read_peer_version_file(self, peer_email: str) -> Optional["VersionInfo"]: print(f"Warning: could not read the version file of {peer_email}: {e}") return None - def read_peer_version_file_modified_time( - self, peer_email: str - ) -> Optional[datetime]: - """Drive's last-modified time of a peer's version file, or None without one. - - The peer writes the file at each login. Drive sets this time, not the - peer's clock. - """ - query = _peer_version_file_query(peer_email) - results = execute_with_retries( - self.drive_service.files().list(q=query, fields="files(id,modifiedTime)") - ) - items = results.get("files", []) - if not items: - return None - # Python 3.10 fromisoformat() does not accept the "Z" suffix. - return datetime.fromisoformat(items[0]["modifiedTime"].replace("Z", "+00:00")) - def share_version_file_with_peer(self, peer_email: str) -> None: """Share the version file with a peer so they can read it.""" file_id = self._get_version_file_id() diff --git a/syft/sync/syftbox_manager.py b/syft/sync/syftbox_manager.py index fa7038c6375..2fe4c6f0023 100644 --- a/syft/sync/syftbox_manager.py +++ b/syft/sync/syftbox_manager.py @@ -5,7 +5,6 @@ import time from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager -from datetime import timedelta from pathlib import Path from typing import List, Optional, cast @@ -51,7 +50,6 @@ random_syftbox_folder_for_testing, ) from syft.sync.version.peer_manager import ( - DEFAULT_PEER_LOGIN_MAX_AGE, DEFAULT_PEER_POLL_INTERVAL, PeerManager, PeerManagerConfig, @@ -1016,7 +1014,6 @@ def reject_peer_request(self, email_or_peer: str | Peer): def validate_peer( self, peer_email: str, - max_age: timedelta = DEFAULT_PEER_LOGIN_MAX_AGE, timeout: float = 0, poll_interval: float = DEFAULT_PEER_POLL_INTERVAL, ) -> Peer: @@ -1024,7 +1021,7 @@ def validate_peer( Use it after peering, before the rest of a notebook runs. The rules are in ``PeerManager.validate_peer``. With ``timeout`` (seconds), wait for - the peer to approve or log in. Each poll, every ``poll_interval`` + the peer to approve the request. Each poll, every ``poll_interval`` seconds, is one Drive request; the peers load again only when that request shows a change. A peer that cannot become valid by waiting fails at once. @@ -1036,17 +1033,16 @@ def validate_peer( self.load_peers() while True: try: - return self.peer_manager.validate_peer(peer_email, max_age=max_age) + return self.peer_manager.validate_peer(peer_email) except PeerNotReadyError as e: self._wait_until_peer_may_be_valid( - peer_email, max_age, deadline, poll_interval, e + peer_email, deadline, poll_interval, e ) self.load_peers() def _wait_until_peer_may_be_valid( self, peer_email: str, - max_age: timedelta, deadline: float, poll_interval: float, error: PeerNotReadyError, @@ -1060,7 +1056,7 @@ def _wait_until_peer_may_be_valid( print(f"Waiting for {peer_email}: {error}") while (remaining := deadline - time.monotonic()) > 0: time.sleep(min(poll_interval, remaining)) - if self.peer_manager.peer_may_be_valid(peer_email, max_age=max_age): + if self.peer_manager.peer_may_be_valid(peer_email): return raise error @@ -1248,6 +1244,7 @@ def delete_syftbox( # Clear in-memory caches and filesystem cache contents self._clear_caches() + self.peer_manager.forget_states_at_start() self.reset_all_connection_caches() # Delete local syftbox folder and cache directories diff --git a/syft/sync/version/peer_manager.py b/syft/sync/version/peer_manager.py index 9246e8d1064..986d765cb4d 100644 --- a/syft/sync/version/peer_manager.py +++ b/syft/sync/version/peer_manager.py @@ -6,7 +6,6 @@ import logging import warnings from concurrent.futures import ThreadPoolExecutor -from datetime import datetime, timedelta, timezone from enum import Enum from pathlib import Path from typing import Dict, List, Optional @@ -44,21 +43,11 @@ logger = logging.getLogger(__name__) -# How recent a peer login must be for validate_peer() to accept a connection -# that was already approved at start. It covers a demo day; connections left -# over from earlier runs are usually older. -DEFAULT_PEER_LOGIN_MAX_AGE = timedelta(hours=12) - # Seconds between polls when validate_peer() waits. Each poll is one Drive # request; a peer approval takes longer than this anyway. DEFAULT_PEER_POLL_INTERVAL = 15 -def _is_within(moment: Optional[datetime], max_age: timedelta) -> bool: - """True when ``moment`` is known and not older than ``max_age``.""" - return moment is not None and moment >= datetime.now(timezone.utc) - max_age - - # Key wrapping the DID document inside a published encryption bundle file. BUNDLE_FILE_KEY = "public_encryption_bundle" @@ -189,9 +178,9 @@ class PeerManager(BaseModel): _loaded_peer_versions: Dict[str, Optional[VersionInfo]] = PrivateAttr( default_factory=dict ) - # Emails of the peers that were approved at the first load_peers(). None - # until that load. - _approved_at_start: Optional[set[str]] = PrivateAttr(default=None) + # Peer email -> state at the first load_peers(), for accepted peers and + # waiting requests. None until that load. + _states_at_start: Optional[Dict[str, PeerState]] = PrivateAttr(default=None) # ========== Peer List Properties ========== @@ -862,9 +851,11 @@ def load_peers(self, force_download: bool = False): email, PeerState.ACCEPTED.value ) - if self._approved_at_start is None: - self._approved_at_start = { - p.email for p in peers if p.state == PeerState.ACCEPTED + if self._states_at_start is None: + self._states_at_start = { + p.email: p.state + for p in peers + if p.state in (PeerState.ACCEPTED, PeerState.REQUESTED_BY_PEER) } # set_peers keeps the locally pinned bundle over whatever the Drive @@ -897,26 +888,18 @@ def _pin_missing_peer_bundles(self, peers: List[Peer]) -> None: peer.email, peer.state.value, public_encryption_bundle=adopted ) - def validate_peer( - self, peer_email: str, max_age: timedelta = DEFAULT_PEER_LOGIN_MAX_AGE - ) -> Peer: - """Return the peer ``peer_email`` if it is a live connection, else raise. + def validate_peer(self, peer_email: str) -> Peer: + """Return the peer ``peer_email`` if it is approved, else raise. - A peer is valid when it is approved, and one of these is true: - - It was not approved yet at the first ``load_peers()`` of this client. - - It logged in within ``max_age``. The Drive modified time of its - version file gives the time. - - A connection that was approved at start can be left over from an - earlier run, with an account that is not in use now. Thus it does not - show that ``peer_email`` is the correct peer. This is a setup check, - not authentication. + If the peer was already approved, or already waiting for our approval, + at the first ``load_peers()`` of this client, warn: the connection can + be left over from an earlier run, with an account that is not in use + now. This is a setup check for demos, not authentication. Raises: - PeerNotReadyError: the peer did not approve the request yet, or it - was approved at start and did not log in within ``max_age``. - PeerSetupError: the peer is unknown, rejected, or waits for this - client to approve its request. + PeerNotReadyError: the peer did not approve the request yet. + PeerSetupError: the peer is unknown or rejected, or waits for + this client to approve its request. """ peer = self.get_cached_peer(peer_email) if peer is None: @@ -938,45 +921,43 @@ def validate_peer( raise PeerSetupError( f"{peer_email} is not approved (state: {peer.state.value})." ) - if peer_email in (self._approved_at_start or set()): - self._require_recent_login(peer_email, max_age) + self._warn_if_known_at_start(peer_email) return peer - def _require_recent_login(self, peer_email: str, max_age: timedelta) -> None: - """Raise ``PeerNotReadyError`` unless ``peer_email`` logged in within ``max_age``.""" - last_login = self.connection_router.read_peer_version_file_modified_time( - peer_email - ) - if _is_within(last_login, max_age): + def _warn_if_known_at_start(self, peer_email: str) -> None: + """Warn if ``peer_email`` was connected, or waiting, at the first load.""" + state = (self._states_at_start or {}).get(peer_email) + if state is None: return - raise PeerNotReadyError( - f"{peer_email} was already connected when this client started, and " - f"did not log in within {max_age} (last login: {last_login or 'unknown'}). " - "The connection can be left over from an earlier run. Check the " - "email, and make sure that the peer is logged in." + found = ( + "was already connected" + if state == PeerState.ACCEPTED + else "sent its request" ) + warnings.warn( + f"{peer_email} {found} before this client started. If this email " + "is wrong, the connection can be left over from an earlier run." + ) + + def forget_states_at_start(self) -> None: + """Count no peer as there at start, for the rest of this session. + + Call it after this client deletes its state. Its peers then connect + again from nothing, so ``validate_peer`` does not warn about them. + """ + self._states_at_start = {} - def peer_may_be_valid( - self, peer_email: str, max_age: timedelta = DEFAULT_PEER_LOGIN_MAX_AGE - ) -> bool: + def peer_may_be_valid(self, peer_email: str) -> bool: """Tell if ``validate_peer`` can pass after the next ``load_peers()``. Use it between polls, after ``validate_peer`` raised ``PeerNotReadyError``. It makes one Drive request and does not load the peers. It is True when the peer created its folders for this - datasite (it approved our request), or when it logged in within - ``max_age``. + datasite, which it does when it approves our request. """ - peer = self.get_cached_peer(peer_email) - if peer is not None and peer.is_requested_by_me: - return any( - p.email == peer_email - for p in self.connection_router.get_peer_requests() - ) - last_login = self.connection_router.read_peer_version_file_modified_time( - peer_email + return any( + p.email == peer_email for p in self.connection_router.get_peer_requests() ) - return _is_within(last_login, max_age) def check_peer_request_exists(self, email: str) -> bool: """Check if a peer request exists for the given email.""" diff --git a/tests/unit/test_validate_peer.py b/tests/unit/test_validate_peer.py index 0db9f06e854..0fa95f6042a 100644 --- a/tests/unit/test_validate_peer.py +++ b/tests/unit/test_validate_peer.py @@ -1,6 +1,3 @@ -import inspect -from datetime import datetime, timedelta, timezone - import pytest import syft.sync.syftbox_manager as syftbox_manager_module @@ -15,36 +12,25 @@ def _pair(add_peers: bool): ) -def _set_last_login(manager: SyftboxManager, when: datetime) -> None: - """Make Drive report that ``manager`` last wrote its version file at ``when``.""" - connection = manager._connection_router.connection_for_own_syftbox() - store = connection.drive_service._backing_store - store.files[connection._get_version_file_id()].modifiedTime = ( - when.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" - ) - - def _no_sleep(_seconds): raise AssertionError("validate_peer must not wait here") +def _start_both_then_request(ds_manager, do_manager): + """Both clients load their peers, then the DS sends its request.""" + do_manager.load_peers() + ds_manager.add_peer(do_manager.email) + + def test_peer_connected_in_this_session_is_valid(): ds_manager, do_manager = _pair(add_peers=False) - ds_manager.add_peer(do_manager.email) - do_manager.load_peers() + _start_both_then_request(ds_manager, do_manager) do_manager.approve_peer_request(ds_manager.email) assert ds_manager.validate_peer(do_manager.email).email == do_manager.email assert do_manager.validate_peer(ds_manager.email).email == ds_manager.email -def test_unknown_peer_is_invalid(): - ds_manager, _ = _pair(add_peers=True) - - with pytest.raises(PeerSetupError, match="not a peer"): - ds_manager.validate_peer("wrong@test.com") - - def test_unapproved_peer_is_invalid(): ds_manager, do_manager = _pair(add_peers=False) ds_manager.add_peer(do_manager.email) @@ -53,14 +39,6 @@ def test_unapproved_peer_is_invalid(): ds_manager.validate_peer(do_manager.email) -def test_peer_request_not_approved_by_me_is_invalid(): - ds_manager, do_manager = _pair(add_peers=False) - ds_manager.add_peer(do_manager.email) - - with pytest.raises(PeerSetupError, match="approve_peer_request"): - do_manager.validate_peer(ds_manager.email) - - def test_peer_approved_without_request_is_invalid_until_peer_connects(): # approve_peer_request(..., peer_must_exist=False) with a wrong email only # sends a request; the peer is not connected. @@ -74,41 +52,28 @@ def test_peer_approved_without_request_is_invalid_until_peer_connects(): assert do_manager.validate_peer(ds_manager.email).is_approved -def test_leftover_connection_with_stale_peer_is_invalid(): +def test_leftover_connection_warns_and_passes(monkeypatch): ds_manager, do_manager = _pair(add_peers=True) + monkeypatch.setattr(syftbox_manager_module.time, "sleep", _no_sleep) # The DS loaded peers first after the DO approved: the connection was # already there when the DS client started. - _set_last_login(do_manager, datetime.now(timezone.utc) - timedelta(days=2)) - - with pytest.raises(PeerNotReadyError, match="already connected"): - ds_manager.validate_peer(do_manager.email) - -def test_leftover_connection_with_same_day_peer_login_is_valid(): - ds_manager, do_manager = _pair(add_peers=True) - _set_last_login(do_manager, datetime.now(timezone.utc) - timedelta(hours=2)) - - assert ds_manager.validate_peer(do_manager.email).is_approved - - -def test_login_time_comes_from_drive_not_from_the_peer_clock(): - ds_manager, do_manager = _pair(add_peers=True) - # The peer's clock is two days behind; Drive stamps the upload with its own. - do_manager.peer_manager.get_own_version().updated_at = datetime.now( - timezone.utc - ) - timedelta(days=2) - do_manager.peer_manager.write_own_version() - - assert ds_manager.validate_peer(do_manager.email).is_approved + with pytest.warns(UserWarning, match="already connected before this client"): + peer = ds_manager.validate_peer(do_manager.email, timeout=60) + assert peer.is_approved -def test_max_age_sets_how_recent_the_peer_login_must_be(): - ds_manager, do_manager = _pair(add_peers=True) - _set_last_login(do_manager, datetime.now(timezone.utc) - timedelta(hours=2)) +def test_request_waiting_at_start_warns_once_approved(monkeypatch): + # The DS request was already there when the DO client started. + ds_manager, do_manager = _pair(add_peers=False) + ds_manager.add_peer(do_manager.email) + monkeypatch.setattr(syftbox_manager_module.time, "sleep", _no_sleep) - assert ds_manager.validate_peer(do_manager.email, max_age=timedelta(hours=3)) - with pytest.raises(PeerNotReadyError, match="already connected"): - ds_manager.validate_peer(do_manager.email, max_age=timedelta(hours=1)) + with pytest.raises(PeerSetupError, match="approve_peer_request"): + do_manager.validate_peer(ds_manager.email, timeout=60) + do_manager.approve_peer_request(ds_manager.email) + with pytest.warns(UserWarning, match="sent its request before this client"): + assert do_manager.validate_peer(ds_manager.email).is_approved def test_timeout_waits_for_a_late_approval(monkeypatch): @@ -128,17 +93,9 @@ def approve_during_wait(seconds): assert waits == [1] -def test_timeout_expires_when_the_peer_never_approves(): - ds_manager, do_manager = _pair(add_peers=False) - ds_manager.add_peer(do_manager.email) - - with pytest.raises(PeerNotReadyError, match="not approved"): - ds_manager.validate_peer(do_manager.email, timeout=0.05, poll_interval=0.01) - - def test_timeout_does_not_wait_when_waiting_cannot_help(monkeypatch): ds_manager, do_manager = _pair(add_peers=False) - ds_manager.add_peer(do_manager.email) + _start_both_then_request(ds_manager, do_manager) monkeypatch.setattr(syftbox_manager_module.time, "sleep", _no_sleep) with pytest.raises(PeerSetupError, match="not a peer"): @@ -213,33 +170,6 @@ def record_poll(seconds): assert loads[0] == 1 -def test_wait_for_login_reloads_once_the_peer_logs_in(monkeypatch): - ds_manager, do_manager = _pair(add_peers=True) - _set_last_login(do_manager, datetime.now(timezone.utc) - timedelta(days=2)) - loads = _count_load_peers(monkeypatch) - polls = [] - - def log_in_on_second_poll(_seconds): - polls.append(_seconds) - if len(polls) == 2: - do_manager.peer_manager.write_own_version() - - monkeypatch.setattr(syftbox_manager_module.time, "sleep", log_in_on_second_poll) - - assert ds_manager.validate_peer(do_manager.email, timeout=60, poll_interval=1) - assert len(polls) == 2 - assert loads[0] == 2 - - -def test_default_poll_interval_is_at_least_15s(): - default = ( - inspect.signature(SyftboxManager.validate_peer) - .parameters["poll_interval"] - .default - ) - assert default >= 15 - - def test_timeout_shorter_than_poll_interval_still_waits(monkeypatch): ds_manager, do_manager = _pair(add_peers=False) ds_manager.add_peer(do_manager.email) @@ -258,3 +188,17 @@ def approve_during_wait(seconds): peer = ds_manager.validate_peer(do_manager.email, timeout=10, poll_interval=15) assert peer.is_approved assert sleeps == [10] + + +def test_reset_then_re_peer_in_the_same_session_is_valid(recwarn): + ds_manager, do_manager = _pair(add_peers=True) # connected at start + for manager in (ds_manager, do_manager): + manager.delete_syftbox(verbose=False, broadcast_delete_events=False) + manager.peer_manager.write_own_version() + + ds_manager.add_peer(do_manager.email) + do_manager.approve_peer_request(ds_manager.email, peer_must_exist=False) + + assert ds_manager.validate_peer(do_manager.email).is_approved + assert do_manager.validate_peer(ds_manager.email).is_approved + assert not [w for w in recwarn if "before this client started" in str(w.message)] From 4ab778ff802d0052992842ab15338d4515e9ec2c Mon Sep 17 00:00:00 2001 From: Pedro Werneck Date: Thu, 1 Oct 2026 07:09:54 -0300 Subject: [PATCH 4/4] refactor: make validate_peer an immediate check Waiting for an approval belongs to wait_until_peered, so validate_peer() loses its timeout, the poll loop and peer_may_be_valid(). The notebooks call it without a timeout. --- .../1. DO-benchmark-owner-dbe.ipynb | 6 +- .../2. DO-model-owner-dbe.ipynb | 6 +- .../1. DO-model-owner-gemma-restrict.ipynb | 6 +- ...2. DO-benchmark-owner-gemma-restrict.ipynb | 6 +- .../3. DS-researcher-gemma-restrict.ipynb | 6 +- .../1. DO-model-owner-gemma-shield.ipynb | 6 +- .../2. DO-benchmark-owner-gemma-shield.ipynb | 6 +- .../1. DO-model-owner-gemma-restrict.ipynb | 6 +- ...2. DO-benchmark-owner-gemma-restrict.ipynb | 6 +- .../nanolm/2. enclave_eval_localenc.ipynb | 14 +- .../enclave/nanolm/3. enclave_eval_e2e.ipynb | 14 +- .../nbsplit/1. DO-model-owner-nanolm.ipynb | 6 +- .../2. DO-benchmark-owner-nanolm.ipynb | 6 +- .../nbsplit/3. DS-researcher-nanolm.ipynb | 6 +- .../syft-enclave/src/syft_enclaves/client.py | 13 +- syft/sync/syftbox_manager.py | 50 +------ syft/sync/version/peer_manager.py | 16 --- tests/unit/test_validate_peer.py | 130 +----------------- 18 files changed, 61 insertions(+), 248 deletions(-) diff --git a/notebooks/enclave/double-blind-eval-w-receipts/1. DO-benchmark-owner-dbe.ipynb b/notebooks/enclave/double-blind-eval-w-receipts/1. DO-benchmark-owner-dbe.ipynb index 1cf121a0df1..52e2b6b208c 100644 --- a/notebooks/enclave/double-blind-eval-w-receipts/1. DO-benchmark-owner-dbe.ipynb +++ b/notebooks/enclave/double-blind-eval-w-receipts/1. DO-benchmark-owner-dbe.ipynb @@ -168,10 +168,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", - "# Warn if the peer was already there before this notebook logged in.\n", + "# Fail on a wrong email, or if the peer has not approved yet. Warn if the peer\n", + "# was already there before this notebook logged in.\n", "# attest_peer() below checks the enclave.\n", - "benchmark_owner.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", + "benchmark_owner.validate_peer(MODEL_OWNER_EMAIL)\n", "print(f\" Connected to {MODEL_OWNER_EMAIL}\")" ] }, diff --git a/notebooks/enclave/double-blind-eval-w-receipts/2. DO-model-owner-dbe.ipynb b/notebooks/enclave/double-blind-eval-w-receipts/2. DO-model-owner-dbe.ipynb index ebb59963e94..a13306cf8b1 100644 --- a/notebooks/enclave/double-blind-eval-w-receipts/2. DO-model-owner-dbe.ipynb +++ b/notebooks/enclave/double-blind-eval-w-receipts/2. DO-model-owner-dbe.ipynb @@ -176,10 +176,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", - "# Warn if the peer was already there before this notebook logged in.\n", + "# Fail on a wrong email, or if the peer has not approved yet. Warn if the peer\n", + "# was already there before this notebook logged in.\n", "# attest_peer() below checks the enclave.\n", - "model_owner.validate_peer(BENCHMARK_OWNER_EMAIL, timeout=300)\n", + "model_owner.validate_peer(BENCHMARK_OWNER_EMAIL)\n", "print(f\" Connected to {BENCHMARK_OWNER_EMAIL}\")" ] }, diff --git a/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb index c100dab22e8..c0363f3b148 100644 --- a/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab-3-persona/1. DO-model-owner-gemma-restrict.ipynb @@ -198,10 +198,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", - "# Warn if the peer was already there before this notebook logged in.\n", + "# Fail on a wrong email, or if the peer has not approved yet. Warn if the peer\n", + "# was already there before this notebook logged in.\n", "# attest_peer() below checks the enclave.\n", - "model_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", + "model_owner.validate_peer(RESEARCHER_EMAIL)\n", "print(f\" Connected to {RESEARCHER_EMAIL}\")" ] }, diff --git a/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb index 5142d00eaf2..94d51734ef7 100644 --- a/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab-3-persona/2. DO-benchmark-owner-gemma-restrict.ipynb @@ -171,10 +171,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", - "# Warn if the peer was already there before this notebook logged in.\n", + "# Fail on a wrong email, or if the peer has not approved yet. Warn if the peer\n", + "# was already there before this notebook logged in.\n", "# attest_peer() below checks the enclave.\n", - "benchmark_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", + "benchmark_owner.validate_peer(RESEARCHER_EMAIL)\n", "print(f\" Connected to {RESEARCHER_EMAIL}\")" ] }, diff --git a/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb index fd789d864b6..30505d2a4ab 100644 --- a/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab-3-persona/3. DS-researcher-gemma-restrict.ipynb @@ -158,11 +158,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", - "# Warn if the peer was already there before this notebook logged in.\n", + "# Fail on a wrong email, or if the peer has not approved yet. Warn if the peer\n", + "# was already there before this notebook logged in.\n", "# attest_peer() below checks the enclave.\n", "for peer_email in [MODEL_OWNER_EMAIL, BENCHMARK_OWNER_EMAIL]:\n", - " researcher.validate_peer(peer_email, timeout=300)\n", + " researcher.validate_peer(peer_email)\n", " print(f\" Connected to {peer_email}\")" ] }, diff --git a/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb b/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb index d4707d8b37a..d1a165f43e9 100644 --- a/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb +++ b/notebooks/enclave/gemma/colab-shield/1. DO-model-owner-gemma-shield.ipynb @@ -249,10 +249,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", - "# Warn if the peer was already there before this notebook logged in.\n", + "# Fail on a wrong email, or if the peer has not approved yet. Warn if the peer\n", + "# was already there before this notebook logged in.\n", "# attest_peer() below checks the enclave.\n", - "model_owner.validate_peer(BENCHMARK_OWNER_EMAIL, timeout=300)\n", + "model_owner.validate_peer(BENCHMARK_OWNER_EMAIL)\n", "print(f\" Connected to {BENCHMARK_OWNER_EMAIL}\")" ] }, diff --git a/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb b/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb index da66d8a57c3..5072d9ff69b 100644 --- a/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb +++ b/notebooks/enclave/gemma/colab-shield/2. DO-benchmark-owner-gemma-shield.ipynb @@ -191,10 +191,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", - "# Warn if the peer was already there before this notebook logged in.\n", + "# Fail on a wrong email, or if the peer has not approved yet. Warn if the peer\n", + "# was already there before this notebook logged in.\n", "# attest_peer() below checks the enclave.\n", - "benchmark_owner.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", + "benchmark_owner.validate_peer(MODEL_OWNER_EMAIL)\n", "print(f\" Connected to {MODEL_OWNER_EMAIL}\")" ] }, diff --git a/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb index 468f8ca7350..60d9d236dcc 100644 --- a/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab/1. DO-model-owner-gemma-restrict.ipynb @@ -201,10 +201,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", - "# Warn if the peer was already there before this notebook logged in.\n", + "# Fail on a wrong email, or if the peer has not approved yet. Warn if the peer\n", + "# was already there before this notebook logged in.\n", "# attest_peer() below checks the enclave.\n", - "model_owner.validate_peer(BENCHMARK_OWNER_EMAIL, timeout=300)\n", + "model_owner.validate_peer(BENCHMARK_OWNER_EMAIL)\n", "print(f\" Connected to {BENCHMARK_OWNER_EMAIL}\")" ] }, diff --git a/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb b/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb index 1f7f49cee3b..b100e647deb 100644 --- a/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb +++ b/notebooks/enclave/gemma/colab/2. DO-benchmark-owner-gemma-restrict.ipynb @@ -180,10 +180,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", - "# Warn if the peer was already there before this notebook logged in.\n", + "# Fail on a wrong email, or if the peer has not approved yet. Warn if the peer\n", + "# was already there before this notebook logged in.\n", "# attest_peer() below checks the enclave.\n", - "benchmark_owner.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", + "benchmark_owner.validate_peer(MODEL_OWNER_EMAIL)\n", "print(f\" Connected to {MODEL_OWNER_EMAIL}\")" ] }, diff --git a/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb b/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb index 2eb57b9bab1..ed211eca3ab 100644 --- a/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb +++ b/notebooks/enclave/nanolm/2. enclave_eval_localenc.ipynb @@ -352,15 +352,13 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", - "# Warn if the peer was already there before this notebook logged in.\n", + "# Fail on a wrong email, or if the peer has not approved yet. Warn if the peer\n", + "# was already there before this notebook logged in.\n", "# Nothing in this notebook checks the enclave address.\n", - "# Each call waits at most 300 s, so the worst case for this cell is\n", - "# 20 minutes (4 x 300 s).\n", - "researcher.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", - "researcher.validate_peer(BENCHMARK_OWNER_EMAIL, timeout=300)\n", - "model_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", - "benchmark_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", + "researcher.validate_peer(MODEL_OWNER_EMAIL)\n", + "researcher.validate_peer(BENCHMARK_OWNER_EMAIL)\n", + "model_owner.validate_peer(RESEARCHER_EMAIL)\n", + "benchmark_owner.validate_peer(RESEARCHER_EMAIL)\n", "print(\" Peers validated\")" ] }, diff --git a/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb b/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb index 7c69038eb2f..2c3e521fc53 100644 --- a/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb +++ b/notebooks/enclave/nanolm/3. enclave_eval_e2e.ipynb @@ -224,15 +224,13 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", - "# Warn if the peer was already there before this notebook logged in.\n", + "# Fail on a wrong email, or if the peer has not approved yet. Warn if the peer\n", + "# was already there before this notebook logged in.\n", "# Nothing in this notebook checks the enclave address.\n", - "# Each call waits at most 300 s, so the worst case for this cell is\n", - "# 20 minutes (4 x 300 s).\n", - "researcher.validate_peer(MODEL_OWNER_EMAIL, timeout=300)\n", - "researcher.validate_peer(BENCHMARK_OWNER_EMAIL, timeout=300)\n", - "model_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", - "benchmark_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", + "researcher.validate_peer(MODEL_OWNER_EMAIL)\n", + "researcher.validate_peer(BENCHMARK_OWNER_EMAIL)\n", + "model_owner.validate_peer(RESEARCHER_EMAIL)\n", + "benchmark_owner.validate_peer(RESEARCHER_EMAIL)\n", "print(\" Peers validated\")" ] }, diff --git a/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb b/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb index 7f1d47098bc..d3042fc417e 100644 --- a/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb +++ b/notebooks/enclave/nanolm/nbsplit/1. DO-model-owner-nanolm.ipynb @@ -166,10 +166,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", - "# Warn if the peer was already there before this notebook logged in.\n", + "# Fail on a wrong email, or if the peer has not approved yet. Warn if the peer\n", + "# was already there before this notebook logged in.\n", "# attest_peer() below checks the enclave.\n", - "model_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", + "model_owner.validate_peer(RESEARCHER_EMAIL)\n", "print(f\" Connected to {RESEARCHER_EMAIL}\")" ] }, diff --git a/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb b/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb index 542cc5cc84b..490d9841f6a 100644 --- a/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb +++ b/notebooks/enclave/nanolm/nbsplit/2. DO-benchmark-owner-nanolm.ipynb @@ -166,10 +166,10 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", - "# Warn if the peer was already there before this notebook logged in.\n", + "# Fail on a wrong email, or if the peer has not approved yet. Warn if the peer\n", + "# was already there before this notebook logged in.\n", "# attest_peer() below checks the enclave.\n", - "benchmark_owner.validate_peer(RESEARCHER_EMAIL, timeout=300)\n", + "benchmark_owner.validate_peer(RESEARCHER_EMAIL)\n", "print(f\" Connected to {RESEARCHER_EMAIL}\")" ] }, diff --git a/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb b/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb index 856d5f4035b..3314f5eec98 100644 --- a/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb +++ b/notebooks/enclave/nanolm/nbsplit/3. DS-researcher-nanolm.ipynb @@ -158,11 +158,11 @@ "metadata": {}, "outputs": [], "source": [ - "# Wait up to 5 minutes for each peer to approve. Fail at once on a wrong email.\n", - "# Warn if the peer was already there before this notebook logged in.\n", + "# Fail on a wrong email, or if the peer has not approved yet. Warn if the peer\n", + "# was already there before this notebook logged in.\n", "# attest_peer() below checks the enclave.\n", "for peer_email in [MODEL_OWNER_EMAIL, BENCHMARK_OWNER_EMAIL]:\n", - " researcher.validate_peer(peer_email, timeout=300)\n", + " researcher.validate_peer(peer_email)\n", " print(f\" Connected to {peer_email}\")" ] }, diff --git a/packages/syft-enclave/src/syft_enclaves/client.py b/packages/syft-enclave/src/syft_enclaves/client.py index 097f7923479..5a7b6b39e6c 100644 --- a/packages/syft-enclave/src/syft_enclaves/client.py +++ b/packages/syft-enclave/src/syft_enclaves/client.py @@ -15,7 +15,7 @@ from syft.sync.peers.peer import Peer from syft.sync.peers.peer_list import PeerList -from syft.sync.version.peer_manager import DEFAULT_PEER_POLL_INTERVAL, CompatAction +from syft.sync.version.peer_manager import CompatAction from syft_enclaves.attestation.dispatch import policy_for, verify_evidence from syft_enclaves.attestation.envelope import AttestationEvidence from syft_enclaves.enclave_job_client import EnclaveJobClient @@ -110,15 +110,8 @@ def approve_peer_request( def reject_peer_request(self, email_or_peer: str | Peer): self._rds.reject_peer_request(email_or_peer) - def validate_peer( - self, - peer_email: str, - timeout: float = 0, - poll_interval: float = DEFAULT_PEER_POLL_INTERVAL, - ) -> Peer: - return self._rds.validate_peer( - peer_email, timeout=timeout, poll_interval=poll_interval - ) + def validate_peer(self, peer_email: str) -> Peer: + return self._rds.validate_peer(peer_email) def attest_peer( self, diff --git a/syft/sync/syftbox_manager.py b/syft/sync/syftbox_manager.py index 2fe4c6f0023..61ad24d8ced 100644 --- a/syft/sync/syftbox_manager.py +++ b/syft/sync/syftbox_manager.py @@ -24,7 +24,7 @@ ) from syft.sync.file_writer import FileWriter from syft.sync.job_file_change_handler import JobFileChangeHandler -from syft.sync.peers.peer import Peer, PeerNotReadyError +from syft.sync.peers.peer import Peer from syft.sync.peers.peer_list import PeerList from syft.sync.peers.peer_store import PeerStore from syft.sync.platforms.base_platform import BasePlatform @@ -50,7 +50,6 @@ random_syftbox_folder_for_testing, ) from syft.sync.version.peer_manager import ( - DEFAULT_PEER_POLL_INTERVAL, PeerManager, PeerManagerConfig, ) @@ -1011,54 +1010,17 @@ def reject_peer_request(self, email_or_peer: str | Peer): """Reject a pending peer request. Delegates to PeerManager.""" self.peer_manager.reject_peer_request(email_or_peer) - def validate_peer( - self, - peer_email: str, - timeout: float = 0, - poll_interval: float = DEFAULT_PEER_POLL_INTERVAL, - ) -> Peer: - """Load the peers, then return ``peer_email`` if it is a live connection. + def validate_peer(self, peer_email: str) -> Peer: + """Load the peers, then return ``peer_email`` if it is approved. Use it after peering, before the rest of a notebook runs. The rules are - in ``PeerManager.validate_peer``. With ``timeout`` (seconds), wait for - the peer to approve the request. Each poll, every ``poll_interval`` - seconds, is one Drive request; the peers load again only when that - request shows a change. A peer that cannot become valid by waiting - fails at once. + in ``PeerManager.validate_peer``. Raises: - PeerSetupError: ``peer_email`` is not a live connection. + PeerSetupError: ``peer_email`` is not approved. """ - deadline = time.monotonic() + timeout self.load_peers() - while True: - try: - return self.peer_manager.validate_peer(peer_email) - except PeerNotReadyError as e: - self._wait_until_peer_may_be_valid( - peer_email, deadline, poll_interval, e - ) - self.load_peers() - - def _wait_until_peer_may_be_valid( - self, - peer_email: str, - deadline: float, - poll_interval: float, - error: PeerNotReadyError, - ) -> None: - """Poll until ``peer_may_be_valid`` is True; raise ``error`` at ``deadline``. - - The last sleep is cut to end at ``deadline``. - """ - if deadline - time.monotonic() <= 0: - raise error - print(f"Waiting for {peer_email}: {error}") - while (remaining := deadline - time.monotonic()) > 0: - time.sleep(min(poll_interval, remaining)) - if self.peer_manager.peer_may_be_valid(peer_email): - return - raise error + return self.peer_manager.validate_peer(peer_email) # ========== Encryption key fingerprints ========== diff --git a/syft/sync/version/peer_manager.py b/syft/sync/version/peer_manager.py index 986d765cb4d..9df67c11fa3 100644 --- a/syft/sync/version/peer_manager.py +++ b/syft/sync/version/peer_manager.py @@ -43,10 +43,6 @@ logger = logging.getLogger(__name__) -# Seconds between polls when validate_peer() waits. Each poll is one Drive -# request; a peer approval takes longer than this anyway. -DEFAULT_PEER_POLL_INTERVAL = 15 - # Key wrapping the DID document inside a published encryption bundle file. BUNDLE_FILE_KEY = "public_encryption_bundle" @@ -947,18 +943,6 @@ def forget_states_at_start(self) -> None: """ self._states_at_start = {} - def peer_may_be_valid(self, peer_email: str) -> bool: - """Tell if ``validate_peer`` can pass after the next ``load_peers()``. - - Use it between polls, after ``validate_peer`` raised - ``PeerNotReadyError``. It makes one Drive request and does not load - the peers. It is True when the peer created its folders for this - datasite, which it does when it approves our request. - """ - return any( - p.email == peer_email for p in self.connection_router.get_peer_requests() - ) - def check_peer_request_exists(self, email: str) -> bool: """Check if a peer request exists for the given email.""" return any(p.email == email for p in self.requested_by_peer_peers) diff --git a/tests/unit/test_validate_peer.py b/tests/unit/test_validate_peer.py index 0fa95f6042a..1610cdababc 100644 --- a/tests/unit/test_validate_peer.py +++ b/tests/unit/test_validate_peer.py @@ -1,7 +1,5 @@ import pytest -import syft.sync.syftbox_manager as syftbox_manager_module -from syft.sync.connections.drive import mock_drive_service from syft.sync.peers.peer import PeerNotReadyError, PeerSetupError from syft.sync.syftbox_manager import SyftboxManager @@ -12,10 +10,6 @@ def _pair(add_peers: bool): ) -def _no_sleep(_seconds): - raise AssertionError("validate_peer must not wait here") - - def _start_both_then_request(ds_manager, do_manager): """Both clients load their peers, then the DS sends its request.""" do_manager.load_peers() @@ -52,144 +46,28 @@ def test_peer_approved_without_request_is_invalid_until_peer_connects(): assert do_manager.validate_peer(ds_manager.email).is_approved -def test_leftover_connection_warns_and_passes(monkeypatch): +def test_leftover_connection_warns_and_passes(): ds_manager, do_manager = _pair(add_peers=True) - monkeypatch.setattr(syftbox_manager_module.time, "sleep", _no_sleep) # The DS loaded peers first after the DO approved: the connection was # already there when the DS client started. with pytest.warns(UserWarning, match="already connected before this client"): - peer = ds_manager.validate_peer(do_manager.email, timeout=60) + peer = ds_manager.validate_peer(do_manager.email) assert peer.is_approved -def test_request_waiting_at_start_warns_once_approved(monkeypatch): +def test_request_waiting_at_start_warns_once_approved(): # The DS request was already there when the DO client started. ds_manager, do_manager = _pair(add_peers=False) ds_manager.add_peer(do_manager.email) - monkeypatch.setattr(syftbox_manager_module.time, "sleep", _no_sleep) with pytest.raises(PeerSetupError, match="approve_peer_request"): - do_manager.validate_peer(ds_manager.email, timeout=60) + do_manager.validate_peer(ds_manager.email) do_manager.approve_peer_request(ds_manager.email) with pytest.warns(UserWarning, match="sent its request before this client"): assert do_manager.validate_peer(ds_manager.email).is_approved -def test_timeout_waits_for_a_late_approval(monkeypatch): - ds_manager, do_manager = _pair(add_peers=False) - ds_manager.add_peer(do_manager.email) - do_manager.load_peers() - waits = [] - - def approve_during_wait(seconds): - waits.append(seconds) - do_manager.approve_peer_request(ds_manager.email) - - monkeypatch.setattr(syftbox_manager_module.time, "sleep", approve_during_wait) - - peer = ds_manager.validate_peer(do_manager.email, timeout=60, poll_interval=1) - assert peer.is_approved - assert waits == [1] - - -def test_timeout_does_not_wait_when_waiting_cannot_help(monkeypatch): - ds_manager, do_manager = _pair(add_peers=False) - _start_both_then_request(ds_manager, do_manager) - monkeypatch.setattr(syftbox_manager_module.time, "sleep", _no_sleep) - - with pytest.raises(PeerSetupError, match="not a peer"): - ds_manager.validate_peer("wrong@test.com", timeout=60) - # A request that only this client can approve. - with pytest.raises(PeerSetupError, match="approve_peer_request"): - do_manager.validate_peer(ds_manager.email, timeout=60) - - -def _count_drive_requests(monkeypatch) -> list[int]: - """Count mock Drive requests; the returned one-item list holds the count.""" - count = [0] - for request_class in ( - mock_drive_service.MockListRequest, - mock_drive_service.MockGetRequest, - ): - original = request_class.execute - - def counted(self, *args, _original=original, **kwargs): - count[0] += 1 - return _original(self, *args, **kwargs) - - monkeypatch.setattr(request_class, "execute", counted) - # Downloads go through get_media(), not execute(). - original_get_media = mock_drive_service.MockFilesResource.get_media - - def counted_get_media(self, *args, **kwargs): - count[0] += 1 - return original_get_media(self, *args, **kwargs) - - monkeypatch.setattr( - mock_drive_service.MockFilesResource, "get_media", counted_get_media - ) - return count - - -def _count_load_peers(monkeypatch) -> list[int]: - count = [0] - original = SyftboxManager.load_peers - - def counted(self, *args, **kwargs): - count[0] += 1 - return original(self, *args, **kwargs) - - monkeypatch.setattr(SyftboxManager, "load_peers", counted) - return count - - -def test_wait_for_approval_polls_with_one_drive_request(monkeypatch): - ds_manager, do_manager = _pair(add_peers=False) - ds_manager.add_peer(do_manager.email) - drive_requests = _count_drive_requests(monkeypatch) - loads = _count_load_peers(monkeypatch) - clock = [0.0] - per_poll = [] - - def record_poll(seconds): - clock[0] += seconds - per_poll.append(drive_requests[0]) - drive_requests[0] = 0 - - monkeypatch.setattr(syftbox_manager_module.time, "sleep", record_poll) - monkeypatch.setattr(syftbox_manager_module.time, "monotonic", lambda: clock[0]) - - with pytest.raises(PeerNotReadyError, match="not approved"): - ds_manager.validate_peer(do_manager.email, timeout=4, poll_interval=1) - # Four polls in four seconds. Before the first sleep: the initial full - # load. After it: one Drive request per poll, and no other load. - assert len(per_poll) == 4 - assert per_poll[1:] == [1, 1, 1] - assert drive_requests[0] == 1 - assert loads[0] == 1 - - -def test_timeout_shorter_than_poll_interval_still_waits(monkeypatch): - ds_manager, do_manager = _pair(add_peers=False) - ds_manager.add_peer(do_manager.email) - do_manager.load_peers() - clock = [0.0] - sleeps = [] - - def approve_during_wait(seconds): - clock[0] += seconds - sleeps.append(seconds) - do_manager.approve_peer_request(ds_manager.email) - - monkeypatch.setattr(syftbox_manager_module.time, "sleep", approve_during_wait) - monkeypatch.setattr(syftbox_manager_module.time, "monotonic", lambda: clock[0]) - - peer = ds_manager.validate_peer(do_manager.email, timeout=10, poll_interval=15) - assert peer.is_approved - assert sleeps == [10] - - def test_reset_then_re_peer_in_the_same_session_is_valid(recwarn): ds_manager, do_manager = _pair(add_peers=True) # connected at start for manager in (ds_manager, do_manager):