Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
18 changes: 17 additions & 1 deletion node/airdrop_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

Implements RIP-305: Cross-Chain Airdrop for wRTC on Solana + Base


Tracks:
A: Solana SPL Token (wRTC)
B: Base ERC-20 Token (wRTC)
Expand Down Expand Up @@ -37,6 +38,7 @@
import math
import os
import re
import requests
import sqlite3
import time
from dataclasses import dataclass, asdict
Expand Down Expand Up @@ -793,8 +795,22 @@ def claim_airdrop(
github_username = self._normalize_github_username(github_username)
if not self._is_valid_github_username(github_username):
return False, "Invalid GitHub username", None
# Verify ownership if a token is provided
if github_token:
try:
auth_resp = requests.get(
"https://api.github.com/user",
headers={"Accept": "application/vnd.github.v3+json", "Authorization": f"token {github_token}", "User-Agent": "RustChain-Airdrop-Verifier"},
timeout=10,
)
if auth_resp.status_code != 200:
return False, "Failed to verify GitHub token", None
auth_login = auth_resp.json().get("login", "").strip().casefold()
if auth_login != github_username:
return False, "GitHub token does not match provided username", None
except requests.RequestException:
return False, "GitHub verification failed", None
chain_lower = chain.lower()

if self._has_claimed(github_username, wallet_address, chain_lower):
return False, "Claim already exists for this GitHub account or wallet", None

Expand Down
21 changes: 21 additions & 0 deletions node/gpu_render_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,13 +645,34 @@ def create_escrow():
if metadata is not None and not isinstance(metadata, dict):
return jsonify({"error": "metadata must be an object"}), 400

# ---- Security checks ----
# Require Ed25519 signature over a message to authenticate from_wallet.
signature_hex, sig_err = _string_field(data, "signature")
if sig_err is not None:
return sig_err
message, msg_err = _string_field(data, "message")
if msg_err is not None:
return msg_err

try:
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
pub_bytes = bytes.fromhex(from_wallet)
sig_bytes = bytes.fromhex(signature_hex)
Ed25519PublicKey.from_public_bytes(pub_bytes).verify(sig_bytes, message.encode())
except Exception as e:
logger.warning("Ed25519 signature verification failed: %s", e)
return jsonify({"error": "Invalid or missing signature for from_wallet"}), 401
# End of security checks

result = protocol.create_escrow(
job_type=job_type,
from_wallet=from_wallet,
to_wallet=to_wallet,
amount_rtc=amount_rtc,
metadata=metadata,
)
# Remove escrow_secret from API response to avoid leakage
result.pop("escrow_secret", None)
status_code = 201 if "error" not in result else 400
return jsonify(result), status_code

Expand Down
91 changes: 91 additions & 0 deletions tests/test_gpu_render_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,5 +394,96 @@ def test_gpu_protocol_attest_accepts_api_key_header(tmp_path, monkeypatch):
assert nodes["count"] == 1


def test_gpu_protocol_escrow_valid_signature(tmp_path, monkeypatch):
client = _route_client(tmp_path, monkeypatch)
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
priv = Ed25519PrivateKey.generate()
pub = priv.public_key()

# We serialize the public key natively (often 32 bytes for Ed25519)
from cryptography.hazmat.primitives import serialization
pub_bytes = pub.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
pub_hex = pub_bytes.hex()
message = "test_nonce_timestamp"
sig = priv.sign(message.encode())
sig_hex = sig.hex()

response = client.post(
"/render/escrow",
json={
"job_type": "render",
"from_wallet": pub_hex,
"to_wallet": "provider",
"amount_rtc": 10.0,
"signature": sig_hex,
"message": message
}
)

assert response.status_code == 201
data = response.get_json()
assert data["status"] == "locked"
assert data["from_wallet"] == pub_hex
assert "escrow_secret" not in data

def test_gpu_protocol_escrow_invalid_signature(tmp_path, monkeypatch):
client = _route_client(tmp_path, monkeypatch)
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives import serialization
priv = Ed25519PrivateKey.generate()
pub = priv.public_key()
pub_bytes = pub.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
pub_hex = pub_bytes.hex()
message = "test_nonce"

priv2 = Ed25519PrivateKey.generate()
sig2 = priv2.sign(message.encode())

response = client.post(
"/render/escrow",
json={
"job_type": "render",
"from_wallet": pub_hex,
"to_wallet": "provider",
"amount_rtc": 10.0,
"signature": sig2.hex(),
"message": message
}
)

assert response.status_code == 401
assert response.get_json() == {"error": "Invalid or missing signature for from_wallet"}

def test_gpu_protocol_escrow_missing_signature(tmp_path, monkeypatch):
client = _route_client(tmp_path, monkeypatch)
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives import serialization
priv = Ed25519PrivateKey.generate()
pub = priv.public_key()
pub_bytes = pub.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
pub_hex = pub_bytes.hex()

response = client.post(
"/render/escrow",
json={
"job_type": "render",
"from_wallet": pub_hex,
"to_wallet": "provider",
"amount_rtc": 10.0
}
)

assert response.status_code == 401
assert response.get_json() == {"error": "Invalid or missing signature for from_wallet"}

if __name__ == "__main__":
unittest.main()