@palliora.org/chainsdk 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +61 -0
- package/CHAIN-RULES.md +760 -0
- package/README.md +161 -26
- package/dist/index.cjs +1313 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1311 -24
- package/dist/index.d.ts +1311 -24
- package/dist/index.js +1282 -93
- package/dist/index.js.map +1 -1
- package/package.json +20 -11
package/CHAIN-RULES.md
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
# Palliora chain rules
|
|
2
|
+
|
|
3
|
+
The behavioural rules of the Palliora chain, for anyone — human or agent — building an
|
|
4
|
+
application on `@palliora.org/chainsdk`.
|
|
5
|
+
|
|
6
|
+
The SDK README documents *which function to call*. This document covers *what the chain
|
|
7
|
+
does with the call*: what it charges, what it reserves, who has to agree, and why a
|
|
8
|
+
correctly-shaped transaction still gets rejected. These rules are not visible from the
|
|
9
|
+
SDK's TypeScript signatures, which is why they are written down separately.
|
|
10
|
+
|
|
11
|
+
Source citations point into [`palliora-org/palliora`](https://github.com/palliora-org/palliora)
|
|
12
|
+
(the chain) and this SDK. Line numbers drift; the function and type names are the stable
|
|
13
|
+
anchors. When this document and the pallet disagree, the pallet is right.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Orientation: which repository owns which fact
|
|
18
|
+
|
|
19
|
+
| Repository | Owns | You need it when |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| [`palliora`](https://github.com/palliora-org/palliora) | The chain. `pallets/compute` (agreements, fees, settlement), `pallets/dactr` (DA + guardian groups), `runtime` (constants), `guardian` (threshold crypto service) | Deciding what the chain will accept or charge |
|
|
22
|
+
| [`palliora-sdk`](https://github.com/palliora-org/chainsdk) | TypeScript wrappers, type registrations (`src/chain/spec.ts`), crypto helpers | Writing application code |
|
|
23
|
+
| `compute-core` | `orchestrator` (executes jobs), `result_relay` (submits `compute.result`), `docker_gateway` | Running a compute node, or understanding how a result reaches the chain |
|
|
24
|
+
| [`palliora-core`](https://github.com/palliora-org/palliora-core) | Kate commitments, DA primitives shared by the chain | Working on DA internals — rarely needed by applications |
|
|
25
|
+
|
|
26
|
+
**An application developer only ever signs transactions from `palliora-sdk`.** The
|
|
27
|
+
orchestrator and result relay are node-operator infrastructure: you do not call them,
|
|
28
|
+
but the fees you offer pay them, and the results you wait for come from them.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 2. The money model
|
|
33
|
+
|
|
34
|
+
This is the single most common source of failure, so it comes first.
|
|
35
|
+
|
|
36
|
+
### 2.1 Two gates, not one
|
|
37
|
+
|
|
38
|
+
An `Active` or `Subscription` agreement must pass **two independent checks**. They test
|
|
39
|
+
different fields, they are enforced in different places, and passing one tells you nothing
|
|
40
|
+
about the other.
|
|
41
|
+
|
|
42
|
+
| | Gate 1 — the chain's fee floor | Gate 2 — the guardians' rate threshold |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| **Tests** | `compute.fees` (the absolute amount) | `compute.computeRate` (the per-ms rate) |
|
|
45
|
+
| **Against** | A formula over live chain parameters | Each guardian's own declared minimum |
|
|
46
|
+
| **Enforced by** | `CheckCompute` signed extension, at validation | Guardians' offchain workers, before inclusion |
|
|
47
|
+
| **Enforced in** | [`pallets/compute/src/lib.rs`](https://github.com/palliora-org/palliora/blob/main/pallets/compute/src/lib.rs) — `ensure_min_free_balance` | [`pallets/compute/src/offchain.rs`](https://github.com/palliora-org/palliora/blob/main/pallets/compute/src/offchain.rs) — `decide` |
|
|
48
|
+
| **Failure looks like** | Transaction rejected: `InsufficientFreeBalance` | Transaction included, then `AgreementFailed` event |
|
|
49
|
+
| **Failure timing** | Immediate, at submission | After all guardians respond — or never, if one is offline |
|
|
50
|
+
|
|
51
|
+
**How Gate 2 actually plays out.** `CheckCompute` gives a `compute.agreement` transaction a
|
|
52
|
+
`requires` tag *per named guardian*, satisfied only when that guardian submits its
|
|
53
|
+
`agreement_response`. Until all of them have, your transaction sits in the node's **future
|
|
54
|
+
queue** — not rejected, not included, no error. Guardians read that same future queue to
|
|
55
|
+
decide (`accept_agreements` in `offchain.rs`), which is what lets them judge an agreement
|
|
56
|
+
before it is ever in a block.
|
|
57
|
+
|
|
58
|
+
The consequences are worth stating explicitly, because the failure is silent:
|
|
59
|
+
|
|
60
|
+
- If a named guardian is offline or never responds, the agreement **never gets included and
|
|
61
|
+
never errors**. A hanging `signAndSend` is the expected symptom.
|
|
62
|
+
- If every guardian responds and any one rejected, the transaction *is* included, and
|
|
63
|
+
`compute.agreement` emits `AgreementFailed` and returns without reserving anything.
|
|
64
|
+
- So name guardians you have reason to believe are live — from `getGuardianList()` or the
|
|
65
|
+
current era's `guardian.guardians` — and treat a stalled agreement as a guardian
|
|
66
|
+
availability problem, not a fee problem.
|
|
67
|
+
|
|
68
|
+
A generous `fees` does not buy you past Gate 2, and a generous `computeRate` does not by
|
|
69
|
+
itself satisfy Gate 1 — though it does *raise* Gate 1, because `computeRate` is one of the
|
|
70
|
+
floor's four components. Raising the rate to satisfy guardians raises the minimum `fees`
|
|
71
|
+
you must also offer.
|
|
72
|
+
|
|
73
|
+
`Dormant` contracts are exempt from both gates. They reserve nothing, so they may offer
|
|
74
|
+
any `fees`, including zero.
|
|
75
|
+
|
|
76
|
+
### 2.2 Gate 1: the fee floor
|
|
77
|
+
|
|
78
|
+
`fees` must be at least the sum of four components, and the signer must actually hold
|
|
79
|
+
that much free balance:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
result_fee = MaxDaStorageSize × ProviderStorageRate
|
|
83
|
+
input_fee = usage_price of the input contract (0 if no ContractId input)
|
|
84
|
+
threshold_decryption_fee = ThresholdDecryptionFee (flat)
|
|
85
|
+
offered_component = computeRate × MillisecondsPerBlock (one block of compute)
|
|
86
|
+
|
|
87
|
+
min_fees = result_fee + input_fee + threshold_decryption_fee + offered_component
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Defined once in `pallet_compute::Pallet::fee_components` and used from three places —
|
|
91
|
+
`ensure_min_free_balance` at validation, the guardian offchain worker, and `compute.result`
|
|
92
|
+
at settlement — specifically so the three cannot drift apart.
|
|
93
|
+
|
|
94
|
+
**Do not hard-code these numbers.** The first three parameters are root-settable at
|
|
95
|
+
runtime (`compute.setMaxDaStorageSize`, `setProviderStorageRate`,
|
|
96
|
+
`setThresholdDecryptionFee`). A stale copy fails silently, as an
|
|
97
|
+
`InsufficientFreeBalance` you cannot explain. Query them:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { estimateMinFee, getFeeParams, fromAtomicPaliAmount } from "@palliora.org/chainsdk";
|
|
101
|
+
|
|
102
|
+
const floor = await estimateMinFee({ computeRate: "0.000000001" });
|
|
103
|
+
console.log(fromAtomicPaliAmount(floor.minFee)); // smallest acceptable `fees`
|
|
104
|
+
console.log(floor.resultFee, floor.offeredComponent); // the breakdown, in atomic units
|
|
105
|
+
|
|
106
|
+
const params = await getFeeParams(); // the four raw chain values
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`estimateMinFee` mirrors `fee_components` exactly. Offer at least `minFee`; offer more to
|
|
110
|
+
buy more compute time.
|
|
111
|
+
|
|
112
|
+
Worked example, at the genesis parameters (`MaxDaStorageSize` 10485760,
|
|
113
|
+
`ProviderStorageRate` 1e9, `ThresholdDecryptionFee` 1e15, block time 500ms, 18 decimals):
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
result_fee = 10485760 × 1e9 = 1.048576e16 ≈ 0.0105 PALI
|
|
117
|
+
threshold_decryption_fee = 1e15 = 0.001 PALI
|
|
118
|
+
offered_component = computeRate × 500
|
|
119
|
+
min_fees (no input contract, rate 0) ≈ 0.0115 PALI
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Verify against the live chain rather than trusting these figures — they are genesis
|
|
123
|
+
values for one network, not constants.
|
|
124
|
+
|
|
125
|
+
### 2.3 How much compute your fee buys
|
|
126
|
+
|
|
127
|
+
Beyond the floor, `fees` is a *budget*. Compute is billed at `computeRate` per millisecond
|
|
128
|
+
of actual execution, settled when the result lands:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
compute_fee = computeRate × compute_duration_ms
|
|
132
|
+
budget_for_compute = fees − result_fee − input_fee − threshold_decryption_fee
|
|
133
|
+
max_duration_ms = budget_for_compute ÷ computeRate
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Anything unspent is returned to you when the contract settles.
|
|
137
|
+
|
|
138
|
+
### 2.4 Gate 2: guardian rate thresholds
|
|
139
|
+
|
|
140
|
+
Every guardian declares a minimum rate it will take work at, **per compute type**:
|
|
141
|
+
|
|
142
|
+
```rust
|
|
143
|
+
pub enum ComputeType { Trusted, Tee, Mpc, Fhe, Zkp }
|
|
144
|
+
pub type FeeThresholds = BoundedVec<(ComputeType, u128), ConstU32<8>>;
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
A compute type absent from a guardian's list carries **no threshold and reads as zero** —
|
|
148
|
+
that guardian accepts any rate for it. The compute type is not something you set directly;
|
|
149
|
+
it is derived from your contract's `confidentiality` field:
|
|
150
|
+
|
|
151
|
+
| `confidentiality` in your contract | `ComputeType` the threshold is looked up under |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `{ Trusted: <index> }` | `Trusted` |
|
|
154
|
+
| `"TEE"` | `Tee` |
|
|
155
|
+
| `"SMPC"` | `Mpc` |
|
|
156
|
+
| `"FHE"` | `Fhe` |
|
|
157
|
+
|
|
158
|
+
Each guardian named in `contract.guardians` independently decides. If **any** of them
|
|
159
|
+
rejects, the agreement fails — `compute.agreement` emits `AgreementFailed` and returns
|
|
160
|
+
without reserving anything. Choosing a confidentiality level therefore changes the price
|
|
161
|
+
floor you must clear, because it changes which threshold applies.
|
|
162
|
+
|
|
163
|
+
A guardian may instead be pointed at an **oracle quote**: pass an `oracle_quote_id` to
|
|
164
|
+
`compute.agreement`, and guardians price against the oracle's quoted rate rather than
|
|
165
|
+
their own threshold. If the oracle is unreachable, times out (500ms), or does not know the
|
|
166
|
+
quote ID, each guardian silently falls back to its own threshold — so an oracle quote is a
|
|
167
|
+
hint, never a guarantee.
|
|
168
|
+
|
|
169
|
+
### 2.5 Who gets paid, and when
|
|
170
|
+
|
|
171
|
+
At `compute.result`, the reserved deposit is split:
|
|
172
|
+
|
|
173
|
+
| Recipient | Amount |
|
|
174
|
+
|---|---|
|
|
175
|
+
| Owner of the input contract | `input_fee` (only when input is a `ContractId`) |
|
|
176
|
+
| The contract's guardians | `threshold_decryption_fee`, split equally — **only when there is more than one guardian** |
|
|
177
|
+
| The result submitter | `result_fee + (computeRate × compute_duration_ms)` |
|
|
178
|
+
| You, the contract owner | Everything still reserved, refunded at settlement |
|
|
179
|
+
|
|
180
|
+
The guardian split divides by `max(guardian_count, 3)`, so a 2-guardian contract leaves a
|
|
181
|
+
third share reserved, which returns to you at settlement rather than being paid out.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 3. The `Contract` object
|
|
186
|
+
|
|
187
|
+
`compute.agreement` takes exactly one structured argument, and you build it by hand. The
|
|
188
|
+
SDK types it loosely (`compute: Record<string, unknown>`, `preCheck?: unknown`,
|
|
189
|
+
`resultCipher: unknown`), so TypeScript will not catch a wrong shape — the chain will,
|
|
190
|
+
usually as an opaque decode failure. This section is the field reference.
|
|
191
|
+
|
|
192
|
+
Two conventions apply throughout, and both bite:
|
|
193
|
+
|
|
194
|
+
- **Field names are camelCase**, not the Rust snake_case. `computeRate`, not
|
|
195
|
+
`compute_rate`; `resultCipher`, not `result_cipher`. The names come from the manual type
|
|
196
|
+
registry in `src/chain/spec.ts`.
|
|
197
|
+
- **Omitted fields do not error.** `Option` fields default to `None`, and missing struct
|
|
198
|
+
members default to zero. Passing `{ Url: { url } }` without `size` silently sends
|
|
199
|
+
`size: 0`. Be explicit about anything that matters.
|
|
200
|
+
|
|
201
|
+
### 3.1 Shape at a glance
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
{
|
|
205
|
+
contractType: "Dormant" | "Active" | "Subscription",
|
|
206
|
+
guardians: string[], // account IDs; indices referenced below
|
|
207
|
+
preCheck: null, // Option<ComputeInfo> — input verification
|
|
208
|
+
compute: { // ComputeInfo — the primary step
|
|
209
|
+
cipher: "Plaintext", // how `input` is encrypted
|
|
210
|
+
computerIndices: [0, 1, 2], // which of `guardians` execute this step
|
|
211
|
+
fees: "10000000000000000", // atomic units; see §2.2 for the floor
|
|
212
|
+
computeRate: "1000000000", // atomic units, per millisecond
|
|
213
|
+
deadline: 0, // block number; 0 = none
|
|
214
|
+
confidentiality: { Trusted: 0 },// selects the ComputeType priced in §2.4
|
|
215
|
+
feeFunction: null, // Option<u8>
|
|
216
|
+
programEnv: null, // Option<Vec<u8>> — env vars for the program
|
|
217
|
+
input: { Inline: { data: [] } },// DAInput — the data
|
|
218
|
+
program: { NativeExecute: "Inference" }, // DAInput — the code
|
|
219
|
+
metadata: null, // Option<ComputeMetadata>
|
|
220
|
+
},
|
|
221
|
+
postCheck: null, // Option<ComputeInfo> — result verification
|
|
222
|
+
resultCipher: "Plaintext", // how the result is encrypted back to you
|
|
223
|
+
currencyId: "Native",
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`buildFee({ amount, computeRate })` fills in `fees` and `computeRate` from human PALI
|
|
228
|
+
strings — prefer it over writing atomic units by hand.
|
|
229
|
+
|
|
230
|
+
### 3.2 `Contract` fields
|
|
231
|
+
|
|
232
|
+
| Field | Type | Notes |
|
|
233
|
+
|---|---|---|
|
|
234
|
+
| `contractType` | enum | Governs reservation, settlement and invocability. See §3.6 |
|
|
235
|
+
| `guardians` | `Vec<AccountId>` | The participant set. Every index elsewhere in the contract points into **this list, in this order** |
|
|
236
|
+
| `preCheck` | `Option<ComputeInfo>` | Input verification step. `null` to skip |
|
|
237
|
+
| `compute` | `ComputeInfo` | The primary step. Required — this is what gets billed |
|
|
238
|
+
| `postCheck` | `Option<ComputeInfo>` | Result verification step. `null` to skip |
|
|
239
|
+
| `resultCipher` | `CipherSuite` | Encryption applied to the result on its way back to you |
|
|
240
|
+
| `currencyId` | enum | `"Native"` \| `"USDC"` \| `{ ForeignAsset: n }`. The deposit is reserved in, and settlement paid from, this currency. `createAgreement` defaults it to `"Native"` |
|
|
241
|
+
|
|
242
|
+
`guardians` is validated by `CheckCompute` before the contract is ever decoded by the
|
|
243
|
+
pallet, with three separate rules — each a distinct rejection code:
|
|
244
|
+
|
|
245
|
+
| Rule | Rejected as |
|
|
246
|
+
|---|---|
|
|
247
|
+
| Must be non-empty, unless `compute.program` is `"Null"` | `Custom(149)` |
|
|
248
|
+
| Entries must be unique — no duplicates | `Custom(145)` |
|
|
249
|
+
| Every entry must be a registered, staked guardian | `Custom(148)` |
|
|
250
|
+
|
|
251
|
+
### 3.3 `ComputeInfo` fields
|
|
252
|
+
|
|
253
|
+
The same struct is used for `preCheck`, `compute` and `postCheck`. **Only `compute` is
|
|
254
|
+
billed** — fee fields on the check steps are not what settlement reads.
|
|
255
|
+
|
|
256
|
+
| Field | Type | Notes |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| `cipher` | `CipherSuite` | How `input` is encrypted. `"Plaintext"` when it is not |
|
|
259
|
+
| `computerIndices` | `Vec<u32>` | Indices into `Contract.guardians` that execute this step. Usually all of them: `guardians.map((_, i) => i)` |
|
|
260
|
+
| `fees` | `u128` | Total offered, atomic units. Must clear the floor (§2.2) for `Active`/`Subscription` |
|
|
261
|
+
| `computeRate` | `u128` | Atomic units **per millisecond**. Must be non-zero except on `Dormant`. Weighed against guardian thresholds (§2.4) |
|
|
262
|
+
| `deadline` | `u64` | Block number by which the step must complete. `0` means no deadline — and is not the same as the contract-level deadline (§3.6) |
|
|
263
|
+
| `confidentiality` | enum | `{ Trusted: <index into guardians> }`, or `"TEE"` / `"FHE"` / `"SMPC"`. Determines which guardian threshold prices the offer |
|
|
264
|
+
| `feeFunction` | `Option<u8>` | Dynamic fee function selector. `null` in every current SDK helper |
|
|
265
|
+
| `programEnv` | `Option<Vec<u8>>` | Environment for program execution. Omitted by all SDK helpers — set it explicitly if you need it |
|
|
266
|
+
| `input` | `DAInput` | Where the data comes from |
|
|
267
|
+
| `program` | `DAInput` | Where the code comes from. Same type as `input` |
|
|
268
|
+
| `metadata` | `Option<ComputeMetadata>` | `{ name, description, storeType, groupId }`. `groupId` links the entry to a guardian group (§4.3). Used when registering datasets/models via `Dormant` contracts |
|
|
269
|
+
|
|
270
|
+
### 3.4 `DAInput` — how `input` and `program` are located
|
|
271
|
+
|
|
272
|
+
One enum serves both fields, which is why `program` can be a URL, a container image
|
|
273
|
+
reference, or a built-in. Variants:
|
|
274
|
+
|
|
275
|
+
| Variant | Shape | Use |
|
|
276
|
+
|---|---|---|
|
|
277
|
+
| `"Null"` | — | Nothing. On `program`, this is the one case where `guardians` may be empty |
|
|
278
|
+
| `Inline` | `{ data: number[] }` | Bytes carried in the extrinsic. Simplest, but counts against block size |
|
|
279
|
+
| `ChainTransaction` | `{ blockNumber, extrinsicIndex }` | Points at data already submitted on-chain |
|
|
280
|
+
| `ContractId` | `{ id: [u8; 32] }` | References another contract. **This is what triggers `input_fee`** — the referenced contract's owner is paid its `usage_price` (§2.5) |
|
|
281
|
+
| `Ipfs` | `{ cid: number[], size: u64 }` | Content-addressed pointer |
|
|
282
|
+
| `Url` | `{ url: number[], size: u64, hash: Option<number[]> }` | Remote fetch. `url` is UTF-8 bytes, not a string |
|
|
283
|
+
| `NativeExecute` | `"Inference"` \| `"ContractAccess"` | Built-in programs. `"Inference"` routes to the orchestrator's Ollama path |
|
|
284
|
+
| `NativeData` | `"DaFalse"` \| `"DaTrue"` | Built-in static data flags |
|
|
285
|
+
|
|
286
|
+
`Subscription` contracts are intended to take a `ContractId` input on each invocation —
|
|
287
|
+
that is how a subscription references the dataset it runs against.
|
|
288
|
+
|
|
289
|
+
### 3.5 `CipherSuite` and confidentiality — two different things
|
|
290
|
+
|
|
291
|
+
These are easy to conflate, and they are unrelated:
|
|
292
|
+
|
|
293
|
+
- **`cipher` / `resultCipher`** are about *encryption of bytes*: `"Plaintext"`,
|
|
294
|
+
`{ ThresholdHybrid: {...} }` (needs a guardian group, §4.3), or
|
|
295
|
+
`{ AsymmetricHybrid: {...} }`. Use the `encryptedInference*` helpers rather than
|
|
296
|
+
assembling these by hand — the parameter structs are large and order-sensitive.
|
|
297
|
+
- **`confidentiality`** is about *where execution happens*: `Trusted`, `TEE`, `FHE`,
|
|
298
|
+
`SMPC`. It carries no key material. It selects which guardian fee threshold applies
|
|
299
|
+
(§2.4), so it is also a pricing decision.
|
|
300
|
+
|
|
301
|
+
A `"Plaintext"` contract with `{ Trusted: 0 }` is the normal starting point: unencrypted
|
|
302
|
+
data, executed by the guardian at index 0.
|
|
303
|
+
|
|
304
|
+
### 3.6 Contract types and lifecycle
|
|
305
|
+
|
|
306
|
+
`ContractType` decides nearly everything about how a contract behaves.
|
|
307
|
+
|
|
308
|
+
| | `Dormant` | `Active` | `Subscription` |
|
|
309
|
+
|---|---|---|---|
|
|
310
|
+
| Reserves a deposit | No | Yes, `fees` | Yes, `fees` (upfront budget) |
|
|
311
|
+
| `computeRate` required non-zero | No | **Yes** (`ZeroComputeRate`) | **Yes** |
|
|
312
|
+
| Subject to the fee floor | No | Yes | Yes |
|
|
313
|
+
| Subject to guardian thresholds | No | Yes | Yes |
|
|
314
|
+
| `compute.invoke` allowed | No | No | **Yes** |
|
|
315
|
+
| Settles on | Never (registration only) | First result | Budget exhausted, or deadline |
|
|
316
|
+
| `usage_price` set to | `fees` | 0 | 0 |
|
|
317
|
+
|
|
318
|
+
**`Dormant`** registers something — a dataset, a model, a program, a set of terms — without
|
|
319
|
+
buying execution. Its `fees` becomes its `usage_price`: the amount another contract pays
|
|
320
|
+
its owner when referencing it via `DAInput::ContractId`. This is how you charge for data
|
|
321
|
+
you publish.
|
|
322
|
+
|
|
323
|
+
**`Active`** is one job. It reserves, runs, settles on the first result.
|
|
324
|
+
|
|
325
|
+
**`Subscription`** is a long-lived funded contract. `compute.agreement` reserves the whole
|
|
326
|
+
budget; each `compute.invoke` runs one job against it and stamps `invocation_block`.
|
|
327
|
+
`compute.result` bills that invocation but leaves the contract open. It settles when the
|
|
328
|
+
remaining reserve drops below one block of compute (`BudgetExhausted`) or the deadline
|
|
329
|
+
passes (`DeadlineReached`) — and, importantly, `invoke` **settles and returns `Ok`** in
|
|
330
|
+
both cases rather than erroring. A successful `invoke` is not proof that a job started;
|
|
331
|
+
check for a `ComputeInvoked` event, and treat `ContractSettled` as the terminal signal.
|
|
332
|
+
|
|
333
|
+
Every contract also gets a deadline at creation: `current_block + ContractDeadlineDuration`
|
|
334
|
+
(14400 blocks at genesis), independent of the `deadline` field inside `ComputeInfo`.
|
|
335
|
+
|
|
336
|
+
### 3.7 Contract IDs are derived, not returned
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
contract_id = blake2_256(signer_account_id ++ signer_nonce)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
using the nonce the `agreement` call is applied with. `createAgreement` reads the ID off
|
|
343
|
+
the `AgreementCreated` event, which is the reliable way to get it. Note the consequence:
|
|
344
|
+
the ID depends on the nonce, so a resubmitted or reordered transaction produces a
|
|
345
|
+
different contract.
|
|
346
|
+
|
|
347
|
+
Read a contract back with `api.query.compute.contracts(contractId)` — status, owner,
|
|
348
|
+
`origin_block`, `invocation_block`, `usage_price`, `contract_type`.
|
|
349
|
+
|
|
350
|
+
**`origin_block` is zero on a `Dormant` contract.** The pallet sets it only for `Active`
|
|
351
|
+
and `Subscription`, because the field doubles as the settlement clock and a `Dormant`
|
|
352
|
+
contract never settles. `index` is zero on every contract type. So neither field tells you
|
|
353
|
+
where a registered artifact lives — which matters, because a `ContractId` reference points
|
|
354
|
+
at exactly the contracts that lack it. The registration block survives in the deadline
|
|
355
|
+
instead:
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
// ContractDeadlines[id] = registration_block + ContractDeadlineDuration,
|
|
359
|
+
// and a Dormant contract never settles, so the entry is never cleared.
|
|
360
|
+
const deadline = await api.query.compute.contractDeadlines(contractId);
|
|
361
|
+
const duration = await api.query.compute.contractDeadlineDuration();
|
|
362
|
+
const registrationBlock = BigInt(deadline.toString()) - BigInt(duration.toString());
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
This is a derivation, not a record: it is wrong if root changed
|
|
366
|
+
`ContractDeadlineDuration` between registration and lookup.
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## 4. Guardians
|
|
371
|
+
|
|
372
|
+
### 4.1 Listing guardians
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
const guardians = await getGuardianList(); // custom RPC: guardian.guardianList
|
|
376
|
+
const detail = await getGuardianParticipants(); // current + upcoming, with prefs and stake
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
`getGuardianParticipants` reads `guardian.guardians` (active this era) and
|
|
380
|
+
`guardian.nextGuardians` (active next era). The guardian set rotates per era, so a
|
|
381
|
+
long-lived contract should name guardians present in both.
|
|
382
|
+
|
|
383
|
+
> `getGuardianParticipants` calls `disconnectApi()` in its `finally` block. It tears down
|
|
384
|
+
> the shared API connection on the way out — call it before other SDK work, not between
|
|
385
|
+
> two transactions.
|
|
386
|
+
|
|
387
|
+
### 4.2 Joining as a guardian
|
|
388
|
+
|
|
389
|
+
`joinGuardian(account, prefs)` submits `staking.guard` with a `GuardianPrefs`. The chain
|
|
390
|
+
expects `fee_thresholds` as a list of `(ComputeType, rate)` pairs, one per compute type,
|
|
391
|
+
and `prefs.fee` accepts either shape:
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
// One rate for every compute type named in `compute`
|
|
395
|
+
await joinGuardian(account, { compute: "tee,fhe", fee: 1_500_000_000_000_000_000n });
|
|
396
|
+
// -> fee_thresholds: [[Tee, 1.5e18], [Fhe, 1.5e18]]
|
|
397
|
+
|
|
398
|
+
// A rate per compute type
|
|
399
|
+
await joinGuardian(account, {
|
|
400
|
+
compute: "tee,fhe",
|
|
401
|
+
fee: { tee: 1_500_000_000_000_000_000n, fhe: 3_000_000_000_000_000_000n },
|
|
402
|
+
});
|
|
403
|
+
// -> fee_thresholds: [[Tee, 1.5e18], [Fhe, 3e18]]
|
|
404
|
+
|
|
405
|
+
// No thresholds — accepts any rate for every compute type
|
|
406
|
+
await joinGuardian(account, { compute: "tee", standard: true });
|
|
407
|
+
// -> fee_thresholds: []
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
`fee` is in **atomic units**, not PALI — the CLI converts with `tokenToBigint` before
|
|
411
|
+
calling. Pricing a compute type absent from `compute` throws rather than being sent, and
|
|
412
|
+
duplicate types are impossible in either shape: `staking.guard` rejects them with
|
|
413
|
+
`InvalidGuardianPrefs`.
|
|
414
|
+
|
|
415
|
+
### 4.3 Guardian groups — and why they are hard to find
|
|
416
|
+
|
|
417
|
+
**A guardian group has no on-chain storage.** Neither `dataAvailability.daccGuardianGroup`
|
|
418
|
+
nor `daccGuardianGroupInfo` writes anything; both only emit a `DaccGuardianGroup` event.
|
|
419
|
+
This single fact explains every difficulty around groups, so it is worth stating plainly:
|
|
420
|
+
there is no `api.query` that returns a group, and no way to enumerate groups from state.
|
|
421
|
+
|
|
422
|
+
What you get instead:
|
|
423
|
+
|
|
424
|
+
- **The group ID is derivable.** `group_id = blake2_256(concat(SCALE-encoded guardian
|
|
425
|
+
account IDs, in order))`. Order matters — the same guardians in a different order are a
|
|
426
|
+
different group.
|
|
427
|
+
- **The crypto parameters are not derivable.** `group_pk`, `tau_params` and `agg_key` come
|
|
428
|
+
from the guardian network's distributed key generation and exist only as arguments to
|
|
429
|
+
the `daccGuardianGroupInfo` extrinsic. To obtain them you must read that extrinsic back
|
|
430
|
+
out of its block.
|
|
431
|
+
|
|
432
|
+
So there are exactly two supported ways to get a `GuardianGroupInfo`:
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
// 1. Create a group and watch for the follow-up extrinsic (needs 3 guardians exactly)
|
|
436
|
+
const info = await createGuardianGroupAndWatch(account, guardians);
|
|
437
|
+
|
|
438
|
+
// 2. Reconstruct one you created earlier, from the block+index you recorded at the time
|
|
439
|
+
const info = await getGuardianGroupInfo(
|
|
440
|
+
{ blockNumber, index }, // the daccGuardianGroup creation extrinsic
|
|
441
|
+
undefined, // optional: the daccGuardianGroupInfo extrinsic; scanned for if omitted
|
|
442
|
+
);
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Persist `{ blockNumber, index }` when you create a group.** It is the only handle that
|
|
446
|
+
lets you recover the group later. Losing it means scanning the chain, or creating a new
|
|
447
|
+
group. (The `sealed-bid-auction` demo stores exactly this in a `.group.json` file.)
|
|
448
|
+
|
|
449
|
+
Group creation is a **two-transaction protocol**: your `daccGuardianGroup` call registers
|
|
450
|
+
the intent, and the guardian network responds with a separate `daccGuardianGroupInfo`
|
|
451
|
+
transaction carrying the computed parameters, typically a few blocks later. `maxBlocks`
|
|
452
|
+
(default 20) bounds how long the SDK waits.
|
|
453
|
+
|
|
454
|
+
To *list* groups, index `DaccGuardianGroup` events or `daccGuardianGroupInfo` extrinsics
|
|
455
|
+
from block history yourself, and keep your own record. There is no chain-side index.
|
|
456
|
+
|
|
457
|
+
You only need a group for **threshold-encrypted** work (`encryptedInference*`, encrypted
|
|
458
|
+
DA uploads). Plaintext and trusted compute need a guardian *list*, not a group.
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## 5. Compute results
|
|
463
|
+
|
|
464
|
+
Applications submit agreements; **node operators submit results.** If you are building an
|
|
465
|
+
app, this section is about what to wait for, not what to call.
|
|
466
|
+
|
|
467
|
+
`compute.result(request_id, contract, submitor, compute_duration_ms, execution_outcome)`
|
|
468
|
+
is submitted by `result_relay` after the orchestrator finishes a job. The SDK deliberately
|
|
469
|
+
ships no wrapper for it.
|
|
470
|
+
|
|
471
|
+
The rules that make results fail:
|
|
472
|
+
|
|
473
|
+
- **`compute_duration_ms` is bounded.** It must not exceed
|
|
474
|
+
`(elapsed_blocks + 4) × MillisecondsPerBlock`, measured from `invocation_block` (or
|
|
475
|
+
`origin_block` for non-subscriptions). Overstating duration fails with
|
|
476
|
+
`ComputeDurationTooLarge`. A very fast chain (500ms blocks) makes this bound tight.
|
|
477
|
+
- **The contract must exist.** `request_id` is the contract ID; an unknown one is
|
|
478
|
+
`AgreementNotFound`.
|
|
479
|
+
- **The `contract` argument must be re-supplied in full**, matching the original.
|
|
480
|
+
- **`CheckCompute` gates the extrinsic.** `compute.result` is one of a small allowlist of
|
|
481
|
+
calls permitted to carry a non-default `ComputePayload` (§8); anything else carrying one
|
|
482
|
+
is rejected as `ForbiddenCompute`.
|
|
483
|
+
|
|
484
|
+
To observe a result as an application, watch for these events on your contract ID:
|
|
485
|
+
|
|
486
|
+
| Event | Meaning |
|
|
487
|
+
|---|---|
|
|
488
|
+
| `compute.ComputeResult(request_id)` | A result landed |
|
|
489
|
+
| `compute.ExecutionSuccess` / `ExecutionFailed` / `ExecutionTerminated` | How the job ended |
|
|
490
|
+
| `compute.ExecutionFullSettlement` / `ExecutionPartialSettlement` | How the budget resolved |
|
|
491
|
+
| `compute.ContractSettled { refunded, reason }` | Contract closed, funds returned |
|
|
492
|
+
| `compute.SubmitorFeePaid` / `InputFeePaid` / `DecryptionFeePaid` | Who was paid |
|
|
493
|
+
|
|
494
|
+
`ComputeResult` and `ExecutionFailed` can both fire for the same request: a job that ran
|
|
495
|
+
and failed still consumed compute and still pays out.
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## 6. Writing a compute image
|
|
500
|
+
|
|
501
|
+
This section is for whoever builds the Docker image a contract runs. Everything here is
|
|
502
|
+
enforced by the **orchestrator**, a sidecar every guardian node runs alongside its compute
|
|
503
|
+
node (`compute-core/orchestrator`). It watches the chain, resolves your program and inputs,
|
|
504
|
+
drives the container through `docker_gateway`, and submits the result. You never call it —
|
|
505
|
+
you satisfy its conventions.
|
|
506
|
+
|
|
507
|
+
### 6.1 How your image is located
|
|
508
|
+
|
|
509
|
+
`contract.compute.program` decides both *what* runs and *how the image is obtained*. The
|
|
510
|
+
variants are not interchangeable:
|
|
511
|
+
|
|
512
|
+
| `program` | Image resolution |
|
|
513
|
+
|---|---|
|
|
514
|
+
| `{ Inline: { data } }` | Bytes are UTF-8 decoded into an **image reference** (`"myorg/app:v1.2.3"`) and pulled from a registry. This is the normal path |
|
|
515
|
+
| `{ Url: { url } }` | The URL is fetched and its body written to `<staging>/program/image-<b>-<e>.tar`, then loaded as a **local archive**. The URL must serve a `docker save` tarball, *not* a registry reference |
|
|
516
|
+
| `{ Ipfs: { cid } }` | Same as `Url`, fetched via the configured IPFS gateway |
|
|
517
|
+
| `{ NativeExecute: "Inference" }` | No container at all — routed to Ollama (§6.6) |
|
|
518
|
+
|
|
519
|
+
A registry reference passed as `Url` will be downloaded as if it were a tarball and fail.
|
|
520
|
+
|
|
521
|
+
### 6.2 Inputs: `/input`, read-only
|
|
522
|
+
|
|
523
|
+
Every entry in `contract.compute.input` is resolved to bytes and written to a file before
|
|
524
|
+
your container starts:
|
|
525
|
+
|
|
526
|
+
```
|
|
527
|
+
/input/<blockHeight>-<extrinsicIndex>-<i> read-only
|
|
528
|
+
/output/ read-write
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
`<i>` is the zero-based position of the input. A single-input job gets exactly one file.
|
|
532
|
+
The host side lives at `<STAGING_DIR>/<blockHeight>-<extrinsicIndex>/`, and the whole tree
|
|
533
|
+
is deleted once the result is submitted.
|
|
534
|
+
|
|
535
|
+
Rules worth building around:
|
|
536
|
+
|
|
537
|
+
- **Do not hard-code the filename.** It embeds the block height and extrinsic index, which
|
|
538
|
+
you cannot know in advance. List `/input`, sort, and read — this is exactly what the
|
|
539
|
+
built-in inference path does.
|
|
540
|
+
- **Inputs arrive decrypted.** If `contract.compute.cipher` is not `"Plaintext"`, the
|
|
541
|
+
orchestrator decrypts before writing. Your container always sees plaintext.
|
|
542
|
+
- **Multi-input encrypted jobs are rejected**, because one `CipherSuite` carries one nonce
|
|
543
|
+
and it cannot be safely reused across inputs. Multiple inputs are fine when plaintext.
|
|
544
|
+
- All DA variants (`Inline`, `Url`, `Ipfs`, `ChainTransaction`, `ContractId`) are resolved
|
|
545
|
+
by the orchestrator. Your image only ever sees a file.
|
|
546
|
+
|
|
547
|
+
### 6.3 Output: **stdout is the result**
|
|
548
|
+
|
|
549
|
+
This is the single most important rule, and the one most likely to surprise you:
|
|
550
|
+
|
|
551
|
+
> **The result submitted on-chain is your container's combined stdout + stderr.**
|
|
552
|
+
|
|
553
|
+
`/output` is mounted read-write, but **nothing reads it.** The orchestrator collects the
|
|
554
|
+
container's logs, and then deletes the entire staging tree — `/output` included. Writing
|
|
555
|
+
your result to a file there means submitting an empty result.
|
|
556
|
+
|
|
557
|
+
The consequences:
|
|
558
|
+
|
|
559
|
+
- **stdout and stderr are interleaved** into one stream in chronological order. Anything
|
|
560
|
+
you log for diagnostics — progress bars, warnings, a stray library banner on stderr —
|
|
561
|
+
is concatenated into the on-chain result. Emit the result and nothing else; send
|
|
562
|
+
diagnostics nowhere, or accept that they become part of your output.
|
|
563
|
+
- **Output is read as UTF-8 text.** Binary written to stdout is decoded lossily and
|
|
564
|
+
corrupted. Base64- or hex-encode anything that is not text.
|
|
565
|
+
- **Keep it small.** The result is embedded *inline* in the `compute.result` extrinsic, so
|
|
566
|
+
it is bounded by extrinsic and block size limits — far below the gateway's 100 MiB log
|
|
567
|
+
ceiling. That ceiling protects the host's disk; it is not a budget for your result.
|
|
568
|
+
- **Encryption is applied for you.** If `contract.result_cipher` is not `"Plaintext"`, the
|
|
569
|
+
orchestrator encrypts your bytes to the requester before submission. Emit plaintext.
|
|
570
|
+
|
|
571
|
+
### 6.4 Exit codes and execution outcome
|
|
572
|
+
|
|
573
|
+
Your exit code selects the `ExecutionOutcome` reported on-chain (§5):
|
|
574
|
+
|
|
575
|
+
| Container ends with | `ExecutionOutcome` |
|
|
576
|
+
|---|---|
|
|
577
|
+
| Exit code `0` | `Success` |
|
|
578
|
+
| Any non-zero exit code | `Failed` |
|
|
579
|
+
| Exceeds its timeout (container is force-removed) | `Terminated` |
|
|
580
|
+
| Orchestrator cannot determine the outcome | `Failed` |
|
|
581
|
+
|
|
582
|
+
**A failed run still submits a result and is still billed.** Compute time is metered from
|
|
583
|
+
container start regardless of outcome, so a crash costs the requester real money. Exit
|
|
584
|
+
non-zero to signal failure honestly — but note the logs captured up to that point are what
|
|
585
|
+
gets submitted as the result.
|
|
586
|
+
|
|
587
|
+
### 6.5 What you cannot rely on
|
|
588
|
+
|
|
589
|
+
Two fields look available from the contract but do not currently reach your container:
|
|
590
|
+
|
|
591
|
+
- **Environment variables.** `ComputeInfo.programEnv` exists on-chain, but the orchestrator
|
|
592
|
+
reads `env` from a top-level extrinsic argument that `compute.agreement` does not have.
|
|
593
|
+
A chain-submitted job therefore runs with **no environment variables** from the contract.
|
|
594
|
+
- **Port publications.** The orchestrator looks for `ports` on the compute step, but
|
|
595
|
+
`ComputeInfo` has no such field. Ports are reachable only through the orchestrator's
|
|
596
|
+
direct HTTP job API, not from an on-chain contract.
|
|
597
|
+
|
|
598
|
+
Design your image to take everything it needs from `/input`.
|
|
599
|
+
|
|
600
|
+
Also note that `contract.compute.deadline` is documented on-chain as a **block number**
|
|
601
|
+
(§3.3) but is consumed by the orchestrator as a **timeout in seconds**, defaulting to 300
|
|
602
|
+
when zero or absent. Until that is reconciled, treat it as your container's wall-clock
|
|
603
|
+
budget in seconds.
|
|
604
|
+
|
|
605
|
+
### 6.6 The built-in inference path
|
|
606
|
+
|
|
607
|
+
`{ NativeExecute: "Inference" }` runs no image of yours. The orchestrator reads the **first**
|
|
608
|
+
input file as a complete OpenAI-compatible `/v1/chat/completions` request body, POSTs it to
|
|
609
|
+
Ollama, and submits the raw JSON response as the result. Supply a full request body as your
|
|
610
|
+
input — not a bare prompt. `"ContractAccess"` is defined on-chain but not implemented by the
|
|
611
|
+
orchestrator; any other `NativeExecute` command is rejected.
|
|
612
|
+
|
|
613
|
+
### 6.7 Minimal example
|
|
614
|
+
|
|
615
|
+
```dockerfile
|
|
616
|
+
FROM python:3.12-slim
|
|
617
|
+
COPY main.py /main.py
|
|
618
|
+
ENTRYPOINT ["python", "/main.py"]
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
```python
|
|
622
|
+
import os, sys, json, base64
|
|
623
|
+
|
|
624
|
+
input_dir = "/input"
|
|
625
|
+
files = sorted(os.listdir(input_dir)) # never hard-code the filename
|
|
626
|
+
with open(os.path.join(input_dir, files[0]), "rb") as f:
|
|
627
|
+
payload = f.read()
|
|
628
|
+
|
|
629
|
+
result = {"length": len(payload), "sha": base64.b64encode(payload[:8]).decode()}
|
|
630
|
+
|
|
631
|
+
# The result is stdout. Nothing else may be printed - not even to stderr.
|
|
632
|
+
sys.stdout.write(json.dumps(result))
|
|
633
|
+
sys.exit(0)
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## 7. Known drift — read before debugging
|
|
639
|
+
|
|
640
|
+
Verified against the current `dev` branches at the time of writing. These are real
|
|
641
|
+
inconsistencies between the repositories, not documentation gaps.
|
|
642
|
+
|
|
643
|
+
1. **`joinGuardian` now tracks the `dev` runtime, not the deployed one.** `spec.ts` and
|
|
644
|
+
`GuardianJoinPrefs` were updated together to `fee_thresholds: Vec<(ComputeType, u128)>`,
|
|
645
|
+
matching `dev`. A runtime predating that change has either a scalar `fee_threshold` or
|
|
646
|
+
no threshold field at all, and encoding against it fails or silently drops the value —
|
|
647
|
+
check which runtime you target before debugging a threshold that did not take effect.
|
|
648
|
+
|
|
649
|
+
Note that extrinsic arguments are encoded from *metadata*, not from `spec.ts` —
|
|
650
|
+
`api.tx.staking.guard` resolves `prefs` through the metadata lookup id, so the
|
|
651
|
+
`GuardianPrefs` entry in `spec.ts` is documentation, and the *shape* `joinGuardian`
|
|
652
|
+
passes is what actually has to match.
|
|
653
|
+
|
|
654
|
+
The general rule this illustrates: **entries in `spec.ts` that duplicate a metadata type
|
|
655
|
+
are inert** — metadata wins for everything it describes. Only types reached by *name*
|
|
656
|
+
(custom RPCs in `API_RPC`, signed extensions in `API_EXTENSIONS`) are load-bearing.
|
|
657
|
+
|
|
658
|
+
2. **`MillisecondsPerBlock` is not in metadata.** It is a plain `Get<u64>` on
|
|
659
|
+
`pallet_compute::Config`, not a `#[pallet::constant]`, so `api.consts.compute` does not
|
|
660
|
+
expose it. `getFeeParams()` reads `babe.expectedBlockTime` instead — the runtime wires
|
|
661
|
+
both to the same `MILLISECS_PER_BLOCK`. Adding `#[pallet::constant]` would make this
|
|
662
|
+
exact rather than inferred.
|
|
663
|
+
|
|
664
|
+
3. **The orchestrator assumes 6000ms blocks.** `computeMaxRunningTimeSecs` in
|
|
665
|
+
`orchestrator/src/chain.ts` hard-codes `blockTimeMs = 6000`, while the runtime's
|
|
666
|
+
`MILLISECS_PER_BLOCK` is `500`. Treat its running-time estimates as unreliable.
|
|
667
|
+
|
|
668
|
+
4. **`scripts/test-compute-result.mjs` is stale.** It calls `compute.result` with three
|
|
669
|
+
arguments; the extrinsic now takes five.
|
|
670
|
+
|
|
671
|
+
5. **`ComputeInfo` has fields the SDK helpers omit.** `program_env` and `metadata` exist
|
|
672
|
+
on the chain struct but are not set by `simpleCompute`, `inferenceCompute` or
|
|
673
|
+
`dataContract`. Supply them explicitly if you need them.
|
|
674
|
+
|
|
675
|
+
6. **`deadline` means two different things.** `ComputeInfo.deadline` is documented on-chain
|
|
676
|
+
as a block number and used as one by `compute.invoke`'s expiry check, but the
|
|
677
|
+
orchestrator reads the same field as a **timeout in seconds** when running a container.
|
|
678
|
+
At 500ms blocks, a value meant as N blocks (N/2 seconds) becomes an N-second container
|
|
679
|
+
budget — twice the intended window.
|
|
680
|
+
|
|
681
|
+
7. **`/output` is a dead mount.** The orchestrator bind-mounts
|
|
682
|
+
`<staging>/output` at `/output` read-write, then never reads it and deletes the staging
|
|
683
|
+
tree after submission. The result channel is stdout (§6.3). Either the mount should be
|
|
684
|
+
removed or it should be collected — as it stands it silently invites data loss.
|
|
685
|
+
|
|
686
|
+
8. **`programEnv` and ports never reach the container.** The orchestrator sources `env` and
|
|
687
|
+
`ports` from top-level extrinsic arguments that `compute.agreement` does not define, so
|
|
688
|
+
the on-chain `ComputeInfo.programEnv` field is inert (§6.5).
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## 8. The `ComputePayload` signer option
|
|
693
|
+
|
|
694
|
+
Several SDK calls pass an `opts` object into `signAndSend` that is neither a normal
|
|
695
|
+
extrinsic argument nor a standard signer option:
|
|
696
|
+
|
|
697
|
+
```ts
|
|
698
|
+
{ compute: { daType: 1, verification: 0, compute: 1 } }
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
This is `ComputePayload`, extra data carried by the `CheckCompute` **signed extension** —
|
|
702
|
+
it travels with the transaction and is validated before dispatch. `CheckCompute` requires
|
|
703
|
+
it to be *default/empty* for every call except an allowlist: `dataAvailability.submitData`,
|
|
704
|
+
`compute.agreement`, `compute.result`, `compute.invoke`, and
|
|
705
|
+
`guardSession.agreementResponse`. Attaching a non-default payload to any other call is
|
|
706
|
+
rejected as `ForbiddenCompute`.
|
|
707
|
+
|
|
708
|
+
| Field | Meaning |
|
|
709
|
+
|---|---|
|
|
710
|
+
| `daType` | `0` none, `1` DA, `4` compute request |
|
|
711
|
+
| `compute` | `0` dormant, `1` active |
|
|
712
|
+
| `verification` | Verification mode |
|
|
713
|
+
| `agreement` | 32-byte guardian peer keys (base58-decode the peer ID, drop the first 6 bytes) |
|
|
714
|
+
|
|
715
|
+
The SDK sets this for you in `createAgreement`, `submitData` and the encrypted-inference
|
|
716
|
+
helpers. You need to construct one by hand only when calling `api.tx` directly.
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
## 9. Failure reference
|
|
721
|
+
|
|
722
|
+
| Symptom | Cause |
|
|
723
|
+
|---|---|
|
|
724
|
+
| `InsufficientFreeBalance` | `fees` below the floor (§2.2), or free balance below `fees`. Call `estimateMinFee`. |
|
|
725
|
+
| `ZeroComputeRate` | `computeRate` is 0 on an `Active`/`Subscription` contract. Only `Dormant` may be 0. |
|
|
726
|
+
| `AgreementFailed` event, nothing reserved | A guardian rejected the offer — `computeRate` under its threshold (§2.4). |
|
|
727
|
+
| `ForbiddenCompute` | Non-default `ComputePayload` on a call not in the allowlist (§8). |
|
|
728
|
+
| `Invalid: Custom(149)` | `guardians` is empty while `compute.program` is not `"Null"` (§3.2). |
|
|
729
|
+
| `Invalid: Custom(145)` | Duplicate entries in `guardians` (§3.2). |
|
|
730
|
+
| `Invalid: Custom(148)` | A named guardian is not registered/staked (§3.2). |
|
|
731
|
+
| Opaque decode failure on `agreement` | Wrong field shape — snake_case keys, or a missing enum payload (§3). |
|
|
732
|
+
| `ComputeDurationTooLarge` | Reported duration exceeds `(elapsed_blocks + 4) × block_ms` (§5). |
|
|
733
|
+
| `InvalidContractType` | `invoke` called on a non-`Subscription` contract. |
|
|
734
|
+
| `ContractSettled` | Contract already settled — budget exhausted or deadline passed. |
|
|
735
|
+
| `AgreementNotFound` | Unknown contract ID, or a `Subscription` with no settlement record. |
|
|
736
|
+
| `ContractExpired` | Past the contract deadline. |
|
|
737
|
+
| `invoke` returns Ok but nothing runs | Deadline or budget check settled the contract instead (§3.6). |
|
|
738
|
+
| `compute.agreement` hangs, never included, no error | A named guardian has not submitted its `agreement_response`; the tx is parked in the future queue (§2.1). |
|
|
739
|
+
| Result is empty on-chain | The image wrote to `/output`; only stdout is collected (§6.3). |
|
|
740
|
+
| Result contains log noise or banners | stderr is interleaved into stdout (§6.3). |
|
|
741
|
+
| Result is corrupted / mojibake | Binary written to stdout; it is decoded as UTF-8 (§6.3). |
|
|
742
|
+
| Container sees no environment variables | `programEnv` is not plumbed through (§6.5). |
|
|
743
|
+
| Image pull fails for a `Url` program | `Url` expects a `docker save` tarball, not a registry reference (§6.1). |
|
|
744
|
+
| Group info cannot be found | No on-chain storage for groups; you need the creation block+index (§4.3). |
|
|
745
|
+
| API disconnects mid-flow | `getGuardianParticipants` disconnects the shared API on exit (§4.1). |
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
|
|
749
|
+
## 10. Conventions
|
|
750
|
+
|
|
751
|
+
- **Amounts.** PALI has 18 decimals. `Fee.amount` and `Fee.computeRate` take *human* PALI
|
|
752
|
+
values (`"1.5"`); `buildFee` converts via `toAtomicPaliAmount`. Everything read back off
|
|
753
|
+
the chain is in atomic units — convert with `fromAtomicPaliAmount`.
|
|
754
|
+
- **`computeRate` is per millisecond**, not per block or per second.
|
|
755
|
+
- **`deadline: 0` means no deadline.**
|
|
756
|
+
- **`computerIndices`** indexes into `contract.guardians`, as does `Trusted { trust_index }`.
|
|
757
|
+
- **Finality.** `signAndSend` resolves at in-block by default; pass
|
|
758
|
+
`init({ txWaitFinalization: true })` to wait for finalization.
|
|
759
|
+
- **Currency.** `currencyId` defaults to `"Native"`; the deposit is reserved in and settled
|
|
760
|
+
from that currency.
|