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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/aleph/sdk/client/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
List,
Mapping,
Optional,
Sequence,
Tuple,
Type,
Union,
Expand All @@ -29,11 +30,14 @@
parse_message,
)
from aleph_message.models.execution.environment import (
DEFAULT_SNP_POLICY,
HostRequirements,
HypervisorType,
LaunchMeasurement,
TrustedExecutionEnvironment,
)
from aleph_message.models.execution.program import Encoding
from aleph_message.models.execution.vprogram import VerifiedVolume, VerifiedWorkload
from aleph_message.status import MessageStatus
from typing_extensions import deprecated

Expand Down Expand Up @@ -534,6 +538,57 @@ async def create_program(
"Did you mean to import `AuthenticatedAlephHttpClient`?"
)

@abstractmethod
async def create_verifiable_program(
self,
runtime: str,
workload: Union[VerifiedWorkload, Mapping[str, Any]],
measurements: Sequence[Union[LaunchMeasurement, Mapping[str, Any]]],
policy: int = DEFAULT_SNP_POLICY,
runtime_comment: str = "",
metadata: Optional[dict[str, Any]] = None,
address: Optional[str] = None,
payment: Optional[Payment] = None,
vcpus: Optional[int] = None,
memory: Optional[int] = None,
timeout_seconds: Optional[float] = None,
internet: bool = True,
volumes: Optional[Sequence[Union[VerifiedVolume, Mapping[str, Any]]]] = None,
requirements: Optional[HostRequirements] = None,
sync: bool = False,
channel: Optional[str] = settings.DEFAULT_CHANNEL,
storage_engine: StorageEnum = StorageEnum.storage,
) -> Tuple[AlephMessage, MessageStatus]:
"""
Post a (create) V-PROGRAM message: an auto-booting SEV-SNP confidential
VM whose full software stack is attestable.

V-Programs are credit-only and immutable: no amendments, no
environment variables and no authorized keys. Every input reaching the
guest is either measured or dm-verity bound.

:param runtime: Item hash of the runtime manifest STORE message
:param workload: The verity-bound workload volume (ref, hash_tree, roothash)
:param measurements: Expected launch measurements, one per vcpu_type
:param policy: SEV-SNP guest policy (Default: DEFAULT_SNP_POLICY)
:param runtime_comment: Free-form comment on the runtime
:param metadata: Metadata to attach to the message
:param address: Address to use (Default: account.get_address())
:param payment: Payment method, must be credit (Default: credit on ETH)
:param vcpus: Number of vCPUs to allocate
:param memory: Memory to allocate, in MiB
:param timeout_seconds: Timeout in seconds
:param internet: Whether the VM has internet access
:param volumes: Extra verity-bound read-only volumes (at most 8)
:param requirements: Host requirements (e.g. a specific CRN)
:param sync: If true, waits for the message to be processed by the API server
:param channel: Channel to use (Default: "TEST")
:param storage_engine: Storage engine to use (Default: "storage")
"""
raise NotImplementedError(
"Did you mean to import `AuthenticatedAlephHttpClient`?"
)

@abstractmethod
async def create_instance(
self,
Expand Down
93 changes: 70 additions & 23 deletions src/aleph/sdk/client/authenticated_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import time
from io import BytesIO
from pathlib import Path
from typing import Any, Dict, Mapping, NoReturn, Optional, Tuple, Union
from typing import Any, Dict, Mapping, NoReturn, Optional, Sequence, Tuple, Union

import aiohttp
import aleph_cid
Expand All @@ -27,19 +27,28 @@
ProgramMessage,
StoreContent,
StoreMessage,
VerifiableProgramMessage,
)
from aleph_message.models.execution.base import Encoding, Payment, PaymentType
from aleph_message.models.execution.environment import (
DEFAULT_SNP_POLICY,
HostRequirements,
HypervisorType,
LaunchMeasurement,
TrustedExecutionEnvironment,
)
from aleph_message.models.execution.vprogram import VerifiedVolume, VerifiedWorkload
from aleph_message.status import MessageStatus

