第 8 章 · 03 VersionedTransaction 签名流程(ultra_tool...


文档摘要

第 8 章 · 03 VersionedTransaction 签名流程(ultratools 精读) 本节摘要:本节是 AutoHedge 全教程技术密度最高的部分,逐行精读 里的 函数——这是全项目唯一真实动钱的代码。它完成 Solana 链上交易的最后一步:把 Jupiter 返回的未签名交易,用你本地私钥签名,再广播上链。核心是用 solders 库操作 VersionedTransaction(版本化交易):从 base64 解码 → 还原交易 → 取待签消息 → 签名 → 填入签名重新组装 → base64 编码 → POST 给 Jupiter /execute。读完本节,你彻底理解 Solana 交易签名的工程实现,以及「本地签名、远程广播」这一 DeFi 安全模式。

第 8 章 · 03 VersionedTransaction 签名流程(ultra_tools 精读)

本节摘要:本节是 AutoHedge 全教程技术密度最高的部分,逐行精读 autohedge/tools/ultra_tools.py 里的 execute_trade 函数——这是全项目唯一真实动钱的代码。它完成 Solana 链上交易的最后一步:把 Jupiter 返回的未签名交易,用你本地私钥签名,再广播上链。核心是用 solders 库操作 VersionedTransaction(版本化交易):从 base64 解码 → from_bytes 还原交易 → to_bytes_versioned 取待签消息 → sign_message 签名 → populate 填入签名重新组装 → base64 编码 → POST 给 Jupiter /execute。读完本节,你彻底理解 Solana 交易签名的工程实现,以及「本地签名、远程广播」这一 DeFi 安全模式。

内容来源:原项目源码 autohedge/tools/ultra_tools.py,逐行精读并套用体系化模板。

⚠️ 风险提示:本节涉及私钥与真实链上交易。务必用测试钱包 + 测试网。私钥(SOLANA_PRIVATE_KEY)一旦泄露,钱包资产即刻清零。AutoHedge 的风控极不成熟,切勿用主资产钱包跑这套代码。

学习目标

阅读完本节,你应当能够:

  1. 逐行读懂 execute_trade 函数的全部代码。
  2. 说清 VersionedTransaction 签名的五步流程(解码→还原→取消息→签名→重组)。
  3. 理解 solders 库的 Keypairfrom_bytesto_bytes_versionedsign_messagepopulate 各自作用。
  4. 解释「本地签名 + 远程广播」为何是 DeFi 的安全范式。

一、execute_trade 的完整代码

先把 execute_trade(ultra_tools.py 第 158-238 行)完整呈现:

def execute_trade(unsigned_transaction: str, request_id: str) -> str: if not unsigned_transaction or not unsigned_transaction.strip(): raise ValueError("unsigned_transaction is required") if not request_id or not request_id.strip(): raise ValueError("request_id is required") keypair = _get_keypair() # ① 从 .env 私钥恢复 Keypair tx_b64 = unsigned_transaction.strip() try: tx_bytes = base64.b64decode(tx_b64) # ② base64 解码为原始字节 except Exception as e: raise ValueError(f"unsigned_transaction is not valid base64: {e}") from e try: raw_tx = VersionedTransaction.from_bytes(tx_bytes) # ③ 还原为交易对象 except Exception as e: raise ValueError(f"unsigned_transaction is not a valid versioned transaction: {e}") from e message_bytes = to_bytes_versioned(raw_tx.message) # ④ 取待签名的消息字节 signature = keypair.sign_message(message_bytes) # ⑤ 用私钥签名 signed_tx = VersionedTransaction.populate( # ⑥ 用签名重组交易 raw_tx.message, [signature] ) signed_b64 = base64.b64encode(bytes(signed_tx)).decode("ascii") # ⑦ 重新 base64 编码 url = f"{JUPITER_ULTRA_BASE}/execute" payload = {"signedTransaction": signed_b64, "requestId": request_id.strip()} try: with httpx.Client(timeout=30) as client: resp = client.post(url, json=payload, headers=_headers() or None) # ⑧ POST 广播 resp.raise_for_status() return json.dumps(resp.json()) except httpx.HTTPError as e: logger.error(f"Jupiter Ultra execute request failed: {e}") raise

整个函数的逻辑链:未签名交易字符串 + request_id签名后的交易POST 广播返回结果 JSON。下面把关键步骤拆开讲。

二、前提:私钥如何变成 Keypair

签名前,要先从 .env 的私钥恢复出 Keypair 对象。这是 _get_keypair() 做的(第 39-67 行):

def _get_keypair() -> Keypair: raw = os.getenv("SOLANA_PRIVATE_KEY") if not raw or not raw.strip(): raise ValueError("SOLANA_PRIVATE_KEY is required in .env to sign transactions") try: return Keypair.from_base58_string(raw.strip()) # base58 私钥 → Keypair except Exception as e: raise ValueError(f"Invalid SOLANA_PRIVATE_KEY in .env: {e}") from e

要点:

  • Solana 私钥是 base58 编码的字符串(不是十六进制)。
  • Keypair.from_base58_string() 把它转成 Keypair 对象,这个对象同时持有公钥与私钥,能用于签名。
  • 公钥可由 keypair.pubkey() 派生(get_wallet_pubkey 就是这么做的)。

💡 核心心法:Keypair 是「公私钥对」的封装——公钥用于验签和地址,私钥用于签名。Solana 的设计是「公钥即地址」,所以知道公钥就知道钱包地址,但只有持有私钥才能动用里面的资产。

三、五步签名流程详解

② base64 解码

tx_bytes = base64.b64decode(tx_b64)

Jupiter 的 /order 端点返回的交易是 base64 编码的字节串(便于在 JSON/HTTP 里传输)。第一步先把它解码回原始字节。base64 不是加密,只是编码——任何拿到这串的人都能解码,它此时还未签名,所以不含你的授权。

③ 还原为 VersionedTransaction

raw_tx = VersionedTransaction.from_bytes(tx_bytes)

把字节还原成 solders 的 VersionedTransaction 对象。这里的关键概念是 Versioned Transaction(版本化交易)——Solana 的新版交易格式,支持 Address Lookup Table(地址查找表),比 legacy 格式更省空间、能装更多账户。

💡 为什么是 VersionedTransaction 而不是 Transaction:Jupiter 这类聚合器返回的几乎都是版本化交易(因为路由涉及大量账户,legacy 格式装不下)。所以代码用 VersionedTransaction 而非旧的 Transaction,这是对的选择。

④ 取待签名消息

message_bytes = to_bytes_versioned(raw_tx.message)

签名时,你只签「消息」部分,不签整个交易raw_tx.message 是交易的「内容」(付款方、收款方、金额、指令等),to_bytes_versioned() 把它序列化成用于签名的规范字节。

这一步至关重要:它确保签名覆盖的内容是确定的——链上验签时,节点会用同样的序列化方式还原消息,再用你的公钥验签。任何对消息的篡改都会让签名失效。

⑤ 用私钥签名

signature = keypair.sign_message(message_bytes)

用 Keypair 里的私钥,对消息字节做Ed25519 签名(Solana 用的签名算法)。产出 signature 对象。注意:

  • 签名是对消息的承诺,证明「持有私钥的人认可这段消息」。
  • 签名不暴露私钥(Ed25519 是安全的,无法从签名反推私钥)。

⑥ 重组已签名交易

signed_tx = VersionedTransaction.populate(raw_tx.message, [signature])

把「消息 + 签名」重新组装成一个完整的已签名交易对象populate 接受消息和一个签名列表(因为交易可能需要多个签名者,这里只有一个 payer)。

至此,signed_tx 就是一笔合法的、可广播的交易——它有内容、有你的授权签名。

⑦ 重新编码

signed_b64 = base64.b64encode(bytes(signed_tx)).decode("ascii")

把已签名交易序列化回 base64 字符串,准备发给 Jupiter。

四、广播:POST 给 Jupiter /execute

url = f"{JUPITER_ULTRA_BASE}/execute" # https://api.jup.ag/ultra/v1/execute payload = {"signedTransaction": signed_b64, "requestId": request_id.strip()} with httpx.Client(timeout=30) as client: resp = client.post(url, json=payload, headers=_headers() or None) resp.raise_for_status() return json.dumps(resp.json())

注意几个细节:

  • 超时 30 秒:链上交易确认可能较慢,比 get_order 的 15 秒更长。
  • requestId:必须和 get_order 返回的一致——Jupiter 用它关联「这笔签名交易对应哪个报价」,防止你拿旧报价套用。
  • 返回值:含 status(Success/Failed)signature(链上交易哈希)slottotalInputAmount/totalOutputAmount 等,可用于核对成交。

五、本地签名 + 远程广播:为何安全

这套「Jupiter 给未签名交易 → 本地签名 → 交给 Jupiter 广播」的模式,是 DeFi 的安全范式:

传统 CEX 模式 AutoHedge/Jupiter 模式
把 API key 交给服务器,服务器代你下单 私钥永不离开本地,只把签名后的交易交出去
服务器被攻破=资产丢失 服务器(Jupiter)被攻破,没有你的私钥,无法伪造你的新交易
信任服务器不乱下单 信任只限于「这笔已签名的交易」,你签什么它才能发什么

💡 核心心法:私钥签名是不可伪造的授权。只要私钥不出本地,任何中间方都无法伪造你的交易——这是加密签名给 DeFi 带来的根本安全保证。代价是:私钥一旦本地泄露,安全保证立刻失效。所以密钥管理是这套模式的生命线。

六、整条链路串起来

把 get_order 与 execute_trade 串起来,完整流程:

关键安全点:从 get_order 拿到的未签名交易,在被你签名之前,对链上无效;只有你用私钥签了名,它才成为你的授权。所以即使 get_order 返回的交易被篡改,你的签名也只覆盖你看到的(实际上篡改会让签名失效,广播会被拒)。

七、常见坑与注意事项

  1. 私钥格式:必须是 base58 的完整字符串,多一个空格都可能解析失败(代码已 .strip(),但格式错仍报错)。
  2. amount 单位:get_order 的 amount 是最小单位(SOL 是 lamports,1 SOL = 10^9 lamports),不是人类可读的整数。
  3. mint 地址要全:不能写 SOLUSDC,必须写完整 mint 地址(如 SOL 是 So111...11112,USDC 是 EPjF...Dt1v)。
  4. requestId 配对:execute 的 requestId 必须用 get_order 返回的那个,不能用别的。
  5. 报价时效:Jupiter 报价有有效期,签名太晚提交可能因价格变动被拒(滑点保护)。

⚠️ 现实澄清:这套签名代码本身是正确且可用的——这是 AutoHedge 项目里最货真价实的部分。但请注意:它没有任何前置风控(不检查金额上限、不检查目标合约是否安全、不防夹子)。Agent 一旦决定交易,直接就签了。生产环境必须在这之前加风控闸门。

本节要点回顾

  1. execute_trade 全貌:未签名交易 + requestId → 私钥签名 → POST 广播 → 成交结果;是项目唯一真实动钱的代码。
  2. 私钥恢复:Keypair.from_base58_string(SOLANA_PRIVATE_KEY),Keypair 同时持有公钥(地址)与私钥(签名能力)。
  3. 五步签名:base64 解码 → VersionedTransaction.from_bytes 还原 → to_bytes_versioned 取消息字节 → sign_message 签名 → populate 重组。
  4. 只签消息不签整交易:签名覆盖的是规范化的消息字节,确保链上验签可复现;Ed25519 签名不泄露私钥。
  5. 广播细节:POST /execute 带 signedTransactionrequestId(须配对),超时 30 秒,返回含链上 signature 与成交金额。
  6. 安全范式:私钥不离开本地,只交签名后的交易——Jupiter 被攻破也无法伪造你的新交易;代价是私钥管理是生命线。
  7. 真实可用但无风控:签名代码正确可用,但缺前置风控闸门,生产必须补。

下一节,我们把 get_order + execute_trade + get_holdings 串成完整实盘链路,看 Agent 如何触发一次真实交易。


发布者: 作者: 灏天文库 转发
评论区 (0)
U