> ## 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-Hant/programs/fees)中說明。本頁講的是結果如何上鏈。

<h2 id="where-to-go-next">
  後續閱讀
</h2>

<CardGroup cols={2}>
  <Card title="索引器" href="/zh-Hant/protocol/architecture/indexer">
    這些服務所消費的資料流。
  </Card>

  <Card title="手續費" href="/zh-Hant/programs/fees">
    商業面：有哪些級距，各自的費率是多少。
  </Card>

  <Card title="IntentionKernel" href="/zh-Hant/protocol/architecture/kernel">
    寫回的配置在執行期間怎麼讀。
  </Card>

  <Card title="狀態模型" href="/zh-Hant/protocol/architecture/state/model">
    配置鍵為何帶版本，以及用戶端為何應當即時解析它們。
  </Card>
</CardGroup>
