Skip to content

Commit ae84f93

Browse files
authored
Merge pull request #2961 from humanprotocol/develop
Release 2024-12-26
2 parents 38c9a5d + aae6f4b commit ae84f93

File tree

36 files changed

+1324
-898
lines changed

36 files changed

+1324
-898
lines changed
Lines changed: 182 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,201 @@
11
import datetime
2-
import hashlib
32
import json
3+
from collections.abc import Generator
4+
from contextlib import ExitStack, contextmanager
5+
from logging import Logger
46
from pathlib import Path
57
from typing import Any
8+
from unittest import mock
69

710
import uvicorn
811
from httpx import URL
912

1013
from src.chain.kvstore import register_in_kvstore
1114
from src.core.config import Config
15+
from src.db import SessionLocal
1216
from src.services import cloud
17+
from src.services import cvat as cvat_service
1318
from src.services.cloud import BucketAccessInfo
14-
from src.utils.logging import get_function_logger
19+
from src.utils.logging import format_sequence, get_function_logger
1520

1621

17-
def apply_local_development_patches():
18-
"""
19-
Applies local development patches to avoid manual source code modification.:
20-
- Overrides `EscrowUtils.get_escrow` to return local escrow data with mock values if the escrow
21-
address matches a local manifest.
22-
- Loads local manifest files from cloud storage into `LOCAL_MANIFEST_FILES`.
23-
- Disables address validation by overriding `validate_address`.
24-
- Replaces `validate_oracle_webhook_signature` with a lenient version that uses
25-
partial signature parsing.
26-
- Generates ECDSA keys if not already present for local JWT signing,
27-
and sets the public key in `Config.human_app_config`.
28-
"""
29-
import src.handlers.job_creation
30-
31-
original_make_cvat_cloud_storage_params = (
32-
src.handlers.job_creation._make_cvat_cloud_storage_params
22+
@contextmanager
23+
def _mock_cvat_cloud_storage_params(logger: Logger) -> Generator[None, None, None]:
24+
from src.handlers.job_creation import (
25+
_make_cvat_cloud_storage_params as original_make_cvat_cloud_storage_params,
3326
)
3427

3528
def patched_make_cvat_cloud_storage_params(bucket_info: BucketAccessInfo) -> dict:
3629
original_host_url = bucket_info.host_url
3730

3831
if Config.development_config.cvat_in_docker:
3932
bucket_info.host_url = str(
40-
URL(original_host_url).copy_with(host=Config.development_config.cvat_local_host)
33+
URL(original_host_url).copy_with(
34+
host=Config.development_config.exchange_oracle_host
35+
)
4136
)
4237
logger.info(
4338
f"DEV: Changed {original_host_url} to {bucket_info.host_url} for CVAT storage"
4439
)
40+
4541
try:
4642
return original_make_cvat_cloud_storage_params(bucket_info)
4743
finally:
4844
bucket_info.host_url = original_host_url
4945

50-
src.handlers.job_creation._make_cvat_cloud_storage_params = (
51-
patched_make_cvat_cloud_storage_params
52-
)
46+
with mock.patch(
47+
"src.handlers.job_creation._make_cvat_cloud_storage_params",
48+
patched_make_cvat_cloud_storage_params,
49+
):
50+
yield
5351

54-
def prepare_signed_message(
55-
escrow_address,
56-
chain_id,
57-
message: str | None = None,
58-
body: dict | None = None,
59-
) -> tuple[None, str]:
60-
digest = hashlib.sha256(
61-
(escrow_address + ":".join(map(str, (chain_id, message, body)))).encode()
62-
).hexdigest()
63-
signature = f"{OracleWebhookTypes.recording_oracle}:{digest}"
64-
logger.info(f"DEV: Generated patched signature {signature}")
65-
return None, signature
66-
67-
src.utils.webhooks.prepare_signed_message = prepare_signed_message
6852

53+
@contextmanager
54+
def _mock_get_manifests_from_minio(logger: Logger) -> Generator[None, None, None]:
6955
from human_protocol_sdk.constants import ChainId
7056
from human_protocol_sdk.escrow import EscrowData, EscrowUtils
7157

72-
logger = get_function_logger(apply_local_development_patches.__name__)
73-
7458
minio_client = cloud.make_client(BucketAccessInfo.parse_obj(Config.storage_config))
59+
original_get_escrow = EscrowUtils.get_escrow
7560

76-
def get_local_escrow(chain_id: int, escrow_address: str) -> EscrowData:
77-
possible_manifest_name = escrow_address.split(":")[0]
78-
local_manifests = minio_client.list_files(bucket="manifests")
79-
logger.info(f"DEV: Local manifests: {local_manifests}")
80-
if possible_manifest_name in local_manifests:
81-
logger.info(f"DEV: Using local manifest {escrow_address}")
82-
return EscrowData(
83-
chain_id=ChainId(chain_id),
84-
id="test",
85-
address=escrow_address,
86-
amount_paid=10,
87-
balance=10,
88-
count=1,
89-
factory_address="",
90-
launcher="",
91-
status="Pending",
92-
token="HMT", # noqa: S106
93-
total_funded_amount=10,
94-
created_at=datetime.datetime(2023, 1, 1, tzinfo=datetime.timezone.utc),
95-
manifest_url=(
96-
f"http://{Config.storage_config.endpoint_url}/manifests/{possible_manifest_name}"
97-
),
61+
def patched_get_escrow(chain_id: int, escrow_address: str) -> EscrowData:
62+
minio_manifests = minio_client.list_files(bucket="manifests")
63+
logger.debug(f"DEV: Local manifests: {format_sequence(minio_manifests)}")
64+
65+
candidate_files = [fn for fn in minio_manifests if f"{escrow_address}.json" in fn]
66+
if not candidate_files:
67+
return original_get_escrow(ChainId(chain_id), escrow_address)
68+
elif len(candidate_files) != 1:
69+
raise Exception(
70+
"Can't select local manifest to be used for escrow '{}'"
71+
" - several manifests math: {}".format(
72+
escrow_address, format_sequence(candidate_files)
73+
)
9874
)
99-
return original_get_escrow(ChainId(chain_id), escrow_address)
10075

101-
original_get_escrow = EscrowUtils.get_escrow
102-
EscrowUtils.get_escrow = get_local_escrow
76+
manifest_file = candidate_files[0]
77+
escrow = EscrowData(
78+
chain_id=ChainId(chain_id),
79+
id="test",
80+
address=escrow_address,
81+
amount_paid=10,
82+
balance=10,
83+
count=1,
84+
factory_address="",
85+
launcher="",
86+
status="Pending",
87+
token="HMT", # noqa: S106
88+
total_funded_amount=10,
89+
created_at=datetime.datetime(2023, 1, 1, tzinfo=datetime.timezone.utc),
90+
manifest_url=(f"http://{Config.storage_config.endpoint_url}/manifests/{manifest_file}"),
91+
)
92+
93+
logger.info(f"DEV: Using local manifest '{manifest_file}' for escrow '{escrow_address}'")
94+
return escrow
95+
96+
with mock.patch.object(EscrowUtils, "get_escrow", patched_get_escrow):
97+
yield
98+
99+
100+
@contextmanager
101+
def _mock_webhook_signature_checking(_: Logger) -> Generator[None, None, None]:
102+
"""
103+
Allows to receive webhooks from other services:
104+
- from launcher - with signature "job_launcher<number>"
105+
- from recording oracle -
106+
encoded with Config.localhost.recording_oracle_address wallet address
107+
or signature "recording_oracle<number>"
108+
- from reputation oracle -
109+
encoded with Config.localhost.reputation_oracle_address wallet address
110+
or signature "reputation_oracle<number>"
111+
"""
103112

104-
import src.schemas.webhook
113+
from src.chain.escrow import (
114+
get_available_webhook_types as original_get_available_webhook_types,
115+
)
105116
from src.core.types import OracleWebhookTypes
117+
from src.validators.signature import (
118+
validate_oracle_webhook_signature as original_validate_oracle_webhook_signature,
119+
)
106120

107-
src.schemas.webhook.validate_address = lambda x: x
121+
async def patched_validate_oracle_webhook_signature(request, signature, webhook):
122+
for webhook_type in OracleWebhookTypes:
123+
if signature.startswith(webhook_type.value.lower()):
124+
return webhook_type
125+
126+
return await original_validate_oracle_webhook_signature(request, signature, webhook)
127+
128+
def patched_get_available_webhook_types(chain_id, escrow_address):
129+
d = dict(original_get_available_webhook_types(chain_id, escrow_address))
130+
d[Config.localhost.recording_oracle_address.lower()] = OracleWebhookTypes.recording_oracle
131+
d[Config.localhost.reputation_oracle_address.lower()] = OracleWebhookTypes.reputation_oracle
132+
return d
133+
134+
with (
135+
mock.patch("src.schemas.webhook.validate_address", lambda x: x),
136+
mock.patch(
137+
"src.validators.signature.get_available_webhook_types",
138+
patched_get_available_webhook_types,
139+
),
140+
mock.patch(
141+
"src.endpoints.webhook.validate_oracle_webhook_signature",
142+
patched_validate_oracle_webhook_signature,
143+
),
144+
):
145+
yield
146+
147+
148+
@contextmanager
149+
def _mock_endpoint_auth(logger: Logger) -> Generator[None, None, None]:
150+
"""
151+
Allows simplified authentication:
152+
- Bearer {"wallet_address": "...", "email": "..."}
153+
- Bearer {"role": "human_app"}
154+
"""
108155

109-
async def lenient_validate_oracle_webhook_signature(request, signature, webhook):
110-
from src.validators.signature import validate_oracle_webhook_signature
156+
from src.endpoints.authentication import HUMAN_APP_ROLE, TokenAuthenticator
111157

112-
try:
113-
parsed_type = OracleWebhookTypes(signature.split(":")[0])
114-
logger.info(f"DEV: Recovered {parsed_type} from the signature {signature}")
115-
except (ValueError, TypeError):
116-
return await validate_oracle_webhook_signature(request, signature, webhook)
158+
original_decode_token = TokenAuthenticator._decode_token
117159

118-
import src.endpoints.webhook
160+
def decode_plain_json_token(self, token) -> dict[str, Any]:
161+
try:
162+
token_data = json.loads(token)
119163

120-
src.endpoints.webhook.validate_oracle_webhook_signature = (
121-
lenient_validate_oracle_webhook_signature
122-
)
164+
if (user_wallet := token_data.get("wallet_address")) and not token_data.get("email"):
165+
with SessionLocal.begin() as session:
166+
user = cvat_service.get_user_by_id(session, user_wallet)
167+
if not user:
168+
raise Exception(f"Could not find user with wallet address '{user_wallet}'")
123169

124-
import src.endpoints.authentication
170+
token_data["email"] = user.cvat_email
125171

126-
original_decode_token = src.endpoints.authentication.TokenAuthenticator._decode_token
172+
if token_data.get("role") == HUMAN_APP_ROLE:
173+
token_data["wallet_address"] = None
174+
token_data["email"] = ""
127175

128-
def decode_plain_json_token(self, token) -> dict[str, Any]:
129-
"""
130-
Allows Authentication: Bearer {"wallet_address": "...", "email": "..."}
131-
"""
132-
try:
133-
decoded = json.loads(token)
134-
logger.info(f"DEV: Decoded plain JSON auth token: {decoded}")
176+
logger.info(f"DEV: Decoded plain JSON auth token: {token_data}")
177+
return token_data
135178
except (ValueError, TypeError):
136179
return original_decode_token(self, token)
137180

138-
src.endpoints.authentication.TokenAuthenticator._decode_token = decode_plain_json_token
181+
with mock.patch.object(TokenAuthenticator, "_decode_token", decode_plain_json_token):
182+
yield
183+
184+
185+
@contextmanager
186+
def _mock_human_app_keys(_: Logger) -> Generator[None, None, None]:
187+
"Creates or uses local Human App JWT keys"
139188

140189
from tests.api.test_exchange_api import generate_ecdsa_keys
141190

142191
# generating keys for local development
143192
repo_root = Path(__file__).parent
144-
human_app_private_key_file, human_app_public_key_file = (
145-
repo_root / "human_app_private_key.pem",
146-
repo_root / "human_app_public_key.pem",
147-
)
193+
dev_dir = repo_root / "dev"
194+
dev_dir.mkdir(exist_ok=True)
195+
196+
human_app_private_key_file = dev_dir / "human_app_private_key.pem"
197+
human_app_public_key_file = dev_dir / "human_app_public_key.pem"
198+
148199
if not (human_app_public_key_file.exists() and human_app_private_key_file.exists()):
149200
private_key, public_key = generate_ecdsa_keys()
150201
human_app_private_key_file.write_text(private_key)
@@ -154,20 +205,47 @@ def decode_plain_json_token(self, token) -> dict[str, Any]:
154205

155206
Config.human_app_config.jwt_public_key = public_key
156207

157-
logger.warning("DEV: Local development patches applied.")
208+
yield
209+
210+
211+
@contextmanager
212+
def apply_local_development_patches() -> Generator[None, None, None]:
213+
"""
214+
Applies local development patches to avoid manual source code modification
215+
"""
216+
217+
logger = get_function_logger(apply_local_development_patches.__name__)
218+
219+
logger.warning("DEV: Applying local development patches")
220+
221+
with ExitStack() as es:
222+
for mock_callback in (
223+
_mock_cvat_cloud_storage_params,
224+
_mock_get_manifests_from_minio,
225+
_mock_webhook_signature_checking,
226+
_mock_endpoint_auth,
227+
_mock_human_app_keys,
228+
):
229+
logger.warning(f"DEV: applying patch {mock_callback.__name__}...")
230+
es.enter_context(mock_callback(logger))
231+
232+
logger.warning("DEV: Local development patches applied.")
233+
234+
yield
158235

159236

160237
if __name__ == "__main__":
161-
is_dev = Config.environment == "development"
162-
if is_dev:
163-
apply_local_development_patches()
164-
165-
Config.validate()
166-
register_in_kvstore()
167-
168-
uvicorn.run(
169-
app="src:app",
170-
host="0.0.0.0", # noqa: S104
171-
port=int(Config.port),
172-
workers=Config.workers_amount,
173-
)
238+
with ExitStack() as es:
239+
is_dev = Config.environment == "development"
240+
if is_dev:
241+
es.enter_context(apply_local_development_patches())
242+
243+
Config.validate()
244+
register_in_kvstore()
245+
246+
uvicorn.run(
247+
app="src:app",
248+
host="0.0.0.0", # noqa: S104
249+
port=int(Config.port),
250+
workers=Config.workers_amount,
251+
)

packages/examples/cvat/exchange-oracle/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ ignore = [
114114
"ANN002", # Missing type annotation for `*args`
115115
"TRY300", # Consider moving this statement to an `else` block
116116
"C901", # Function is too complex
117+
"PLW1508", # invalid-envvar-default. Alerts only for os.getenv(), but not for os.environ.get()
117118
"PLW2901", # Variable overwritten by assignment target
118119
"PTH118", # Prefer pathlib instead of os.path
119120
"PTH119", # `os.path.basename()` should be replaced by `Path.name`

0 commit comments

Comments
 (0)