from ..conf import settings
from ..exceptions import BroadcastError, InsufficientFundsError, InvalidMessageError
from ..types import Account, StorageEnum, TokenType
from ..utils import extended_json_encoder, make_instance_content, make_program_content
from ..utils import (
extended_json_encoder,
make_instance_content,
make_program_content,
make_verifiable_program_content,
)
from .abstract import AuthenticatedAlephClient
from .http import AlephHttpClient
from .services.authenticated_port_forwarder import AuthenticatedPortForwarder
Expand All @@ -51,7 +60,7 @@
import magic
except ImportError:
logger.info("Could not import library 'magic', MIME type detection disabled")
magic = None # type:ignore
magic = None # type: ignore


class AuthenticatedAlephHttpClient(AlephHttpClient, AuthenticatedAlephClient):
Expand Down Expand Up @@ -487,8 +496,11 @@ async def create_program(
if status in (MessageStatus.PROCESSED, MessageStatus.PENDING):
return message, status # type: ignore

# get the reason for rejection
rejected_message = await self.get_message_error(message.item_hash)
await self._raise_for_rejected_executable(message.item_hash)

async def _raise_for_rejected_executable(self, item_hash: str) -> NoReturn:
"""Fetch the rejection reason of an executable message and raise."""
rejected_message = await self.get_message_error(item_hash)
assert rejected_message, "No rejected message found"
error_code = rejected_message["error_code"]
if error_code == 5:
Expand All @@ -506,6 +518,58 @@ async def create_program(
else:
raise ValueError(f"Unknown error code {error_code}: {rejected_message}")

async def create_verifiable_program(
self,
runtime: str,
workload: Union[VerifiedWorkload, Mapping[str, Any]],
measurements: Sequence[Union[LaunchMeasurement, Mapping[str, Any]]],
policy: int = DEFAULT_SNP_POLICY,
runtime_comment: str = "",
metadata: Optional[dict[str, Any]] = None,
address: Optional[str] = None,
payment: Optional[Payment] = None,
vcpus: Optional[int] = None,
memory: Optional[int] = None,
timeout_seconds: Optional[float] = None,
internet: bool = True,
volumes: Optional[Sequence[Union[VerifiedVolume, Mapping[str, Any]]]] = None,
requirements: Optional[HostRequirements] = None,
sync: bool = False,
channel: Optional[str] = settings.DEFAULT_CHANNEL,
storage_engine: StorageEnum = StorageEnum.storage,
) -> Tuple[VerifiableProgramMessage, MessageStatus]:
address = address or settings.ADDRESS_TO_USE or self.account.get_address()

content = make_verifiable_program_content(
runtime=runtime,
workload=workload,
measurements=measurements,
policy=policy,
runtime_comment=runtime_comment,
metadata=metadata,
address=address,
vcpus=vcpus,
memory=memory,
timeout_seconds=timeout_seconds,
internet=internet,
volumes=volumes,
requirements=requirements,
payment=payment,
)

message, status, _ = await self.submit(
content=content.model_dump(exclude_none=True),
message_type=MessageType.v_program,
channel=channel,
storage_engine=storage_engine,
sync=sync,
raise_on_rejected=False,
)
if status in (MessageStatus.PROCESSED, MessageStatus.PENDING):
return message, status # type: ignore

await self._raise_for_rejected_executable(message.item_hash)

async def create_instance(
self,
rootfs: str,
Expand Down Expand Up @@ -563,24 +627,7 @@ async def create_instance(
if status in (MessageStatus.PROCESSED, MessageStatus.PENDING):
return message, status # type: ignore

# get the reason for rejection
rejected_message = await self.get_message_error(message.item_hash)
assert rejected_message, "No rejected message found"
error_code = rejected_message["error_code"]
if error_code == 5:
# not enough balance
details = rejected_message["details"]
errors = details["errors"]
error = errors[0]
account_balance = float(error["account_balance"])
required_balance = float(error["required_balance"])
raise InsufficientFundsError(
token_type=TokenType.ALEPH,
required_funds=required_balance,
available_funds=account_balance,
)
else:
raise ValueError(f"Unknown error code {error_code}: {rejected_message}")
await self._raise_for_rejected_executable(message.item_hash)

async def forget(
self,
Expand Down
74 changes: 73 additions & 1 deletion src/aleph/sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
Mapping,
Optional,
Protocol,
Sequence,
Tuple,
Type,
TypeVar,
Expand All @@ -38,14 +39,17 @@
MachineType,
MessageType,
ProgramContent,
VerifiableProgramContent,
)
from aleph_message.models.execution.base import Payment, PaymentType
from aleph_message.models.execution.environment import (
DEFAULT_SNP_POLICY,
FunctionEnvironment,
FunctionTriggers,
HostRequirements,
HypervisorType,
InstanceEnvironment,
LaunchMeasurement,
MachineResources,
Subscription,
TrustedExecutionEnvironment,
Expand All @@ -62,6 +66,13 @@
PersistentVolumeSizeMib,
VolumePersistence,
)
from aleph_message.models.execution.vprogram import (
TeeVerification,
VerifiableProgramEnvironment,
VerifiableProgramRuntime,
VerifiedVolume,
VerifiedWorkload,
)
from aleph_message.utils import Mebibytes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
Expand All @@ -76,7 +87,7 @@
import magic
except ImportError:
logger.info("Could not import library 'magic', MIME type detection disabled")
magic = None # type:ignore
magic = None # type: ignore


def try_open_zip(path: Path) -> None:
Expand Down Expand Up @@ -114,6 +125,67 @@ def create_archive(path: Path) -> Tuple[Path, Encoding]:
raise FileNotFoundError("No file or directory to create the archive from")


def make_verifiable_program_content(
runtime: str,
workload: Union[VerifiedWorkload, Mapping[str, Any]],
measurements: Sequence[Union[LaunchMeasurement, Mapping[str, Any]]],
policy: int = DEFAULT_SNP_POLICY,
runtime_comment: str = "",
metadata: Optional[dict[str, Any]] = None,
address: Optional[str] = None,
vcpus: Optional[int] = None,
memory: Optional[int] = None,
timeout_seconds: Optional[float] = None,
internet: bool = True,
volumes: Optional[Sequence[Union[VerifiedVolume, Mapping[str, Any]]]] = None,
requirements: Optional[HostRequirements] = None,
payment: Optional[Payment] = None,
) -> VerifiableProgramContent:
"""
Create VerifiableProgramContent object given the provided fields.

V-Programs are credit-only and immutable: the payment defaults to
credit on ETH and allow_amend is always False. Environment variables and
authorized keys are not accepted since they would reach the guest
unmeasured; ship them in the verity-bound workload instead.
"""

address = address or "0x0000000000000000000000000000000000000000"
payment = payment or Payment(chain=Chain.ETH, type=PaymentType.credit)
vcpus = vcpus or settings.DEFAULT_VM_VCPUS
memory = memory or settings.DEFAULT_VM_MEMORY
timeout_seconds = timeout_seconds or settings.DEFAULT_VM_TIMEOUT
volumes = volumes if volumes is not None else []

return VerifiableProgramContent(
address=address,
allow_amend=False,
environment=VerifiableProgramEnvironment(internet=internet),
resources=MachineResources(
vcpus=vcpus,
memory=Mebibytes(memory),
seconds=int(timeout_seconds),
),
runtime=VerifiableProgramRuntime(
ref=ItemHash(runtime), comment=runtime_comment
),
workload=VerifiedWorkload.model_validate(workload),
verification=TeeVerification(
backend="sev_snp",
policy=policy,
measurements=[
LaunchMeasurement.model_validate(measurement)
for measurement in measurements
],
),
volumes=[VerifiedVolume.model_validate(volume) for volume in volumes],
time=datetime.now().timestamp(),
metadata=metadata,
requirements=requirements,
payment=payment,
)


def get_message_type_value(message_type: Type[GenericMessage]) -> MessageType:
"""Returns the value of the 'type' field of a message type class."""
type_literal = message_type.__annotations__["type"]
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/services/test_authorizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ async def create_store(self, *args, **kwargs):
async def create_program(self, *args, **kwargs):
raise NotImplementedError

async def create_verifiable_program(self, *args, **kwargs):
raise NotImplementedError

async def create_instance(self, *args, **kwargs):
raise NotImplementedError

Expand Down
Loading
Loading