Skip to content

Request Flow & Consensus

This page describes the planned service request and response model. Concrete endpoint and helper-function names may still change as the feature is implemented.

Services are initiated off the deterministic contract execution path, but their outputs still need validator agreement before any on-chain effect is accepted.

Starting a request

The design supports two request sources, both of which begin their life as transactions from funded wallets:

  1. A wallet submits a service request to a node HTTP endpoint, similar in spirit to contract execution.
  2. A contract triggers a service request via service-request(address, payload).

In both cases, the initial trigger is broadcast to validators as a service request rather than being executed immediately on-chain.

Request acceptance

Validators vote on accepting a request before executing the service:

package chain:service-consensus@1.0.0;

interface request-vote {
  /// On-chain addresses are represented as raw bytes.
  type address = list<u8>;

  /// Vote payload for accepting a service request before execution.
  record service-request-vote {
    /// Service address being invoked.
    service: address,

    /// Opaque input bytes that will be passed to the service.
    payload: list<u8>,

    /// Original caller that will ultimately pay the request's fees.
    origin: address,

    /// Per-service monotonic request number assigned by chain state.
    request-id: u64,
  }
}

origin identifies the payer for downstream fees. request-id is unique per service address.

Service execution and response votes

Once a request is accepted, validators execute the service locally and broadcast derived responses:

package chain:service-consensus@1.0.0;

interface response-vote {
  /// On-chain addresses are represented as raw bytes.
  type address = list<u8>;

  /// Vote payload for one derived service response.
  record service-response-vote {
    /// Stable aggregation key derived from the logical response identity.
    response-id: list<u8>,

    /// Contract to invoke once this response reaches consensus.
    contract: address,

    /// Payload that will be delivered to the target contract.
    payload: list<u8>,

    /// Canonical bytes validators actually compare for consensus.
    consensus-data: list<u8>,

    /// Original request payer/caller.
    origin: address,

    /// Local runtime fuel consumed while producing this response.
    fuel: u64,
  }
}

After enough validators agree on a response, the target contract executes as a normal on-chain transaction with origin as the caller.

Response IDs and aggregation

Responses are aggregated by hashing a logical identifier, not by arrival order:

package chain:service-consensus@1.0.0;

interface response-id {
  /// On-chain addresses are represented as raw bytes.
  type address = list<u8>;

  /// Logical identity for aggregating equivalent responses across validators.
  record service-response-id {
    /// Service that produced the response.
    service: address,

    /// Either the accepted request number or a service-defined event key.
    event-or-request-id: event-or-request-id,
  }

  /// Default aggregation is per request, but services may override that
  /// with an event key when multiple requests describe the same observation.
  variant event-or-request-id {
    /// Service-defined logical event key.
    event(list<u8>),
    /// Chain-assigned request number for this service.
    request(u64),
  }
}

By default, responses are grouped by (service, request-id). A service can override that with event-id when different requests should collapse onto the same logical observation. See Feed or Inbox Watcher for a concrete example of when event-id matters.

Consensus strategies

Each service-response chooses how validators compare outputs:

exact-match

Validators compare the raw contract-data bytes. The response is accepted only if those bytes match across the quorum.

contract-query(address)

Validators call service-consensus-query(payload: list<u8>) -> list<u8> on the referenced contract and compare the returned bytes instead of the raw payload.

This allows a contract to define a deterministic normalization rule. See Oracle for a concrete example where a contract rounds a high-precision price into an agreed bucket before consensus is checked.

The contract ABI hook is documented in the contract WIT reference.

contract-data does not need to be copied from any particular validator's original response. Consensus applies to the agreed comparison value, not necessarily to the highest-fidelity payload any one validator observed.

Request IDs

request-id is tracked in chain state per service address and increments only when the corresponding service-request-vote is accepted.

That means multiple requests can be in flight for a service, but every accepted request still gets a stable per-service sequence number.

Fuel and fees

Service execution is metered similarly to contract execution:

  • there is a base cost for initiating a service request
  • validator execution consumes runtime fuel
  • final fees are charged to the original caller recorded in origin

Because validators may observe slightly different execution costs, final fee accounting is derived from the network's accepted votes rather than from a single machine's local measurement.