跳到主要内容

链上结算

状态:已实现。 由 Rocky 运营按交易场所逐个启用;启用前每笔成交的 settlementStatus 均为 "internal"。

Rocky 在链下撮合现货订单,并在 Canton Network 上完成结算。对于两个客户账户之间的每一笔成交,两种资产在同一笔原子 Canton 交易中——即券款对付(DvP)——直接在双方各自的钱包之间易手。每笔已结算的成交都会产生一个 Canton updateId,通过 GET /api/v3/myTrades 以 cantonUpdateId 暴露,任何人都可以对照账本验证。

本页说明该模型、时序及其风险、状态机、如何自行验证结算,以及由此衍生的规则(零手续费、最小名义金额、自成交防护)。

参与者与分段​

分段对象资金来源结算
钱包出资(wallet-funded)已绑定 Canton party 并完成 MemberAuth 的机构与零售账户资产留在参与者自己的 Canton 钱包中;交易所持有镜像余额每笔成交都在链上结算
内部出资(internally-funded)Rocky 自有做市账户交易所托管DvP 层仅记账;两个此类账户之间的成交记录为 skipped,并在下一个 epoch 承诺 中作为数据上链

钱包出资账户与 Rocky 自有账户之间的成交,在参与者钱包与 Rocky 托管 party 之间链上结算。两个钱包出资账户之间的成交则钱包对钱包结算。账户属于哪个分段在接入时确定(见 机构接入)。

镜像余额​

对于钱包出资账户,GET /api/v3/account 报告的是链上持仓的镜像,由一个按固定间隔运行的对账器维护(SPOT_MIRROR_SYNC_INTERVAL_MS,默认 30 秒)。它仅由定时器驱动——不会被结算事件触发——且只有在连续两轮都观察到同一非零漂移后才会动账,因此一次外部变动大约需要两个周期才会落账。对账器按资产计算:

drift = on-chain holdings − ( mirror free + mirror locked + unsettled outgoing − unsettled incoming )
  • 入金:把资产转入你的钱包;对账器连续两轮看到该增量后即会计入 free。没有充值端点,也没有确认步骤(钱包出资账户调用 /v1/spot/transfer 会被以 HTTP 422 和 {"error": …} 响应体拒绝,见 现货划转)。
  • 出金:无需操作——已结算的资产本来就在你的钱包里。/api/v3/account 报告 canWithdraw: false(该字段对所有现货账户均为 false)。
  • 在你仍有敞口(挂单或未结算的卖出)时从钱包中移走资产,会被检测为负漂移:先减少 free;若不足,则撤销该资产上的挂单以释放 locked;若剩余缺口超过这些撤单所能释放的额度(且大于尘埃级),账户会被冻结;更小的缺口会在下一轮退款到账后重新计算。

被冻结的账户​

被冻结的账户在下单时以 -2010 Account is frozen pending settlement review. 拒绝。已有订单仍可撤销,对账继续进行,进行中成交的结算也继续。解冻是人工运营操作,需在链上缺口被补足且镜像对账漂移归零之后进行。请通过集成包中的渠道联系支持。

乐观结算:时序与风险披露​

结算是异步且乐观的:

  1. 成交执行后,内部镜像余额立即变动。你的下一笔订单可以马上使用所得。
  2. 结算器拾取该成交(轮询间隔 SPOT_CHAIN_POLL_INTERVAL_MS,默认 1 秒;生产环境为 2 秒),用能覆盖每条腿的最少输入 holding 构造两腿 DvP——双方 receiver 都有 direct preapproval 时是同一笔原子交易里的两笔 Token Standard 直接转账,否则走 allocate + execute 路径——并提交到 Canton。一小组 worker(SPOT_CHAIN_WORKERS,默认 2)按成交顺序结算;两笔在途结算绝不会花同一个 holding,因此同一方的一连串交易仍按序交割,「卖出尚未收到的资产」在链上自然得到解决。吞吐量约为每个 worker 每两秒左右一笔;持续高负载下队列会增长,pending 会持续更久。
  3. 确认后记录并暴露 updateId。

净额结算。 同一对 party 之间的成交不是逐笔交割的。它们按对手方对归入一个批次,以一笔原子多腿 DvP 按资产净额交割(BIS DvP Model 3:先买后卖同等数量,在基础资产上净为零,在计价资产上只剩差价)。批次随第一笔成交开启,通常 60 秒后结算;当以稳定币计价的净敞口很小时,它会等待更多可以对冲的成交,最多到开窗后 15 分钟;成交达到 500 笔或敞口较大时立即结算。每笔成交保留自己的 settlementStatus;批次确认后,批内所有成交报告同一个 cantonUpdateId;各资产都净为零的批次不产生账本交易即 confirmed(lastError: "netted-to-zero")。因此驱动 Canton 流量的不再是成交笔数,而是每个窗口内你的对手方数量。

这对你意味着:

  • 成交与交割之间存在信用敞口。 在 confirmed 之前,对手方的交割仅以内部余额变动的形式存在。Rocky 通过镜像对账器(对钱包已无法覆盖其敞口的账户进行钳制与冻结)来约束这一敞口,并要求机构参与者在接入时签署信用条款。在你自己的风险核算中,请把 pending/processing/retry 的余额视为未结算。
  • failed 是可能发生的,且需人工处理。 如果结算无法交割(例如对手方在成交与交割之间撤销了 MemberAuth),内部账目已反映该成交。Rocky 运营会立即收到告警,并与受影响各方人工处理头寸。
  • 你自己的钱包必须始终有能力交割。 在有未结算卖出时把资产移出钱包会触发上述钳制/冻结流程。请把结算库存保留在交易钱包中。

结算状态机​

