mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-10 16:58:08 +00:00
feat: 添加 EdgeX 交易所适配器及相关客户端实现,支持订单、深度和K线数据处理
This commit is contained in:
@@ -0,0 +1,466 @@
|
||||
import asyncio
|
||||
import binascii
|
||||
import hashlib
|
||||
import time
|
||||
import uuid
|
||||
from typing import Dict, Any, Optional, Tuple, List, Union
|
||||
import json
|
||||
|
||||
import aiohttp
|
||||
from Crypto.Hash import keccak
|
||||
|
||||
from .signing_adapter import SigningAdapter
|
||||
|
||||
# Import field prime for modular arithmetic
|
||||
try:
|
||||
from ..crypto.constants import FIELD_PRIME
|
||||
except ImportError:
|
||||
# Fallback if crypto module is not available
|
||||
FIELD_PRIME = 0x800000000000011000000000000000000000000000000000000000000000001
|
||||
|
||||
# Constants
|
||||
LIMIT_ORDER_WITH_FEE_TYPE = 3
|
||||
|
||||
|
||||
class L2Signature:
|
||||
"""Represents a signature for L2 operations."""
|
||||
|
||||
def __init__(self, r: str, s: str, v: str = ""):
|
||||
self.r = r
|
||||
self.s = s
|
||||
self.v = v
|
||||
|
||||
|
||||
class AsyncClient:
|
||||
"""Async base client with common functionality."""
|
||||
|
||||
def __init__(self, base_url: str, account_id: int, stark_pri_key: str,
|
||||
signing_adapter: Optional[SigningAdapter] = None,
|
||||
timeout: float = 30.0, connector_limit: int = 100):
|
||||
"""
|
||||
Initialize the async internal client.
|
||||
|
||||
Args:
|
||||
base_url: Base URL for API endpoints
|
||||
account_id: Account ID for authentication
|
||||
stark_pri_key: Stark private key for signing
|
||||
signing_adapter: Optional signing adapter to use for cryptographic operations
|
||||
timeout: Request timeout in seconds
|
||||
connector_limit: Maximum number of connections in the pool
|
||||
"""
|
||||
self.base_url = base_url
|
||||
self.account_id = account_id
|
||||
self.stark_pri_key = stark_pri_key
|
||||
|
||||
# Use the provided signing adapter (required)
|
||||
if signing_adapter is None:
|
||||
raise ValueError("signing_adapter is required")
|
||||
self.signing_adapter = signing_adapter
|
||||
|
||||
# Store configuration for later session creation
|
||||
self._session = None
|
||||
self._timeout = timeout
|
||||
self._connector_limit = connector_limit
|
||||
self._closed = False
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Async context manager entry."""
|
||||
await self._ensure_session()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Async context manager exit."""
|
||||
await self.close()
|
||||
|
||||
async def _ensure_session(self):
|
||||
"""Ensure the aiohttp session is created."""
|
||||
if self._session is None or self._session.closed:
|
||||
# Create connector and session when needed (inside event loop)
|
||||
timeout_config = aiohttp.ClientTimeout(total=self._timeout)
|
||||
connector = aiohttp.TCPConnector(
|
||||
limit=self._connector_limit,
|
||||
limit_per_host=30,
|
||||
keepalive_timeout=30,
|
||||
enable_cleanup_closed=True
|
||||
)
|
||||
|
||||
self._session = aiohttp.ClientSession(
|
||||
timeout=timeout_config,
|
||||
connector=connector,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
async def close(self):
|
||||
"""Close the HTTP session and cleanup resources."""
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
self._closed = True
|
||||
|
||||
@property
|
||||
def session(self) -> aiohttp.ClientSession:
|
||||
"""Get the HTTP session, ensuring it's created."""
|
||||
if self._session is None or self._session.closed:
|
||||
raise RuntimeError("Session not initialized. Use 'async with client:' or call '_ensure_session()'")
|
||||
return self._session
|
||||
|
||||
def get_account_id(self) -> int:
|
||||
"""Get the account ID."""
|
||||
return self.account_id
|
||||
|
||||
def get_stark_pri_key(self) -> str:
|
||||
"""Get the stark private key."""
|
||||
return self.stark_pri_key
|
||||
|
||||
def sign(self, message_hash: bytes) -> L2Signature:
|
||||
"""
|
||||
Sign a message hash using the client's Stark private key.
|
||||
|
||||
Args:
|
||||
message_hash: The hash of the message to sign
|
||||
|
||||
Returns:
|
||||
L2Signature: The signature components
|
||||
|
||||
Raises:
|
||||
ValueError: If the stark private key is not set or invalid
|
||||
"""
|
||||
private_key = self.get_stark_pri_key()
|
||||
if not private_key:
|
||||
raise ValueError("stark private key not set")
|
||||
|
||||
# Sign the message using the signing adapter
|
||||
try:
|
||||
r, s = self.signing_adapter.sign(message_hash, private_key)
|
||||
return L2Signature(r=r, s=s, v="")
|
||||
except Exception as e:
|
||||
raise ValueError(f"failed to sign message: {str(e)}")
|
||||
|
||||
def generate_uuid(self) -> str:
|
||||
"""Generate a UUID for client order IDs."""
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def calc_nonce(self, client_order_id: str) -> int:
|
||||
"""
|
||||
Calculate a nonce from a client order ID.
|
||||
|
||||
Args:
|
||||
client_order_id: The client order ID
|
||||
|
||||
Returns:
|
||||
int: The calculated nonce
|
||||
"""
|
||||
# Use SHA256 like the Go SDK (not Keccak256)
|
||||
h = hashlib.sha256()
|
||||
h.update(client_order_id.encode())
|
||||
hash_hex = h.hexdigest()
|
||||
return int(hash_hex[:8], 16)
|
||||
|
||||
async def make_authenticated_request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
params: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Make an authenticated HTTP request.
|
||||
|
||||
Args:
|
||||
method: HTTP method (GET, POST, etc.)
|
||||
path: API path (e.g., '/api/v1/private/order/createOrder')
|
||||
data: JSON data for POST requests
|
||||
params: Query parameters for GET requests
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Response JSON data
|
||||
|
||||
Raises:
|
||||
ValueError: If the request fails
|
||||
"""
|
||||
await self._ensure_session()
|
||||
|
||||
# Generate timestamp
|
||||
timestamp = int(time.time() * 1000)
|
||||
|
||||
# Build full URL
|
||||
url = f"{self.base_url}{path}"
|
||||
|
||||
# Generate signature content
|
||||
sign_content = self._build_signature_content(timestamp, method, path, data, params)
|
||||
|
||||
# Sign the content
|
||||
keccak_hash = keccak.new(digest_bits=256)
|
||||
keccak_hash.update(sign_content.encode())
|
||||
content_hash = keccak_hash.digest()
|
||||
|
||||
sig = self.sign(content_hash)
|
||||
|
||||
# Prepare headers
|
||||
headers = {
|
||||
"X-edgeX-Api-Timestamp": str(timestamp),
|
||||
"X-edgeX-Api-Signature": f"{sig.r}{sig.s}"
|
||||
}
|
||||
|
||||
# Make the request
|
||||
try:
|
||||
async with self.session.request(
|
||||
method=method,
|
||||
url=url,
|
||||
json=data,
|
||||
params=params,
|
||||
headers=headers
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
try:
|
||||
error_detail = await response.json()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {error_detail}")
|
||||
except (aiohttp.ContentTypeError, json.JSONDecodeError):
|
||||
text = await response.text()
|
||||
raise ValueError(f"request failed with status code: {response.status}, response: {text}")
|
||||
|
||||
resp_data = await response.json()
|
||||
|
||||
# Check response code
|
||||
if resp_data.get("code") != "SUCCESS":
|
||||
error_param = resp_data.get("errorParam")
|
||||
if error_param:
|
||||
raise ValueError(f"request failed with error params: {error_param}")
|
||||
raise ValueError(f"request failed with code: {resp_data.get('code')}")
|
||||
|
||||
return resp_data
|
||||
|
||||
except aiohttp.ClientError as e:
|
||||
raise ValueError(f"HTTP request failed: {str(e)}")
|
||||
|
||||
def _build_signature_content(
|
||||
self,
|
||||
timestamp: int,
|
||||
method: str,
|
||||
path: str,
|
||||
data: Optional[Dict[str, Any]],
|
||||
params: Optional[Dict[str, Any]]
|
||||
) -> str:
|
||||
"""Build the content string for signature generation."""
|
||||
if data:
|
||||
# Convert body to sorted string format
|
||||
body_str = self.get_value(data)
|
||||
sign_content = f"{timestamp}{method}{path}{body_str}"
|
||||
else:
|
||||
# For requests without body, use query parameters if present
|
||||
if params:
|
||||
# Sort query parameters as strings (matching Go SDK exactly)
|
||||
param_pairs = []
|
||||
for key, value in sorted(params.items()):
|
||||
param_pairs.append(f"{key}={value}")
|
||||
query_string = "&".join(param_pairs)
|
||||
sign_content = f"{timestamp}{method}{path}{query_string}"
|
||||
else:
|
||||
sign_content = f"{timestamp}{method}{path}"
|
||||
|
||||
return sign_content
|
||||
|
||||
def get_value(self, data: Union[Dict[str, Any], List[Any], str, int, float, None]) -> str:
|
||||
"""
|
||||
Convert a value to a string representation for signing.
|
||||
This function recursively processes dictionaries, lists, and primitive types.
|
||||
|
||||
Args:
|
||||
data: The value to convert
|
||||
|
||||
Returns:
|
||||
str: The string representation
|
||||
"""
|
||||
if data is None:
|
||||
return ""
|
||||
|
||||
if isinstance(data, str):
|
||||
return data
|
||||
|
||||
if isinstance(data, bool):
|
||||
# Convert boolean to lowercase string to match Go SDK
|
||||
return str(data).lower()
|
||||
|
||||
if isinstance(data, (int, float)):
|
||||
return str(data)
|
||||
|
||||
if isinstance(data, list):
|
||||
if len(data) == 0:
|
||||
return ""
|
||||
values = [self.get_value(item) for item in data]
|
||||
return "&".join(values)
|
||||
|
||||
if isinstance(data, dict):
|
||||
# Convert all values to strings and sort by keys
|
||||
sorted_map = {}
|
||||
for key, val in data.items():
|
||||
sorted_map[key] = self.get_value(val)
|
||||
|
||||
# Get sorted keys
|
||||
keys = sorted(sorted_map.keys())
|
||||
|
||||
# Build key=value pairs
|
||||
pairs = [f"{key}={sorted_map[key]}" for key in keys]
|
||||
return "&".join(pairs)
|
||||
|
||||
# Handle other types by converting to string
|
||||
return str(data)
|
||||
|
||||
def calc_limit_order_hash(
|
||||
self,
|
||||
synthetic_asset_id: str,
|
||||
collateral_asset_id: str,
|
||||
fee_asset_id: str,
|
||||
is_buy: bool,
|
||||
amount_synthetic: int,
|
||||
amount_collateral: int,
|
||||
amount_fee: int,
|
||||
nonce: int,
|
||||
account_id: int,
|
||||
expire_time: int
|
||||
) -> bytes:
|
||||
"""
|
||||
Calculate the hash for a limit order using StarkEx protocol.
|
||||
|
||||
Args:
|
||||
synthetic_asset_id: The synthetic asset ID (hex string)
|
||||
collateral_asset_id: The collateral asset ID (hex string)
|
||||
fee_asset_id: The fee asset ID (hex string)
|
||||
is_buy: Whether the order is a buy order
|
||||
amount_synthetic: The synthetic amount
|
||||
amount_collateral: The collateral amount
|
||||
amount_fee: The fee amount
|
||||
nonce: The nonce
|
||||
account_id: The account ID (position ID)
|
||||
expire_time: The expiration time
|
||||
|
||||
Returns:
|
||||
bytes: The calculated hash
|
||||
"""
|
||||
# Remove 0x prefix if present
|
||||
if synthetic_asset_id.startswith('0x'):
|
||||
synthetic_asset_id = synthetic_asset_id[2:]
|
||||
if collateral_asset_id.startswith('0x'):
|
||||
collateral_asset_id = collateral_asset_id[2:]
|
||||
if fee_asset_id.startswith('0x'):
|
||||
fee_asset_id = fee_asset_id[2:]
|
||||
|
||||
# Convert hex strings to integers and ensure they're within the field
|
||||
asset_id_synthetic = int(synthetic_asset_id, 16) % FIELD_PRIME
|
||||
asset_id_collateral = int(collateral_asset_id, 16) % FIELD_PRIME
|
||||
asset_id_fee = int(fee_asset_id, 16) % FIELD_PRIME
|
||||
|
||||
# Determine buy/sell assets based on order direction
|
||||
if is_buy:
|
||||
asset_id_sell = asset_id_collateral
|
||||
asset_id_buy = asset_id_synthetic
|
||||
amount_sell = amount_collateral
|
||||
amount_buy = amount_synthetic
|
||||
else:
|
||||
asset_id_sell = asset_id_synthetic
|
||||
asset_id_buy = asset_id_collateral
|
||||
amount_sell = amount_synthetic
|
||||
amount_buy = amount_collateral
|
||||
|
||||
# Use the signing adapter to calculate the Pedersen hash
|
||||
# First hash: hash(asset_id_sell, asset_id_buy)
|
||||
msg = self.signing_adapter.pedersen_hash([asset_id_sell, asset_id_buy])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Second hash: hash(msg, asset_id_fee)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, asset_id_fee])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 0
|
||||
# packed_message0 = amount_sell * 2^64 + amount_buy * 2^64 + max_amount_fee * 2^32 + nonce
|
||||
packed_message0 = amount_sell
|
||||
packed_message0 = (packed_message0 << 64) + amount_buy
|
||||
packed_message0 = (packed_message0 << 64) + amount_fee
|
||||
packed_message0 = (packed_message0 << 32) + nonce
|
||||
packed_message0 = packed_message0 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Third hash: hash(msg, packed_message0)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_message0])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 1
|
||||
# packed_message1 = LIMIT_ORDER_WITH_FEES * 2^64 + position_id * 2^64 + position_id * 2^64 + position_id * 2^32 + expiration_timestamp * 2^17
|
||||
packed_message1 = LIMIT_ORDER_WITH_FEE_TYPE
|
||||
packed_message1 = (packed_message1 << 64) + account_id
|
||||
packed_message1 = (packed_message1 << 64) + account_id
|
||||
packed_message1 = (packed_message1 << 64) + account_id
|
||||
packed_message1 = (packed_message1 << 32) + expire_time
|
||||
packed_message1 = packed_message1 << 17 # Padding
|
||||
packed_message1 = packed_message1 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Final hash: hash(msg, packed_message1)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_message1])
|
||||
|
||||
return msg
|
||||
|
||||
def calc_transfer_hash(
|
||||
self,
|
||||
asset_id: int,
|
||||
asset_id_fee: int,
|
||||
receiver_public_key: int,
|
||||
sender_position_id: int,
|
||||
receiver_position_id: int,
|
||||
fee_position_id: int,
|
||||
nonce: int,
|
||||
amount: int,
|
||||
max_amount_fee: int,
|
||||
expiration_timestamp: int
|
||||
) -> bytes:
|
||||
"""
|
||||
Calculate the hash for a transfer using StarkEx protocol.
|
||||
|
||||
Args:
|
||||
asset_id: The asset ID
|
||||
asset_id_fee: The fee asset ID
|
||||
receiver_public_key: The receiver's public key
|
||||
sender_position_id: The sender's position ID
|
||||
receiver_position_id: The receiver's position ID
|
||||
fee_position_id: The fee position ID
|
||||
nonce: The nonce
|
||||
amount: The transfer amount
|
||||
max_amount_fee: The maximum fee amount
|
||||
expiration_timestamp: The expiration timestamp
|
||||
|
||||
Returns:
|
||||
bytes: The calculated hash
|
||||
"""
|
||||
# First hash: hash(asset_id, asset_id_fee)
|
||||
msg = self.signing_adapter.pedersen_hash([asset_id, asset_id_fee])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Second hash: hash(msg, receiver_public_key)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, receiver_public_key])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 0
|
||||
# packed_msg0 = sender_position_id * 2^64 + receiver_position_id * 2^64 + fee_position_id * 2^32 + nonce
|
||||
packed_msg0 = sender_position_id
|
||||
packed_msg0 = (packed_msg0 << 64) + receiver_position_id
|
||||
packed_msg0 = (packed_msg0 << 64) + fee_position_id
|
||||
packed_msg0 = (packed_msg0 << 32) + nonce
|
||||
packed_msg0 = packed_msg0 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Third hash: hash(msg, packed_msg0)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_msg0])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 1
|
||||
# packed_msg1 = 4 * 2^64 + amount * 2^64 + max_amount_fee * 2^32 + expiration_timestamp * 2^81
|
||||
packed_msg1 = 4 # Transfer type
|
||||
packed_msg1 = (packed_msg1 << 64) + amount
|
||||
packed_msg1 = (packed_msg1 << 64) + max_amount_fee
|
||||
packed_msg1 = (packed_msg1 << 32) + expiration_timestamp
|
||||
packed_msg1 = packed_msg1 << 81 # Padding
|
||||
packed_msg1 = packed_msg1 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Final hash: hash(msg, packed_msg1)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_msg1])
|
||||
|
||||
return msg
|
||||
@@ -0,0 +1,312 @@
|
||||
import binascii
|
||||
import hashlib
|
||||
import time
|
||||
import uuid
|
||||
from typing import Dict, Any, Optional, Tuple, List, Union
|
||||
|
||||
import requests
|
||||
from Crypto.Hash import keccak
|
||||
|
||||
from .signing_adapter import SigningAdapter
|
||||
|
||||
# Import field prime for modular arithmetic
|
||||
try:
|
||||
from ..crypto.constants import FIELD_PRIME
|
||||
except ImportError:
|
||||
# Fallback if crypto module is not available
|
||||
FIELD_PRIME = 0x800000000000011000000000000000000000000000000000000000000000001
|
||||
|
||||
# Constants
|
||||
LIMIT_ORDER_WITH_FEE_TYPE = 3
|
||||
|
||||
|
||||
class L2Signature:
|
||||
"""Represents a signature for L2 operations."""
|
||||
|
||||
def __init__(self, r: str, s: str, v: str = ""):
|
||||
self.r = r
|
||||
self.s = s
|
||||
self.v = v
|
||||
|
||||
|
||||
class Client:
|
||||
"""Base client with common functionality."""
|
||||
|
||||
def __init__(self, base_url: str, account_id: int, stark_pri_key: str, signing_adapter: Optional[SigningAdapter] = None):
|
||||
"""
|
||||
Initialize the internal client.
|
||||
|
||||
Args:
|
||||
base_url: Base URL for API endpoints
|
||||
account_id: Account ID for authentication
|
||||
stark_pri_key: Stark private key for signing
|
||||
signing_adapter: Optional signing adapter to use for cryptographic operations
|
||||
"""
|
||||
self.http_client = requests.Session()
|
||||
self.http_client.headers.update({
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
})
|
||||
self.base_url = base_url
|
||||
self.account_id = account_id
|
||||
self.stark_pri_key = stark_pri_key
|
||||
|
||||
# Use the provided signing adapter (required)
|
||||
if signing_adapter is None:
|
||||
raise ValueError("signing_adapter is required")
|
||||
self.signing_adapter = signing_adapter
|
||||
|
||||
def get_account_id(self) -> int:
|
||||
"""Get the account ID."""
|
||||
return self.account_id
|
||||
|
||||
def get_stark_pri_key(self) -> str:
|
||||
"""Get the stark private key."""
|
||||
return self.stark_pri_key
|
||||
|
||||
def sign(self, message_hash: bytes) -> L2Signature:
|
||||
"""
|
||||
Sign a message hash using the client's Stark private key.
|
||||
|
||||
Args:
|
||||
message_hash: The hash of the message to sign
|
||||
|
||||
Returns:
|
||||
L2Signature: The signature components
|
||||
|
||||
Raises:
|
||||
ValueError: If the stark private key is not set or invalid
|
||||
"""
|
||||
private_key = self.get_stark_pri_key()
|
||||
if not private_key:
|
||||
raise ValueError("stark private key not set")
|
||||
|
||||
# Sign the message using the signing adapter
|
||||
try:
|
||||
r, s = self.signing_adapter.sign(message_hash, private_key)
|
||||
return L2Signature(r=r, s=s, v="")
|
||||
except Exception as e:
|
||||
raise ValueError(f"failed to sign message: {str(e)}")
|
||||
|
||||
def generate_uuid(self) -> str:
|
||||
"""Generate a UUID for client order IDs."""
|
||||
return str(uuid.uuid4())
|
||||
|
||||
def calc_nonce(self, client_order_id: str) -> int:
|
||||
"""
|
||||
Calculate a nonce from a client order ID.
|
||||
|
||||
Args:
|
||||
client_order_id: The client order ID
|
||||
|
||||
Returns:
|
||||
int: The calculated nonce
|
||||
"""
|
||||
# Use SHA256 like the Go SDK (not Keccak256)
|
||||
h = hashlib.sha256()
|
||||
h.update(client_order_id.encode())
|
||||
hash_hex = h.hexdigest()
|
||||
return int(hash_hex[:8], 16)
|
||||
|
||||
def calc_limit_order_hash(
|
||||
self,
|
||||
synthetic_asset_id: str,
|
||||
collateral_asset_id: str,
|
||||
fee_asset_id: str,
|
||||
is_buy: bool,
|
||||
amount_synthetic: int,
|
||||
amount_collateral: int,
|
||||
amount_fee: int,
|
||||
nonce: int,
|
||||
account_id: int,
|
||||
expire_time: int
|
||||
) -> bytes:
|
||||
"""
|
||||
Calculate the hash for a limit order using StarkEx protocol.
|
||||
|
||||
Args:
|
||||
synthetic_asset_id: The synthetic asset ID (hex string)
|
||||
collateral_asset_id: The collateral asset ID (hex string)
|
||||
fee_asset_id: The fee asset ID (hex string)
|
||||
is_buy: Whether the order is a buy order
|
||||
amount_synthetic: The synthetic amount
|
||||
amount_collateral: The collateral amount
|
||||
amount_fee: The fee amount
|
||||
nonce: The nonce
|
||||
account_id: The account ID (position ID)
|
||||
expire_time: The expiration time
|
||||
|
||||
Returns:
|
||||
bytes: The calculated hash
|
||||
"""
|
||||
# Remove 0x prefix if present
|
||||
if synthetic_asset_id.startswith('0x'):
|
||||
synthetic_asset_id = synthetic_asset_id[2:]
|
||||
if collateral_asset_id.startswith('0x'):
|
||||
collateral_asset_id = collateral_asset_id[2:]
|
||||
if fee_asset_id.startswith('0x'):
|
||||
fee_asset_id = fee_asset_id[2:]
|
||||
|
||||
# Convert hex strings to integers and ensure they're within the field
|
||||
asset_id_synthetic = int(synthetic_asset_id, 16) % FIELD_PRIME
|
||||
asset_id_collateral = int(collateral_asset_id, 16) % FIELD_PRIME
|
||||
asset_id_fee = int(fee_asset_id, 16) % FIELD_PRIME
|
||||
|
||||
# Determine buy/sell assets based on order direction
|
||||
if is_buy:
|
||||
asset_id_sell = asset_id_collateral
|
||||
asset_id_buy = asset_id_synthetic
|
||||
amount_sell = amount_collateral
|
||||
amount_buy = amount_synthetic
|
||||
else:
|
||||
asset_id_sell = asset_id_synthetic
|
||||
asset_id_buy = asset_id_collateral
|
||||
amount_sell = amount_synthetic
|
||||
amount_buy = amount_collateral
|
||||
|
||||
# Use the signing adapter to calculate the Pedersen hash
|
||||
# First hash: hash(asset_id_sell, asset_id_buy)
|
||||
msg = self.signing_adapter.pedersen_hash([asset_id_sell, asset_id_buy])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Second hash: hash(msg, asset_id_fee)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, asset_id_fee])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 0
|
||||
# packed_message0 = amount_sell * 2^64 + amount_buy * 2^64 + max_amount_fee * 2^32 + nonce
|
||||
packed_message0 = amount_sell
|
||||
packed_message0 = (packed_message0 << 64) + amount_buy
|
||||
packed_message0 = (packed_message0 << 64) + amount_fee
|
||||
packed_message0 = (packed_message0 << 32) + nonce
|
||||
packed_message0 = packed_message0 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Third hash: hash(msg, packed_message0)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_message0])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 1
|
||||
# packed_message1 = LIMIT_ORDER_WITH_FEES * 2^64 + position_id * 2^64 + position_id * 2^64 + position_id * 2^32 + expiration_timestamp * 2^17
|
||||
packed_message1 = LIMIT_ORDER_WITH_FEE_TYPE
|
||||
packed_message1 = (packed_message1 << 64) + account_id
|
||||
packed_message1 = (packed_message1 << 64) + account_id
|
||||
packed_message1 = (packed_message1 << 64) + account_id
|
||||
packed_message1 = (packed_message1 << 32) + expire_time
|
||||
packed_message1 = packed_message1 << 17 # Padding
|
||||
packed_message1 = packed_message1 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Final hash: hash(msg, packed_message1)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_message1])
|
||||
|
||||
return msg
|
||||
|
||||
def calc_transfer_hash(
|
||||
self,
|
||||
asset_id: int,
|
||||
asset_id_fee: int,
|
||||
receiver_public_key: int,
|
||||
sender_position_id: int,
|
||||
receiver_position_id: int,
|
||||
fee_position_id: int,
|
||||
nonce: int,
|
||||
amount: int,
|
||||
max_amount_fee: int,
|
||||
expiration_timestamp: int
|
||||
) -> bytes:
|
||||
"""
|
||||
Calculate the hash for a transfer using StarkEx protocol.
|
||||
|
||||
Args:
|
||||
asset_id: The asset ID
|
||||
asset_id_fee: The fee asset ID
|
||||
receiver_public_key: The receiver's public key
|
||||
sender_position_id: The sender's position ID
|
||||
receiver_position_id: The receiver's position ID
|
||||
fee_position_id: The fee position ID
|
||||
nonce: The nonce
|
||||
amount: The transfer amount
|
||||
max_amount_fee: The maximum fee amount
|
||||
expiration_timestamp: The expiration timestamp
|
||||
|
||||
Returns:
|
||||
bytes: The calculated hash
|
||||
"""
|
||||
# First hash: hash(asset_id, asset_id_fee)
|
||||
msg = self.signing_adapter.pedersen_hash([asset_id, asset_id_fee])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Second hash: hash(msg, receiver_public_key)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, receiver_public_key])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 0
|
||||
# packed_msg0 = sender_position_id * 2^64 + receiver_position_id * 2^64 + fee_position_id * 2^32 + nonce
|
||||
packed_msg0 = sender_position_id
|
||||
packed_msg0 = (packed_msg0 << 64) + receiver_position_id
|
||||
packed_msg0 = (packed_msg0 << 64) + fee_position_id
|
||||
packed_msg0 = (packed_msg0 << 32) + nonce
|
||||
packed_msg0 = packed_msg0 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Third hash: hash(msg, packed_msg0)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_msg0])
|
||||
msg_int = int.from_bytes(msg, byteorder='big')
|
||||
|
||||
# Pack message 1
|
||||
# packed_msg1 = 4 * 2^64 + amount * 2^64 + max_amount_fee * 2^32 + expiration_timestamp * 2^81
|
||||
packed_msg1 = 4 # Transfer type
|
||||
packed_msg1 = (packed_msg1 << 64) + amount
|
||||
packed_msg1 = (packed_msg1 << 64) + max_amount_fee
|
||||
packed_msg1 = (packed_msg1 << 32) + expiration_timestamp
|
||||
packed_msg1 = packed_msg1 << 81 # Padding
|
||||
packed_msg1 = packed_msg1 % FIELD_PRIME # Ensure within field
|
||||
|
||||
# Final hash: hash(msg, packed_msg1)
|
||||
msg = self.signing_adapter.pedersen_hash([msg_int, packed_msg1])
|
||||
|
||||
return msg
|
||||
|
||||
def get_value(self, data: Union[Dict[str, Any], List[Any], str, int, float, None]) -> str:
|
||||
"""
|
||||
Convert a value to a string representation for signing.
|
||||
This function recursively processes dictionaries, lists, and primitive types.
|
||||
|
||||
Args:
|
||||
data: The value to convert
|
||||
|
||||
Returns:
|
||||
str: The string representation
|
||||
"""
|
||||
if data is None:
|
||||
return ""
|
||||
|
||||
if isinstance(data, str):
|
||||
return data
|
||||
|
||||
if isinstance(data, bool):
|
||||
# Convert boolean to lowercase string to match Go SDK
|
||||
return str(data).lower()
|
||||
|
||||
if isinstance(data, (int, float)):
|
||||
return str(data)
|
||||
|
||||
if isinstance(data, list):
|
||||
if len(data) == 0:
|
||||
return ""
|
||||
values = [self.get_value(item) for item in data]
|
||||
return "&".join(values)
|
||||
|
||||
if isinstance(data, dict):
|
||||
# Convert all values to strings and sort by keys
|
||||
sorted_map = {}
|
||||
for key, val in data.items():
|
||||
sorted_map[key] = self.get_value(val)
|
||||
|
||||
# Get sorted keys
|
||||
keys = sorted(sorted_map.keys())
|
||||
|
||||
# Build key=value pairs
|
||||
pairs = [f"{key}={sorted_map[key]}" for key in keys]
|
||||
return "&".join(pairs)
|
||||
|
||||
# Handle other types by converting to string
|
||||
return str(data)
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Signing adapter interface for the EdgeX Python SDK.
|
||||
|
||||
This module defines the interface for signing adapters that can be used with the SDK.
|
||||
Different implementations can be provided for different environments (development, testing, production).
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Tuple, List
|
||||
|
||||
|
||||
class SigningAdapter(ABC):
|
||||
"""Interface for signing adapters."""
|
||||
|
||||
@abstractmethod
|
||||
def sign(self, message_hash: bytes, private_key: str) -> Tuple[str, str]:
|
||||
"""
|
||||
Sign a message hash using a private key.
|
||||
|
||||
Args:
|
||||
message_hash: The hash of the message to sign
|
||||
private_key: The private key as a hex string
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: The signature as (r, s) hex strings
|
||||
|
||||
Raises:
|
||||
ValueError: If the private key is invalid or the signing fails
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_public_key(self, private_key: str) -> str:
|
||||
"""
|
||||
Get the public key from a private key.
|
||||
|
||||
Args:
|
||||
private_key: The private key as a hex string
|
||||
|
||||
Returns:
|
||||
str: The public key as a hex string
|
||||
|
||||
Raises:
|
||||
ValueError: If the private key is invalid
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def verify(self, message_hash: bytes, signature: Tuple[str, str], public_key: str) -> bool:
|
||||
"""
|
||||
Verify a signature using a public key.
|
||||
|
||||
Args:
|
||||
message_hash: The hash of the message
|
||||
signature: The signature as (r, s) hex strings
|
||||
public_key: The public key as a hex string
|
||||
|
||||
Returns:
|
||||
bool: Whether the signature is valid
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def pedersen_hash(self, elements: List[int]) -> bytes:
|
||||
"""
|
||||
Calculate the Pedersen hash of a list of integers.
|
||||
|
||||
Args:
|
||||
elements: List of integers to hash
|
||||
|
||||
Returns:
|
||||
bytes: The hash result
|
||||
|
||||
Raises:
|
||||
ValueError: If the calculation fails
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,496 @@
|
||||
"""
|
||||
StarkEx signing adapter for the EdgeX Python SDK.
|
||||
|
||||
This module provides an implementation of the signing adapter interface
|
||||
that uses the StarkWare cryptographic primitives for signing operations.
|
||||
"""
|
||||
|
||||
import binascii
|
||||
import math
|
||||
import secrets
|
||||
from typing import List, Tuple
|
||||
|
||||
from .signing_adapter import SigningAdapter
|
||||
from ..crypto.pedersen_hash import pedersen_hash_bytes
|
||||
|
||||
|
||||
# StarkEx curve parameters
|
||||
FIELD_PRIME = 0x800000000000011000000000000000000000000000000000000000000000001
|
||||
ALPHA = 1
|
||||
BETA = 0x6f21413efbe40de150e596d72f7a8c5609ad26c15c915c1f4cdfcb99cee9e89
|
||||
EC_ORDER = 0x800000000000010ffffffffffffffffb781126dcae7b2321e66a241adc64d2f
|
||||
N_ELEMENT_BITS_ECDSA = math.floor(math.log(FIELD_PRIME, 2))
|
||||
assert N_ELEMENT_BITS_ECDSA == 251
|
||||
|
||||
# Generator point for the Stark curve
|
||||
EC_GEN = (
|
||||
0x1ef15c18599971b7beced415a40f0c7deacfd9b0d1819e03d723d8bc943cfca,
|
||||
0x5668060aa49730b7be4801df46ec62de53ecd11abe43a32873000c36e8dc1f
|
||||
)
|
||||
|
||||
|
||||
class StarkExSigningAdapter(SigningAdapter):
|
||||
"""StarkEx implementation of the signing adapter interface."""
|
||||
|
||||
def sign(self, message_hash: bytes, private_key: str) -> Tuple[str, str]:
|
||||
"""
|
||||
Sign a message hash using a private key.
|
||||
|
||||
Args:
|
||||
message_hash: The hash of the message to sign
|
||||
private_key: The private key as a hex string
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: The signature as (r, s) hex strings
|
||||
|
||||
Raises:
|
||||
ValueError: If the private key is invalid or the signing fails
|
||||
"""
|
||||
try:
|
||||
# Validate private key format
|
||||
binascii.unhexlify(private_key)
|
||||
except binascii.Error:
|
||||
raise ValueError("Invalid private key hex string")
|
||||
|
||||
# Convert message hash to integer
|
||||
msg_hash_int = int.from_bytes(message_hash, byteorder='big')
|
||||
|
||||
# Ensure the message hash is in the valid range
|
||||
# Use the same modulus as the Golang SDK (EC_ORDER, which is starkcurve.N)
|
||||
msg_hash_int = msg_hash_int % EC_ORDER
|
||||
|
||||
# Convert private key to integer
|
||||
priv_key_int = int(private_key, 16)
|
||||
|
||||
# Ensure the private key is in the valid range
|
||||
# For testing purposes, we'll just take the modulus
|
||||
priv_key_int = priv_key_int % EC_ORDER
|
||||
if priv_key_int == 0:
|
||||
priv_key_int = 1
|
||||
|
||||
# Sign the message
|
||||
r, s = self._sign(msg_hash_int, priv_key_int)
|
||||
|
||||
# Convert r and s to hex strings
|
||||
r_hex = format(r, '064x')
|
||||
s_hex = format(s, '064x')
|
||||
|
||||
return r_hex, s_hex
|
||||
|
||||
def get_public_key(self, private_key: str) -> str:
|
||||
"""
|
||||
Get the public key from a private key.
|
||||
|
||||
Args:
|
||||
private_key: The private key as a hex string
|
||||
|
||||
Returns:
|
||||
str: The public key as a hex string
|
||||
|
||||
Raises:
|
||||
ValueError: If the private key is invalid
|
||||
"""
|
||||
try:
|
||||
# Validate private key format
|
||||
binascii.unhexlify(private_key)
|
||||
except binascii.Error:
|
||||
raise ValueError("Invalid private key hex string")
|
||||
|
||||
# Convert private key to integer
|
||||
priv_key_int = int(private_key, 16)
|
||||
|
||||
# Ensure the private key is in the valid range
|
||||
# For testing purposes, we'll just take the modulus
|
||||
priv_key_int = priv_key_int % EC_ORDER
|
||||
if priv_key_int == 0:
|
||||
priv_key_int = 1
|
||||
|
||||
# Get the public key
|
||||
public_key = self._private_to_stark_key(priv_key_int)
|
||||
|
||||
# Convert public key to hex string
|
||||
public_key_hex = format(public_key, '064x')
|
||||
|
||||
return public_key_hex
|
||||
|
||||
def verify(self, message_hash: bytes, signature: Tuple[str, str], public_key: str) -> bool:
|
||||
"""
|
||||
Verify a signature using a public key.
|
||||
|
||||
Args:
|
||||
message_hash: The hash of the message
|
||||
signature: The signature as (r, s) hex strings
|
||||
public_key: The public key as a hex string
|
||||
|
||||
Returns:
|
||||
bool: Whether the signature is valid
|
||||
"""
|
||||
try:
|
||||
# Convert message hash to integer
|
||||
msg_hash_int = int.from_bytes(message_hash, byteorder='big')
|
||||
|
||||
# Ensure the message hash is in the valid range
|
||||
# Use the same modulus as the sign method (EC_ORDER)
|
||||
msg_hash_int = msg_hash_int % EC_ORDER
|
||||
|
||||
# Convert signature components to integers
|
||||
r_int = int(signature[0], 16)
|
||||
s_int = int(signature[1], 16)
|
||||
|
||||
# Ensure r and s are in the valid range
|
||||
if not (1 <= r_int < 2**N_ELEMENT_BITS_ECDSA and 1 <= s_int < EC_ORDER):
|
||||
return False
|
||||
|
||||
# Convert public key to integer
|
||||
pub_key_int = int(public_key, 16)
|
||||
|
||||
# Verify the signature
|
||||
return self._verify(msg_hash_int, r_int, s_int, pub_key_int)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def pedersen_hash(self, elements: List[int]) -> bytes:
|
||||
"""
|
||||
Calculate the Pedersen hash of a list of integers.
|
||||
|
||||
This method now uses the full Pedersen hash implementation
|
||||
that follows StarkWare's specification.
|
||||
|
||||
Args:
|
||||
elements: List of integers to hash
|
||||
|
||||
Returns:
|
||||
bytes: The hash result
|
||||
|
||||
Raises:
|
||||
ValueError: If the calculation fails
|
||||
"""
|
||||
try:
|
||||
# Use the full Pedersen hash implementation
|
||||
return pedersen_hash_bytes(*elements)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to calculate Pedersen hash: {str(e)}")
|
||||
|
||||
def _sign(self, msg_hash: int, priv_key: int) -> Tuple[int, int]:
|
||||
"""
|
||||
Sign a message hash using a private key.
|
||||
|
||||
Args:
|
||||
msg_hash: The hash of the message to sign as an integer
|
||||
priv_key: The private key as an integer
|
||||
|
||||
Returns:
|
||||
Tuple[int, int]: The signature as (r, s) integers
|
||||
"""
|
||||
# Choose a valid k. In our version of ECDSA not every k value is valid,
|
||||
# and there is a negligible probability a drawn k cannot be used for signing.
|
||||
# This is why we have this loop.
|
||||
while True:
|
||||
# Use random nonce generation like the Go SDK
|
||||
k = self._generate_random_k()
|
||||
|
||||
# Cannot fail because 0 < k < EC_ORDER and EC_ORDER is prime.
|
||||
x = self._ec_mult(k, EC_GEN)[0]
|
||||
|
||||
# DIFF: in classic ECDSA, we take int(x) % n.
|
||||
r = int(x)
|
||||
if not (1 <= r < 2**N_ELEMENT_BITS_ECDSA):
|
||||
# Bad value. This fails with negligible probability.
|
||||
continue
|
||||
|
||||
if (msg_hash + r * priv_key) % EC_ORDER == 0:
|
||||
# Bad value. This fails with negligible probability.
|
||||
continue
|
||||
|
||||
w = self._div_mod(k, msg_hash + r * priv_key, EC_ORDER)
|
||||
if not (1 <= w < 2**N_ELEMENT_BITS_ECDSA):
|
||||
# Bad value. This fails with negligible probability.
|
||||
continue
|
||||
|
||||
s = self._inv_mod_curve_size(w)
|
||||
return r, s
|
||||
|
||||
def _verify(self, msg_hash: int, r: int, s: int, public_key: int) -> bool:
|
||||
"""
|
||||
Verify a signature using a public key.
|
||||
|
||||
Args:
|
||||
msg_hash: The hash of the message as an integer
|
||||
r: The r component of the signature as an integer
|
||||
s: The s component of the signature as an integer
|
||||
public_key: The public key as an integer
|
||||
|
||||
Returns:
|
||||
bool: Whether the signature is valid
|
||||
"""
|
||||
# Compute w = s^-1 (mod EC_ORDER).
|
||||
if not (1 <= s < EC_ORDER):
|
||||
return False
|
||||
|
||||
w = self._inv_mod_curve_size(s)
|
||||
|
||||
# Preassumptions:
|
||||
# DIFF: in classic ECDSA, we assert 1 <= r, w <= EC_ORDER-1.
|
||||
# Since r, w < 2**N_ELEMENT_BITS_ECDSA < EC_ORDER, we only need to verify r, w != 0.
|
||||
if not (1 <= r < 2**N_ELEMENT_BITS_ECDSA and 1 <= w < 2**N_ELEMENT_BITS_ECDSA):
|
||||
return False
|
||||
|
||||
if not (0 <= msg_hash < 2**N_ELEMENT_BITS_ECDSA):
|
||||
return False
|
||||
|
||||
# Only the x coordinate of the point is given, check the two possibilities for the y
|
||||
# coordinate.
|
||||
try:
|
||||
y = self._get_y_coordinate(public_key)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
# Verify it is on the curve.
|
||||
if (y**2 - (public_key**3 + ALPHA * public_key + BETA)) % FIELD_PRIME != 0:
|
||||
return False
|
||||
|
||||
# Try both possible y coordinates.
|
||||
for y_candidate in [y, (-y) % FIELD_PRIME]:
|
||||
public_key_point = (public_key, y_candidate)
|
||||
|
||||
# Signature validation.
|
||||
try:
|
||||
# Calculate u1 = msg_hash * w mod n
|
||||
u1 = (msg_hash * w) % EC_ORDER
|
||||
|
||||
# Calculate u2 = r * w mod n
|
||||
u2 = (r * w) % EC_ORDER
|
||||
|
||||
# Calculate u1*G + u2*Q
|
||||
point1 = self._ec_mult(u1, EC_GEN)
|
||||
point2 = self._ec_mult(u2, public_key_point)
|
||||
point = self._ec_add(point1, point2)
|
||||
|
||||
# The signature is valid if the x-coordinate of the resulting point equals r
|
||||
if point[0] == r:
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
def _generate_random_k(self) -> int:
|
||||
"""
|
||||
Generate a cryptographically secure random k value.
|
||||
|
||||
Returns:
|
||||
int: The generated k value in range [1, EC_ORDER)
|
||||
"""
|
||||
# Generate a cryptographically secure random number in the range [1, EC_ORDER)
|
||||
# This matches the Go implementation's approach of using random nonces
|
||||
return secrets.randbelow(EC_ORDER - 1) + 1
|
||||
|
||||
def _private_to_stark_key(self, priv_key: int) -> int:
|
||||
"""
|
||||
Convert a private key to a Stark public key.
|
||||
|
||||
Args:
|
||||
priv_key: The private key as an integer
|
||||
|
||||
Returns:
|
||||
int: The public key as an integer
|
||||
"""
|
||||
return self._private_key_to_ec_point_on_stark_curve(priv_key)[0]
|
||||
|
||||
def _private_key_to_ec_point_on_stark_curve(self, priv_key: int) -> Tuple[int, int]:
|
||||
"""
|
||||
Convert a private key to an EC point on the Stark curve.
|
||||
|
||||
Args:
|
||||
priv_key: The private key as an integer
|
||||
|
||||
Returns:
|
||||
Tuple[int, int]: The EC point as (x, y) coordinates
|
||||
"""
|
||||
# Ensure the private key is in the valid range
|
||||
# For testing purposes, we'll just take the modulus
|
||||
priv_key = priv_key % EC_ORDER
|
||||
if priv_key == 0:
|
||||
priv_key = 1
|
||||
|
||||
return self._ec_mult(priv_key, EC_GEN)
|
||||
|
||||
def _inv_mod_curve_size(self, x: int) -> int:
|
||||
"""
|
||||
Calculate the modular inverse of x modulo the curve order.
|
||||
|
||||
Args:
|
||||
x: The value to invert
|
||||
|
||||
Returns:
|
||||
int: The modular inverse
|
||||
"""
|
||||
return self._div_mod(1, x, EC_ORDER)
|
||||
|
||||
def _div_mod(self, n: int, m: int, p: int) -> int:
|
||||
"""
|
||||
Calculate (n / m) mod p.
|
||||
|
||||
Args:
|
||||
n: The numerator
|
||||
m: The denominator
|
||||
p: The modulus
|
||||
|
||||
Returns:
|
||||
int: The result of the division modulo p
|
||||
"""
|
||||
return (n * pow(m, -1, p)) % p
|
||||
|
||||
def _is_quad_residue(self, n: int, p: int) -> bool:
|
||||
"""
|
||||
Check if n is a quadratic residue modulo p.
|
||||
|
||||
Args:
|
||||
n: The number to check
|
||||
p: The modulus
|
||||
|
||||
Returns:
|
||||
bool: True if n is a quadratic residue modulo p, False otherwise
|
||||
"""
|
||||
return pow(n, (p - 1) // 2, p) == 1
|
||||
|
||||
def _sqrt_mod(self, n: int, p: int) -> int:
|
||||
"""
|
||||
Calculate the square root of n modulo p.
|
||||
|
||||
Args:
|
||||
n: The number to take the square root of
|
||||
p: The modulus
|
||||
|
||||
Returns:
|
||||
int: The square root of n modulo p
|
||||
"""
|
||||
# Handle the case where p = 3 mod 4
|
||||
if p % 4 == 3:
|
||||
return pow(n, (p + 1) // 4, p)
|
||||
|
||||
# Handle the general case using the Tonelli-Shanks algorithm
|
||||
q = p - 1
|
||||
s = 0
|
||||
while q % 2 == 0:
|
||||
q //= 2
|
||||
s += 1
|
||||
|
||||
# Find a non-residue
|
||||
z = 2
|
||||
while self._is_quad_residue(z, p):
|
||||
z += 1
|
||||
|
||||
m = s
|
||||
c = pow(z, q, p)
|
||||
t = pow(n, q, p)
|
||||
r = pow(n, (q + 1) // 2, p)
|
||||
|
||||
while t != 1:
|
||||
# Find the least i, 0 < i < m, such that t^(2^i) = 1
|
||||
i = 0
|
||||
t_sq = t
|
||||
while t_sq != 1 and i < m - 1:
|
||||
t_sq = (t_sq * t_sq) % p
|
||||
i += 1
|
||||
|
||||
# Calculate b = c^(2^(m-i-1))
|
||||
b = pow(c, 2**(m - i - 1), p)
|
||||
|
||||
m = i
|
||||
c = (b * b) % p
|
||||
t = (t * b * b) % p
|
||||
r = (r * b) % p
|
||||
|
||||
return r
|
||||
|
||||
def _get_y_coordinate(self, x: int) -> int:
|
||||
"""
|
||||
Given the x coordinate of a point, returns a possible y coordinate such that
|
||||
together the point (x,y) is on the curve.
|
||||
|
||||
Args:
|
||||
x: The x coordinate
|
||||
|
||||
Returns:
|
||||
int: A possible y coordinate
|
||||
|
||||
Raises:
|
||||
ValueError: If x is not a valid x coordinate on the curve
|
||||
"""
|
||||
y_squared = (x * x * x + ALPHA * x + BETA) % FIELD_PRIME
|
||||
if not self._is_quad_residue(y_squared, FIELD_PRIME):
|
||||
raise ValueError("Given x coordinate does not represent any point on the elliptic curve.")
|
||||
|
||||
return self._sqrt_mod(y_squared, FIELD_PRIME)
|
||||
|
||||
def _ec_add(self, p1: Tuple[int, int], p2: Tuple[int, int]) -> Tuple[int, int]:
|
||||
"""
|
||||
Add two points on the elliptic curve.
|
||||
|
||||
Args:
|
||||
p1: The first point as (x, y) coordinates
|
||||
p2: The second point as (x, y) coordinates
|
||||
|
||||
Returns:
|
||||
Tuple[int, int]: The resulting point as (x, y) coordinates
|
||||
"""
|
||||
if p1[0] == p2[0]:
|
||||
if (p1[1] + p2[1]) % FIELD_PRIME == 0:
|
||||
# The points are negatives of each other, return the point at infinity
|
||||
# We represent the point at infinity as None, but this should never happen
|
||||
# in our use case, so we raise an exception instead
|
||||
raise ValueError("Points are negatives of each other")
|
||||
|
||||
# The points are the same, so we're doubling
|
||||
return self._ec_double(p1)
|
||||
|
||||
# Calculate the slope
|
||||
slope = self._div_mod(p2[1] - p1[1], p2[0] - p1[0], FIELD_PRIME)
|
||||
|
||||
# Calculate the new point
|
||||
x3 = (slope * slope - p1[0] - p2[0]) % FIELD_PRIME
|
||||
y3 = (slope * (p1[0] - x3) - p1[1]) % FIELD_PRIME
|
||||
|
||||
return (x3, y3)
|
||||
|
||||
def _ec_double(self, p: Tuple[int, int]) -> Tuple[int, int]:
|
||||
"""
|
||||
Double a point on the elliptic curve.
|
||||
|
||||
Args:
|
||||
p: The point to double as (x, y) coordinates
|
||||
|
||||
Returns:
|
||||
Tuple[int, int]: The resulting point as (x, y) coordinates
|
||||
"""
|
||||
# Calculate the slope
|
||||
slope = self._div_mod(3 * p[0] * p[0] + ALPHA, 2 * p[1], FIELD_PRIME)
|
||||
|
||||
# Calculate the new point
|
||||
x3 = (slope * slope - 2 * p[0]) % FIELD_PRIME
|
||||
y3 = (slope * (p[0] - x3) - p[1]) % FIELD_PRIME
|
||||
|
||||
return (x3, y3)
|
||||
|
||||
def _ec_mult(self, m: int, p: Tuple[int, int]) -> Tuple[int, int]:
|
||||
"""
|
||||
Multiply a point on the elliptic curve by a scalar.
|
||||
|
||||
Args:
|
||||
m: The scalar
|
||||
p: The point as (x, y) coordinates
|
||||
|
||||
Returns:
|
||||
Tuple[int, int]: The resulting point as (x, y) coordinates
|
||||
"""
|
||||
if m == 0:
|
||||
raise ValueError("Cannot multiply by 0")
|
||||
|
||||
if m == 1:
|
||||
return p
|
||||
|
||||
if m % 2 == 0:
|
||||
return self._ec_mult(m // 2, self._ec_double(p))
|
||||
else:
|
||||
return self._ec_add(p, self._ec_mult(m - 1, p))
|
||||
Reference in New Issue
Block a user