> ## Documentation Index
> Fetch the complete documentation index at: https://docs.intention.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# 计划服务

> 在链下依据已提交的交易历史推导账户状态，再通过协议交易把结果写回链上的服务。

交易场所需要知道的某些账户信息，没法在区块执行期间算出来。成交量档位要看三十天的交易，奖励要看一个还没关闭的窗口，推荐归因要看几个月前建立的关系。

把这类活儿放进区块执行，会在两头都出错：一头是让每个区块都为一项几乎没有区块用得上的计算买单，另一头是逼内核去背它本来没有理由持有的历史数据。

计划服务（program services）的解法是把方向倒过来。计算跑在区块**之外**，依据的是已提交的记录；算出来的*结果*再作为协议状态提交**回**链上，执行时像读其他任何配置一样，常数时间就能读到。

<div className="dg" data-dg="program-writeback">
  <div className="dg-c" style={{aspectRatio:"720 / 348"}}>
    <svg className="dg-w" viewBox="0 0 720 348" aria-hidden="true">
      <path className="dg-wire" d="M 144.00 70.00 L 169.60 70.00" />

      <path className="dg-head" d="M 176.00 70.00 L 169.60 74.40 L 169.60 65.60 Z" />

      <path className="dg-wire dg--sky" d="M 344.00 70.00 L 369.60 70.00" />

      <path className="dg-head dg--sky" d="M 376.00 70.00 L 369.60 74.40 L 369.60 65.60 Z" />

      <path className="dg-wire" d="M 534.00 70.00 L 561.75 38.78" />

      <path className="dg-head" d="M 566.00 34.00 L 565.04 41.71 L 558.46 35.86 Z" />

      <path className="dg-wire dg--blue" d="M 534.00 70.00 L 561.33 95.62" />

      <path className="dg-head dg--blue" d="M 566.00 100.00 L 558.32 98.83 L 564.34 92.41 Z" />

      <path className="dg-wire dg--blue" d="M 645.00 128.00 L 645.00 136.00 L 465.00 136.00 L 465.00 142.00" />

      <path className="dg-head dg--blue" d="M 465.00 142.00 L 460.60 135.60 L 469.40 135.60 Z" />

      <path className="dg-wire dg--green" d="M 554.00 181.00 L 567.60 181.00" />

      <path className="dg-head dg--green" d="M 574.00 181.00 L 567.60 185.40 L 567.60 176.60 Z" />
    </svg>

    <div className="dg-b" style={{left:"0.0000%",top:"11.4943%",width:"19.4444%",height:"17.2414%"}}><span className="dg-t">已提交的交易历史</span></div>
    <div className="dg-b dg--sky" style={{left:"25.0000%",top:"5.7471%",width:"22.2222%",height:"28.7356%"}}><span className="dg-t">计划服务</span><span className="dg-s">窗口计算，在区块之外</span><span className="dg-n">从链上实时读取手续费表</span></div>
    <div className="dg-b dg--yellow dg-round" style={{left:"52.7778%",top:"10.3448%",width:"20.8333%",height:"19.5402%"}}><span className="dg-t">自上次应用以来是否变化？</span></div>
    <div className="dg-b" style={{left:"79.1667%",top:"2.8736%",width:"20.8333%",height:"13.7931%"}}><span className="dg-t">不写入</span></div>
    <div className="dg-b dg--blue" style={{left:"79.1667%",top:"21.8391%",width:"20.8333%",height:"13.7931%"}}><span className="dg-t">协议交易</span></div>
    <div className="dg-b dg--green" style={{left:"52.7778%",top:"41.9540%",width:"23.6111%",height:"20.1149%"}}><span className="dg-t">链上状态</span></div>
    <div className="dg-b dg--green" style={{left:"80.2778%",top:"41.9540%",width:"19.7222%",height:"20.1149%"}}><span className="dg-t">内核在执行时读取</span><span className="dg-s">常数时间</span></div>
    <div className="dg-b dg-dashed dg-left" style={{left:"0.0000%",top:"70.1149%",width:"48.8889%",height:"24.1379%"}}><span className="dg-t">比对的是费率，不是档位序号</span><span className="dg-s">阈值挪动，或某个档位重新定价，都会改变账户实际付多少钱，却不改变它的档位序号。</span></div>
    <div className="dg-b dg-dashed dg-left" style={{left:"51.1111%",top:"70.1149%",width:"48.8889%",height:"24.1379%"}}><span className="dg-t">最终费率由链来解析</span><span className="dg-s">每一批都提交成功之后，这个周期才记为已应用；崩溃会把整个周期重放一遍，这样做是安全的，因为计算是幂等的。</span></div>
  </div>
