Я создаю платформу, которая интегрируется с безгазовым релейным устройством Polymarket (
Код: Выделить всё
https://relayer-v2.polymarket.com/submit- Безопасный прокси-кошелек Gnosis (развернутый через завод Polymarket 0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b)
- Privy в качестве ключа хранитель (нет доступа к необработанному закрытому ключу)
- [/b] (официальный SDK Polymarket)
Код: Выделить всё
py-builder-relayer-client==0.0.1 - [/b] для аутентификации HMAC
Код: Выделить всё
py-builder-signing-sdk
Что подтверждено, работает
- → 200 (учетные данные Builder действительны)
Код: Выделить всё
GET /transactions - → {"deployed": true (Safe развертывается на Polygon)
Код: Выделить всё
GET /deployed?address=... - → {"nonce": "0" (исправный текущий nonce)
Код: Выделить всё
GET /nonce - с тестовым закрытым ключом (различный EOA/Safe) → 200 OK
Код: Выделить всё
client.execute() - , полученный с помощью SDK, соответствует развернутому Safe on-chain
Код: Выделить всё
proxyWallet
Когда я внедряю собственный PrivySigner (который вызывает API Privy вместо использования локального закрытого ключа), POST /submit последовательно возвращает:
Код: Выделить всё
{"error": "bad request"}
Класс Signer SDK подписывает следующим образом:
Код: Выделить всё
# SDK original (py-builder-relayer-client/signer.py)
def sign_eip712_struct_hash(self, message_hash):
msg = encode_defunct(HexBytes(message_hash)) # applies EIP-191 prefix
sig = Account.sign_message(msg, self.private_key).signature.hex()
return prepend_zx(sig)
Код: Выделить всё
class PrivySigner:
def __init__(self, privy_wallet_id: str, eoa_address: str, chain_id: int = 137):
self.privy_wallet_id = privy_wallet_id
self._address = to_checksum_address(eoa_address)
def address(self) -> str:
return self._address
def get_chain_id(self) -> int:
return self._chain_id
def sign(self, message_hash) -> str:
h = "0x" + message_hash.hex() if isinstance(message_hash, bytes) else str(message_hash)
return _run_sync(_privy_sign_raw(self.privy_wallet_id, h))
def sign_eip712_struct_hash(self, message_hash) -> str:
h = "0x" + message_hash.hex() if isinstance(message_hash, bytes) else str(message_hash)
# Using personal_sign with encoding=hex — intended to match encode_defunct behavior
sig_hex = _run_sync(_privy_sign_eip191(self.privy_wallet_id, h))
return sig_hex
Код: Выделить всё
async def _privy_sign_eip191(privy_wallet_id: str, hash_hex: str) -> str:
payload = {
"method": "personal_sign",
"params": {"message": hash_hex, "encoding": "hex"},
}
# ... POST to Privy API
Я провел тест шифрования по хэшу структуры, вычисленному SDK:
Код: Выделить всё
from py_builder_relayer_client.builder.safe import create_struct_hash
from eth_account import Account
from eth_account.messages import encode_defunct
from hexbytes import HexBytes
struct_hash = "0x918fb835628db28a00da7606c72e46c127d45b1a7f0827499d0f5091c3d65185"
sig_from_privy_personal_sign = "0xd9965d01...1c"
# Test 1: HexBytes (same as SDK)
msg = encode_defunct(HexBytes(struct_hash))
recovered = Account.recover_message(msg, signature=sig_from_privy_personal_sign)
print(recovered) # → 0xeBcC372BA40bF88e730e0C901114713428B20f49 ← WRONG
# Test 2: secp256k1_sign (raw, no EIP-191 prefix)
sig_raw = "0x3ea38967...1c" # from Privy secp256k1_sign
recovered2 = Account._recover_hash(HexBytes(struct_hash), signature=sig_raw)
print(recovered2) # → 0xD15b9f4Dab19808eb4F25AB647820cD8111cE1ef ← CORRECT
Вопрос
Контракт Gnosis Safe (
Код: Выделить всё
GnosisSafeL2 v1.3.0Код: Выделить всё
v = 31Код: Выделить всё
currentOwner = ecrecover(
keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)),
v - 4, r, s
);
Но Sign_eip712_struct_hash SDK использует encode_defunct (который добавляет префикс EIP-191 перед подписанием), а затем Split_and_pack_sig устанавливает v = v_raw + 4 (выдает v=31/32). Это будет означать, что Safe вычитает 4, получает v=27/28 и снова применяет префикс, что приводит к ситуации с двойным префиксом.
Почему SDK работает с обычным закрытым ключом, но создает неверные подписи, когда префикс EIP-191 применяется через личный_знак Privy?
Правильно ли использовать secp256k1_sign (необработанный хеш, без префикса), а затем установить v = v_raw + 27 перед передачей в Split_and_pack_sig? Или есть что-то еще в том, как Privy обрабатывает комбинацию Personal_sign + кодирование: hex, которая отличается от eth_account.encode_defunct?
Среда
- Python 3.12
Код: Выделить всё
py-builder-relayer-client==0.0.1Код: Выделить всё
eth-account==0.13.4- Polygon (идентификатор цепи 137)
- Gnosis Safe v1.3.0 (GnosisSafeL2)
- Приватный серверный API (кастодиальный кошелек)
Подробнее: https://stackoverflow.com/questions/799 ... nosis-safe
Мобильная версия