┌──────────┐ claim ┌────────────┐ confirmed ┌───────────┐
fill ──────► │ pending │ ──────────► │ processing │ ───────────► │ confirmed │
└──────────┘ └────────────┘ └───────────┘
▲ │ transient error
backoff │ ▼
elapsed, │ ┌─────────┐
re-claimed └─ │ retry │
└─────────┘
│ terminal error, or attempt cap
│ (SPOT_CHAIN_MAX_ATTEMPTS, default 20) reached
▼
┌─────────┐
│ failed │
└─────────┘

fill between two house accounts ───────► skipped
no settlement row (pre-activation, or internally-funded segment) ───► internal
状态终态cantonUpdateId
pending否null
processing否null
retry否null
confirmed是已设置
failed是null
skipped是null
internal是null

各取值的完整语义见 账户成交历史。retry 行在退避期满后回到 processing(不会回到 pending)。传输与争用错误的退避为 1 + attempt 秒(attempt 上限 30),sequencer 背压为 2 + 2 × attempt 秒;被判定为终态的错误(缺少 MemberAuth、Daml 授权失败、不支持的 instrument、缺少 party)不重试,直接进入 failed。停留在 processing 达 5 分钟的行(例如结算器重启后)会被重新领取,并以同一个确定性 command id 重新提交,因此一笔成交绝不会结算两次。尝试次数上限是配置项(SPOT_CHAIN_MAX_ATTEMPTS,默认 20)。Rocky 运营会监控在 processing 或 retry 停留超过 10 分钟的结算。

历史成交永不回填:在交易场所启用链上结算之前执行的成交永久保持 internal。

每笔成交都上链承诺​

独立于上述 DvP,每一笔成交——包括 skipped 的自营成交以及 2026-09-18 之后执行的 internal 成交——和每一个订单事件都会在与事件同一个数据库事务里写入 append-only 日志,并按 epoch 以 Merkle 根加当期成交记录的形式承诺到 Canton。用 GET /v1/chain/proof 可以为任意 tradeId 或 orderId 取得包含证明并对照链上的根验证。见 Epoch 承诺。

在 Canton 上验证结算​

每一笔 confirmed 成交都是一笔 Canton 交易,其根节点是对 Rocky MemberAuth 合约 Auth_Open choice 的 exercise,其中嵌套着恰好两个 Allocation_ExecuteTransfer exercise——每腿一个——它们共享一个 settlementRef,其 id 等于该成交的 trade id。你可以用自己的 Canton participant 或 Canton Scan 浏览器进行核对;Rocky 不参与其中。

  1. 获取 id。 调用 GET /api/v3/myTrades?symbol=CBTC-USDCB。记下该成交的 id(trade UUID)和 cantonUpdateId(十六进制字符串,通常以 1220 开头)。
  2. 打开该 update。 在 Canton Scan 中搜索该 update id,或用 updateId 查询托管你 party 的 participant 的 ledger API(GET /v2/updates/update-by-id,或你所用 participant 版本的等价接口)。你的 party 是自己那条腿的 stakeholder,因此该交易对你可见。
  3. 检查结构。 交易必须恰好包含两个名为 Allocation_ExecuteTransfer 的 exercised choice(外层的 MemberAuth Auth_Open / Auth_RunWith exercise 是包装层)。其他任何情况——一条腿、三条腿、不同的转账 choice 名——都不是 Rocky 现货结算。
  4. 匹配引用。 在每个 exercise 的参数中读取 settlementRef.id。两条腿必须携带相同的值,且必须等于第 1 步中的 trade id。正是这一点把链上交易与你看到的成交绑定在一起。
  5. 匹配金额与各方。 你作为发送方的那条腿恰好转移 qty 的基础资产(若你卖出)或 quoteQty 的计价资产(若你买入);另一条腿转移相反的资产。对手方要么是另一位参与者的 party,要么(对于与自有账户的成交)是 Rocky 的托管 party。instrument id 对应 exchangeInfo 中的 baseAsset / quoteAsset 名称。
  6. 对账。 一段时间内某资产已确认各腿之和,应等于同期你该资产链上持仓的变动,扣除你自己的转入转出。若不一致,请提交支持工单并附上 cantonUpdateId。

Rocky 还为机构参与者生成每日结算报告(updateId 的数量与列表);请通过集成包中的渠道索取。

零手续费​

成交中钱包出资的一方无论 maker 还是 taker 手续费均为零——双方都是钱包出资账户时则双方均为零。此类成交在 myTrades 中的 commission 均为零。原因是结构性的:链上那条腿交割的是成交的全额,任何内部手续费都会使镜像余额与钱包持仓产生偏离。内部出资的自有账户的手续费不受影响,作为外部参与者无需关心。手续费表不会在 exchangeInfo 中公布。

最小名义金额​

钱包出资账户的每一笔订单都必须满足其交易对的 price × quantity ≥ minNotional,在下单时检查(-1013 Filter failure: NOTIONAL)。MARKET 单使用保护价带的价格。当前值按交易对公布于 exchangeInfo(NOTIONAL.minNotional);以 USD 计价的市场(CBTC-USDCB、CETH-USDCB、CBTC-USX、CBTC-USDCX)为 10 个计价单位,CETH-CBTC 为 0.00015 CBTC。请始终读取实时值,不要硬编码。

自成交防护​

自成交防护始终开启且不可配置。当你的一笔订单将与你自己的挂单匹配时,撮合器会跳过你的挂单并继续到下一个价格档或对手方;你的挂单原样留在簿中,你的新订单可能排在它之后(LIMIT)或撤销其剩余部分(MARKET)。被跳过的订单不会产生成交、撤销或错误。没有 selfTradePreventionMode 参数,也没有 EXPIRED_IN_MATCH 状态。