</div>

链仍然是权威。服务并不持有网络依赖的状态——它只是提议一个值，链接受了的才算数。

<h2 id="what-runs-today">
  目前在运行的
</h2>

**基于成交量的费率档位**。这个服务消费节点流式推来的交易历史，按账户累计成交量，并按计划做快照。一个周期结束，它算出每个账户在滚动窗口内的成交量，再拿**从链上实时读来的**手续费配置把成交量映射成档位，最后分批把变了的账户写回链上。

这句话里有几个细节是承重的：

* **档位表从链上读，绝不写死**：服务要是自己留一份副本，网络换了费率表之后，它还会接着用昨天那一张。
* **比对的是费率，不是档位序号**：只比*序号*会漏掉两种真实情况——阈值挪了，成交量没变的账户落进了另一个档位；档位重新定了价，序号却没动。这两种都会改变账户实际付多少钱，却都不改变序号。
* **最终费率由链来解析**：交易带的是一个档位序号，执行时对照链上实时的手续费配置把它解析成费率。序号超出有效范围，整批交易失败，不会只应用一部分。
* **每一批都提交成功之后，这个周期才记为已应用**：周期做到一半崩了，就把整个周期重放一遍。这样做是安全的，因为计算是幂等的——同一个窗口，算出来的东西一样。

<h2 id="the-failure-model">
  失效模型
</h2>

这些服务夹在两个系统中间，而这两个系统都会时不时不可用。设计直接把这件事当成前提，不当成异常。

依赖出问题——数据库、节点的数据流、节点的 API——一律带退避重试，不会把进程干掉：重启修不好一个连不上的依赖，只会在故障之上再加一次冷启动。仍然算致命的，是重启*确实*能修、或者必须让运维看见的那些：启动时配置无效、绑不上健康检查端点，以及 panic。

故障期间，进程照常运行，把自己报成 not-ready，并累计错误数。所以运维信号是“**它 not-ready 多少分钟了**”，不是“**进程还活着吗**”——前者才是有用的问题：进程活着、却一个小时没能摄入数据，这才是真正的事故。

收到编排系统发来的信号，进程会优雅关停：停下手上的活、刷写检查点、干净退出。没有这一步，每一次例行部署都要赔上一个没刷写的窗口和一次重放。

<Note>
  算完了但还没应用的周期，不是丢掉的周期。计算是幂等的，而且要写入成功才会记下已应用的周期，所以中断的那次运行恢复时会把这个窗口重做一遍，不会跳过。
</Note>

<h2 id="why-the-pattern-generalizes">
  这个模式为何可以推广
</h2>

写回路径是通用的。设置账户级配置、设置全局配置的协议交易本来就有；所谓计划服务，就是任何一个从已提交历史里为这些交易算出一个值的进程。

目前跑着的只有费率档位。激励计划、推荐归因、活动资格是同一个形状：对交易历史做窗口计算，和当前已应用的值比差异，再分批写回。它们该放在这里而不是放进内核，理由和费率档位一样——计算是周期性的、面向历史的，而执行要的答案必须是常数时间的查表。这些都还没建；能推广的是这个模式，不是它们一定会用它的承诺。

这些计划的商业条款在[费率与计划](/zh/programs/fees)里讲。本页讲的是结果怎么上链。

<h2 id="where-to-go-next">
  后续阅读
</h2>

<CardGroup cols={2}>
  <Card title="索引器" href="/zh/protocol/architecture/indexer">
    这些服务所消费的数据流。
  </Card>

  <Card title="手续费" href="/zh/programs/fees">
    商业面：有哪些档位，各自的费率是多少。
  </Card>

  <Card title="IntentionKernel" href="/zh/protocol/architecture/kernel">
    写回的配置如何在执行期间被读取。
  </Card>

  <Card title="状态模型" href="/zh/protocol/architecture/state/model">
    配置键为什么带版本，客户端又为什么该实时解析。
  </Card>
</CardGroup>
