From 0c0cfeae431926299995c9524bd51063132594ee Mon Sep 17 00:00:00 2001 From: Hugo Herter Date: Mon, 29 Apr 2024 09:03:11 +0200 Subject: [PATCH] Fix: Could not generate signed messages outside HTTP client (#120) Problem: Developers could not generate signed messages without using the AlephHttpClient. Solution: Move and rename the method on the abstract class; Move the utility that generates sha256 hashes to utils.py. --- src/aleph/sdk/client/abstract.py | 69 +++++++++++++++++++++- src/aleph/sdk/client/authenticated_http.py | 54 +---------------- src/aleph/sdk/utils.py | 6 ++ 3 files changed, 75 insertions(+), 54 deletions(-) diff --git a/src/aleph/sdk/client/abstract.py b/src/aleph/sdk/client/abstract.py index 984293ce..0d0d1e4e 100644 --- a/src/aleph/sdk/client/abstract.py +++ b/src/aleph/sdk/client/abstract.py @@ -1,6 +1,7 @@ # An interface for all clients to implement. - +import json import logging +import time from abc import ABC, abstractmethod from pathlib import Path from typing import ( @@ -18,19 +19,25 @@ from aleph_message.models import ( AlephMessage, + ItemType, MessagesResponse, MessageType, Payment, PostMessage, + parse_message, ) from aleph_message.models.execution.environment import HypervisorType from aleph_message.models.execution.program import Encoding from aleph_message.status import MessageStatus +from aleph.sdk.conf import settings +from aleph.sdk.types import Account +from aleph.sdk.utils import extended_json_encoder + from ..query.filters import MessageFilter, PostFilter from ..query.responses import PostsResponse from ..types import GenericMessage, StorageEnum -from ..utils import Writable +from ..utils import Writable, compute_sha256 DEFAULT_PAGE_SIZE = 200 @@ -231,6 +238,8 @@ def watch_messages( class AuthenticatedAlephClient(AlephClient): + account: Account + @abstractmethod async def create_post( self, @@ -444,6 +453,62 @@ async def forget( "Did you mean to import `AuthenticatedAlephHttpClient`?" ) + async def generate_signed_message( + self, + message_type: MessageType, + content: Dict[str, Any], + channel: Optional[str], + allow_inlining: bool = True, + storage_engine: StorageEnum = StorageEnum.storage, + ) -> AlephMessage: + """Generate a signed aleph.im message ready to be sent to the network. + + If the content is not inlined, it will be pushed to the storage engine via the API of a Core Channel Node. + + :param message_type: Type of the message (PostMessage, ...) + :param content: User-defined content of the message + :param channel: Channel to use (Default: "TEST") + :param allow_inlining: Whether to allow inlining the content of the message (Default: True) + :param storage_engine: Storage engine to use (Default: "storage") + """ + + message_dict: Dict[str, Any] = { + "sender": self.account.get_address(), + "chain": self.account.CHAIN, + "type": message_type, + "content": content, + "time": time.time(), + "channel": channel, + } + + # Use the Pydantic encoder to serialize types like UUID, datetimes, etc. + item_content: str = json.dumps( + content, separators=(",", ":"), default=extended_json_encoder + ) + + if allow_inlining and (len(item_content) < settings.MAX_INLINE_SIZE): + message_dict["item_content"] = item_content + message_dict["item_hash"] = compute_sha256(item_content) + message_dict["item_type"] = ItemType.inline + else: + if storage_engine == StorageEnum.ipfs: + message_dict["item_hash"] = await self.ipfs_push( + content=content, + ) + message_dict["item_type"] = ItemType.ipfs + else: # storage + assert storage_engine == StorageEnum.storage + message_dict["item_hash"] = await self.storage_push( + content=content, + ) + message_dict["item_type"] = ItemType.storage + + message_dict = await self.account.sign_message(message_dict) + return parse_message(message_dict) + + # Alias for backwards compatibility + _prepare_aleph_message = generate_signed_message + @abstractmethod async def submit( self, diff --git a/src/aleph/sdk/client/authenticated_http.py b/src/aleph/sdk/client/authenticated_http.py index 315d2ea6..0d708af2 100644 --- a/src/aleph/sdk/client/authenticated_http.py +++ b/src/aleph/sdk/client/authenticated_http.py @@ -7,7 +7,6 @@ from typing import Any, Dict, List, Mapping, NoReturn, Optional, Tuple, Union import aiohttp -from aleph_message import parse_message from aleph_message.models import ( AggregateContent, AggregateMessage, @@ -17,7 +16,6 @@ ForgetMessage, InstanceContent, InstanceMessage, - ItemType, MessageType, PostContent, PostMessage, @@ -622,54 +620,6 @@ async def forget( ) return message, status - @staticmethod - def compute_sha256(s: str) -> str: - h = hashlib.sha256() - h.update(s.encode("utf-8")) - return h.hexdigest() - - async def _prepare_aleph_message( - self, - message_type: MessageType, - content: Dict[str, Any], - channel: Optional[str], - allow_inlining: bool = True, - storage_engine: StorageEnum = StorageEnum.storage, - ) -> AlephMessage: - message_dict: Dict[str, Any] = { - "sender": self.account.get_address(), - "chain": self.account.CHAIN, - "type": message_type, - "content": content, - "time": time.time(), - "channel": channel, - } - - # Use the Pydantic encoder to serialize types like UUID, datetimes, etc. - item_content: str = json.dumps( - content, separators=(",", ":"), default=extended_json_encoder - ) - - if allow_inlining and (len(item_content) < settings.MAX_INLINE_SIZE): - message_dict["item_content"] = item_content - message_dict["item_hash"] = self.compute_sha256(item_content) - message_dict["item_type"] = ItemType.inline - else: - if storage_engine == StorageEnum.ipfs: - message_dict["item_hash"] = await self.ipfs_push( - content=content, - ) - message_dict["item_type"] = ItemType.ipfs - else: # storage - assert storage_engine == StorageEnum.storage - message_dict["item_hash"] = await self.storage_push( - content=content, - ) - message_dict["item_type"] = ItemType.storage - - message_dict = await self.account.sign_message(message_dict) - return parse_message(message_dict) - async def submit( self, content: Dict[str, Any], @@ -680,7 +630,7 @@ async def submit( sync: bool = False, raise_on_rejected: bool = True, ) -> Tuple[AlephMessage, MessageStatus, Optional[Dict[str, Any]]]: - message = await self._prepare_aleph_message( + message = await self.generate_signed_message( message_type=message_type, content=content, channel=channel, @@ -703,7 +653,7 @@ async def _storage_push_file_with_message( data = aiohttp.FormData() # Prepare the STORE message - message = await self._prepare_aleph_message( + message = await self.generate_signed_message( message_type=MessageType.store, content=store_content.dict(exclude_none=True), channel=channel, diff --git a/src/aleph/sdk/utils.py b/src/aleph/sdk/utils.py index ab17f44a..b1c04cdf 100644 --- a/src/aleph/sdk/utils.py +++ b/src/aleph/sdk/utils.py @@ -1,4 +1,5 @@ import errno +import hashlib import logging import os from datetime import date, datetime, time @@ -178,3 +179,8 @@ def parse_volume(volume_dict: Union[Mapping, MachineVolume]) -> MachineVolume: continue else: raise ValueError(f"Could not parse volume: {volume_dict}") + + +def compute_sha256(s: str) -> str: + """Compute the SHA256 hash of a string.""" + return hashlib.sha256(s.encode()).hexdigest()