You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Gnosis Safe+Privy托管钱包调用Polymarket Relayer v2 POST /submit返回400 bad request的问题排查咨询

Gnosis Safe+Privy托管钱包调用Polymarket Relayer v2 POST /submit返回400 bad request的问题排查咨询

背景概述

我正在开发一个集成Polymarket Gasless Relayer的平台,技术栈包括:

  • Gnosis Safe代理钱包(通过Polymarket的工厂合约0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b部署)
  • Privy作为密钥托管方(无法获取原始私钥)
  • 官方SDK py-builder-relayer-client==0.0.1
  • py-builder-signing-sdk 用于HMAC认证

目标是批量提交6笔ERC-20/ERC-1155授权交易到Relayer,实现Gasless操作。

已确认正常的环节

我已经验证了以下所有流程都能正常工作:

  • 调用GET /transactions返回200,说明Builder凭证有效
  • 调用GET /deployed?address=...返回{"deployed": true},确认Safe已在Polygon链上部署
  • 调用GET /nonce返回{"nonce": "0"},非ce值正确
  • 使用测试私钥(另一EOA/Safe)调用client.execute()返回200 OK
  • SDK推导的proxyWallet地址和链上部署的Safe地址完全匹配

遇到的问题

当我注入自定义的PrivySigner(调用Privy API而非本地私钥签名)后,调用POST /submit始终返回{"error": "bad request"}

签名逻辑对比

SDK原生Signer实现

SDK的默认Signer类签名逻辑如下:

# SDK original (py-builder-relayer-client/signer.py)
def sign_eip712_struct_hash(self, message_hash):
    msg = encode_defunct(HexBytes(message_hash))  # 自动添加EIP-191前缀
    sig = Account.sign_message(msg, self.private_key).signature.hex()
    return prepend_zx(sig)

它会先通过encode_defunct给消息哈希加上EIP-191前缀,再用本地私钥签名。

我的PrivySigner实现

我替换为调用Privy API的实现:

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)
        self._chain_id = chain_id

    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)
        # 尝试用Privy的personal_sign+hex编码来匹配encode_defunct的行为
        sig_hex = _run_sync(_privy_sign_eip191(self.privy_wallet_id, h))
        return sig_hex

其中_privy_sign_eip191的实现是调用Privy的API:

async def _privy_sign_eip191(privy_wallet_id: str, hash_hex: str) -> str:
    payload = {
        "method": "personal_sign",
        "params": {"message": hash_hex, "encoding": "hex"},
    }
    # 此处省略POST到Privy API的逻辑

诊断结果

我通过ecrecover测试验证了签名的有效性:

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"  # 替换为实际签名

# 测试1:模拟SDK的encode_defunct逻辑
msg = encode_defunct(HexBytes(struct_hash))
recovered = Account.recover_message(msg, signature=sig_from_privy_personal_sign)
print(recovered)  # → 0xeBcC372BA40bF88e730e0C901114713428B20f49  ← 错误地址

# 测试2:直接用原始哈希签名(无EIP-191前缀)
sig_raw = "0x3ea38967...1c"  # 来自Privy的secp256k1_sign接口
recovered2 = Account._recover_hash(HexBytes(struct_hash), signature=sig_raw)
print(recovered2)  # → 0xD15b9f4Dab19808eb4F25AB647820cD8111cE1ef  ← 正确地址

结论:

  • Privy的personal_sign+encoding: hex无法复现eth_accountencode_defunct(HexBytes(hash))的签名效果
  • 直接调用Privy的secp256k1_sign(原始哈希,无前缀)可以恢复出正确的EOA地址

核心疑问

我研究了Gnosis Safe L2 v1.3.0合约的checkSignatures逻辑,对于EOA签名(v=31或v=32),合约内部会执行:

currentOwner = ecrecover(
    keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)),
    v - 4, r, s
);

也就是说,Safe合约会自动给dataHash加上EIP-191前缀再做ecrecover,并且会把v值减4(从31/32变回27/28)。

但SDK的sign_eip712_struct_hash逻辑是:先通过encode_defunct加EIP-191前缀,再签名,然后split_and_pack_sig会把v值加4(得到31/32)。这就会导致Safe合约再次添加前缀,出现双重前缀的问题?

但为什么用本地私钥时SDK能正常工作,而用Privy的personal_sign添加前缀就会失败?

正确的处理方式应该是:调用Privy的secp256k1_sign(原始哈希,无前缀),然后手动把v值加27再传给SDK的签名处理逻辑吗?还是说Privy的personal_sign+encoding: hex的参数有什么我没注意到的细节?

火山引擎 最新活动