XChain Platform Action - ATTEST
External-data attestation lifecycle in a single action with three version-discriminated phases:
- v0 — Request. VM emission only. Emitted by
xchain.attestation.request(...)during EXECUTE. Indexer escrows the gas estimate and records apendingrow; staked validators with theattestationcapability pick the request up. - v1 — Response. Broadcast by the leader validator on behalf of the PBFT quorum. Carries the agreed-upon response body plus Ed25519 signatures.
- v2 — Expire. System-injected synthetic action. Fired by the indexer’s per-block expiry pipeline when a v0 request passes its
DEADLINE_BLOCKwithout an accepted v1 response. Not user-broadcastable.
For the full design see claude/reports/specs/2026-05-24_external-attestation-framework.md.
PARAMS
| Name | Type | Versions | Description |
|---|---|---|---|
VERSION |
Integer | all | Format version (0=request, 1=response, 2=expire) |
REQUEST_ID |
String | all | 64-hex SHA-256 over tx_hash ‖ contract_index ‖ emitter_position |
PROVIDER_ID |
String | 0, 1 | Governance-registered provider (http_get, llm, …) |
REQUEST_PAYLOAD |
String | 0 | Provider-specific payload (URL for http_get, JSON envelope for llm) |
CALLBACK_METHOD |
String | 0 | Contract method to invoke on response (≤64 chars) |
CALLBACK_PARAMS_JSON |
String | 0 | JSON array of developer-supplied params, echoed back to callback (each element string-coerced at injection — see §Effects) |
REDUNDANCY |
Integer | 0 | Required validator signatures (1, 3, or 5) |
DEADLINE_BLOCKS |
Integer | 0 | Blocks until the request auto-expires (provider’s deadline_window_blocks cap) |
FEE_TICK |
String | 0 | (optional) Tick the attestation fee is paid in. v1 consensus accepts only the GAS tick (XCHAIN). Required when FEE_AMOUNT > 0. |
FEE_AMOUNT |
String | 0 | (optional) Attestation fee, precision ≤ the GAS tick’s own decimals (8 for the production XCHAIN genesis issuance). > 0 escrows the fee from FEE_PAYER at request time. Absent / 0 ⇒ feeless (zero behavior change). |
RESPONSE_PAYLOAD |
String | 1 | Inline response body (UTF-8). Binary bodies not supported in v0. |
STATUS |
String | 1 | ok | timeout | no_quorum | provider_error | expired |
META |
String | 1 | Provider-defined metadata (HTTP status code for http_get; model ID for llm) |
SIG_COUNT |
Integer | 1 | Number of (pubkey, sig) pairs that follow |
PUBKEY_n |
String | 1 | 64-hex Ed25519 pubkey, qualified for attestation at the request block |
SIG_n |
String | 1 | 128-hex Ed25519 signature over the canonical message |
Formats
Version 0 — Request (VM-emitted)
ATTEST|0|REQUEST_ID|PROVIDER_ID|REQUEST_PAYLOAD|CALLBACK_METHOD|CALLBACK_PARAMS_JSON|REDUNDANCY|DEADLINE_BLOCKS[|FEE_TICK|FEE_AMOUNT]- The trailing
FEE_TICK|FEE_AMOUNTpair is optional. A feeless request omits them entirely (the SDK serializer trims trailing empties), so feeless v0 wire strings are byte-identical to the pre-fee format — no migration.
Version 1 — Response (validator-broadcast, variable-length signature list)
ATTEST|1|REQUEST_ID|PROVIDER_ID|RESPONSE_PAYLOAD|STATUS|META|SIG_COUNT|PUBKEY1|SIG1|PUBKEY2|SIG2|...
Version 2 — Expire (system-synthesized; never user-broadcast)
ATTEST|2|REQUEST_ID
Examples
ATTEST|0|abc...def|http_get|https://example.com/v1/score/42|handleResponse|["ctx-42"]|1|10
VM-emitted request for an off-chain HTTP GET, single-validator redundancy, 10-block deadline, feeless
ATTEST|0|abc...def|http_get|https://example.com/v1/score/42|handleResponse|["ctx-42"]|1|10|XCHAIN|0.5
Same request carrying a 0.5 XCHAIN attestation fee (escrowed from FEE_PAYER at request time)
ATTEST|1|abc...def|http_get|{"score":7}|ok|200|1|a1b2...|c3d4...
Validator-broadcast response with one Ed25519 signature
ATTEST|2|abc...def
System-synthesized expiry for request abc...def
Canonical signing message (v1)
Each SIG_n covers the canonical bytes:
request_id || provider_id || sha256(response_payload) || status || meta
Where sha256(response_payload) is the lowercase hex digest of the UTF-8 response bytes.
Rules
Version 0 (request)
- VM emission only — the
IS_EMISSIONflag must be set byexecute.processEmission. User-broadcast v0 is rejected. PROVIDER_IDmust be governance-registered (indexer validates against its provider registry).REDUNDANCYmust appear in the provider’sallowed_redundancylist.REQUEST_PAYLOADsize must be ≤ provider’smax_request_bytes.DEADLINE_BLOCKSmust be> 0and≤provider’sdeadline_window_blocks.CONTRACT_INDEX(carried viaEMITTER) must reference an existing contract.REQUEST_IDis verified by re-deriving fromtx_hash ‖ contract_index ‖ emitter_position(defends against compromised VM).
Fee fields (v0, optional)
FEE_TICK, when present, must equal the GAS tick (XCHAIN) —invalid: FEE_TICK (only XCHAIN accepted)otherwise. Arbitrary fee ticks are a post-launch rule loosening; the wire carries the tick now so no future format change is needed.FEE_AMOUNTmust parse to a precision no finer than the GAS tick’s own decimals —invalid: FEE_AMOUNT (precision > N dp)otherwise (N =min(8, gasDecimals)). The escrow/debit/credit ledger rows round to the tick’s decimals, so a finer fee would be charged rounded whileattests.fee_amountkept the unrounded string, desyncing the reward split from the escrow. The production XCHAIN genesis issuance is pinned to 8 decimals, so in production the cap is 8 dp; on the decimals-0 regtest GAS tick the cap is 0 (integer fees only). The VM gateway (xchain.attestation.request) additionally rejects > 8 dp at emit time as an outer sanity bound.FEE_AMOUNT > 0requires a non-nullFEE_TICK—invalid: FEE_TICK (required when FEE_AMOUNT > 0).FEE_PAYER(the contract address emitting the request) must hold≥ FEE_AMOUNTof the GAS tick —invalid: insufficient funds (FEE_AMOUNT)otherwise. As with any failed emission validation, this fails the whole enclosing EXECUTE.- A valid
FEE_AMOUNT > 0debitsFEE_PAYERand writes an escrow row at the v0action_index. Absent /0⇒ feeless, no ledger movement.
Version 1 (response)
- Indexer rejects if
REQUEST_IDdoesn’t match apendingrow from a prior v0. PROVIDER_IDmust equal the request’s provider.- Indexer’s
BLOCK_INDEXmust be ≤ the request’sDEADLINE_BLOCK. - Each
PUBKEY_nis checked against theattestationcapability snapshot at the request’s block_index (not the response’s — every hub must compute the same set). - Each
SIG_nmust Ed25519-verify against the canonical message underPUBKEY_n. - Valid signature count must be
≥ REDUNDANCY(the request’sREDUNDANCYparameter). Sub-quorum responses are rejected; the request remains pending.
Version 2 (expire)
- Never user-broadcast —
VALID_ACTION_NAMESacceptsATTESTfor the decoder’s v0/v1 paths, but v2 is rejected if it appears in a user transaction. - Synthesized once per stale pending request: indexer queries
SELECT * FROM attestation_requests WHERE request_status='pending' AND deadline_block < <current_block>and synthesizes one v2 per row. REQUEST_IDmust match an existingpendingrow.
Lifecycle
- VM EXECUTE emits ATTEST v0 → indexer stores in
attestation_requestswithrequest_status='pending'. - Validators staked for
attestationcapability detect the request via the hub’sAttestationRoundpolling. - Top-
REDUNDANCYvalidators (deterministic leader sort bySHA-256(request_id ‖ pubkey)) fetch via the provider and gossip ATTEST_PROPOSE. - Leader publishes ATTEST v1 on-chain with
REDUNDANCYEd25519 signatures. - On a terminal v1 the indexer flips the request to
fulfilled(STATUS=ok) orerrored(a genuinely terminal failure such asexpired) and injects a system EXECUTE invoking the callback. A retryable v1 (STATUSofno_quorum,timeout, orprovider_error) is recorded but leavesrequest_status='pending', so the responsible set can attempt another round before the deadline; no callback fires yet. - If
DEADLINE_BLOCKpasses while stillpending(no terminal v1, or only retryable rounds), the indexer’s per-block expiry pipeline synthesizes ATTEST v2 (flips status toexpired, fires the callback withstatus='expired').
Effects on v1 with valid signatures
- Persists into
attestation_responseswith the agreed body + sigs (always — including retryable rounds, for audit). - Terminal statuses flip the matching
attestation_requestsrow:fulfilled(STATUS=ok) orerrored(a terminal failure such asexpired). - Retryable statuses (
no_quorum,timeout,provider_error) leaverequest_status='pending'untouched so a later round can still reach quorum before the deadline (or the v2 expiry path takes over). No status flip and no callback for these. - On a terminal status only, synthesizes an EXECUTE injecting the callback with:
[request_id, provider_id, status, response_payload, ...original_callback_params]- Every
original_callback_paramselement is coerced to a string before injection (the VM parameter bus is string-typed), so a request that supplied[42, true, null]reaches the callback as['42', 'true', 'null']. Contracts must re-parse numeric or boolean context withparseInt,parseFloat, orJSON.parseas needed. SOURCE = contract_addresssoxchain.getSourceAddress() === xchain.getContractAddress()inside the callback.
- Callback wrapped in a savepoint — failure does NOT roll back the response row.
Effects on v2 (expire)
- Creates an entry in the
actionstable (gets a newaction_indexso the synthetic event is replay-deterministic and rollback-correct). - Flips matching
attestation_requests.request_statusfrompendingtoexpired. - Synthesizes an EXECUTE injecting the contract’s callback with:
[request_id, provider_id, 'expired', '', ...original_callback_params]- As on the v1 path, every
original_callback_paramselement is coerced to a string before injection — re-parse typed context inside the callback. SOURCE = contract_address(matches the v1 callback convention).
- Callback wrapped in a savepoint — failure does not roll back the status flip.
Fee flow
When a v0 request carries FEE_AMOUNT > 0, the fee is escrowed from FEE_PAYER at request time and disposed of when the request reaches a terminal state. All movements are GAS-denominated (XCHAIN). Settlement is deterministic across validators (bcmulfloor to GAS decimals; remainder dust stays in the REWARD pool).
| Event | Fee movement |
|---|---|
v0 valid, FEE_AMOUNT > 0 |
Debit FEE_PAYER + escrow row (at the v0 action_index). |
v1 → fulfilled (STATUS=ok) |
Release escrow → credit the REWARD pool; one validator_rewards row per responsible-set pubkey (reward_type='attest_fee', round_reference=request action_index), each bcmulfloor(bcdiv(fee, N, 18), '1', 8). Floor dust stays in the pool. Empty responsible set ⇒ full fee stays in the pool. |
v1 → errored (terminal non-ok, e.g. expired) |
Release escrow → refund FEE_PAYER (service not rendered). |
v1 retryable (no_quorum / timeout / provider_error) |
No movement — escrow stays locked, request stays pending. |
| v2 expiry (synthesized) | Release escrow → refund FEE_PAYER. |
validator_rewards rows are paid out to stakers via COLLECT (the same path as protocol rewards — hence the XCHAIN-only constraint, since that chain has no per-row tick column). A reorg mid-fulfillment rolls back the release + reward rows generically (credits/debits/escrows by action_index, rewards by block_index) and resets request_status to pending; the earlier v0 escrow row survives.
Notes
REQUEST_IDis the cross-version foreign key — every v1 / v2 must reference an existing v0.- The
attestation_requestsandattestation_responsestable names are unchanged from the pre-consolidation design; only the wire action name collapsed. - The optional
FEE_TICK/FEE_AMOUNTrequest fee (above) is live. The separategas_escrow(callback-gas) field remains stubbed at'0'— real callback-gas escrow is Phase 3 economic work, independent of the request fee.
Copyright © 2025–2026 Dankest, LLC
Based on XChain Platform by Dankest, LLC – https://dankest.llc
Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later) with a commercial license available for proprietary use.
You may use, modify, and distribute this material under the terms of the License. See LICENSE and NOTICE for full terms. See the licensing overview.