Skip to content
Mailing Lists/CIP-TBD: Pinned External Data Fetches for Daml Contracts - Elliott DehnbostelSource on lists.sync.global ↗

CIP-TBD: Pinned External Data Fetches for Daml Contracts - Elliott Dehnbostel

cip-discuss6 messagesstarted 13-04-2026
Also mentions:CIP-0043CIP-0044CIP-0079CIP-0091
  1. #1Elliott Dehnbostel13-04-2026source ↗
    Pinned External Data Fetches for Daml Contracts
     
    CIP: ?
    Layer: Consensus (hard fork)
    Title: Pinned External Data Fetches
    Author: Elliott G. Dehnbostel
    Discussions-To: https://lists.sync.global/g/cip-discuss
    Comments-Summary: No comments yet
    Status: Draft
    Type: Standards Track
    Created: 2026-04-13
    License: CC0-1.0

    Abstract

    This CIP introduces a mechanism for Daml contract choices to issue TCP-based
    requests to external services during transaction construction and pin the
    cryptographically signed response data to the transaction. On validation and
    replay, participants verify the pinned signature rather than re-executing the
    external call, preserving Canton's deterministic execution model while enabling
    smart contracts to consume off-chain data natively.


    Specification

    Overview

    Today, Daml contracts are purely deterministic: given the same inputs, every
    participant that validates a transaction arrives at the same result. External
    data (e.g., price feeds, KYC attestations, off-chain state) must be brought
    on-chain through application-level orchestration outside the contract model.

    This CIP adds a new ledger-level primitive that allows a choice body to declare
    an external data fetch. During transaction construction the submitting
    participant executes the fetch; during validation all other participants verify
    the pinned response instead of repeating the call.

    External Data Fetch Primitive

    A new Daml built-in is introduced:

    ```daml
    fetchExternal : FetchRequest -> Update FetchResponse
    ```

    Where:

    ```daml
    data FetchRequest = FetchRequest
    { endpoint : Text -- TCP endpoint as "host:port"
    , payload : ByteString -- request bytes sent to the service
    , signerKeys : [PublicKey] -- accepted signing keys for the response
    , maxBytes : Int -- maximum response size in bytes
    , timeoutMs : Int -- network timeout in milliseconds
    }

    data FetchResponse = FetchResponse
    { body : ByteString -- response payload
    , signature : ByteString -- cryptographic signature over (body, nonce)
    , signerKey : PublicKey -- which key from signerKeys signed
    , fetchedAt : Timestamp -- wall-clock time of the fetch
    }
    ```

    Transaction Construction (Submitter)

    When the Daml engine encounters a `fetchExternal` call during command
    interpretation on the submitting participant:

    1. Open a TCP connection to `endpoint`.
    2. Send `payload` prefixed with a 32-byte transaction-bound nonce generated
    by the engine. The nonce binds the response to this specific transaction
    to prevent replay of stale signed data across transactions.
    3. Read up to `maxBytes` of response data within `timeoutMs`.
    4. Verify that the response carries a valid signature from one of the
    `signerKeys` over `SHA-256(nonce || body)`.
    5. If verification succeeds, record the `FetchResponse` as a **pinned data
    node** in the transaction view alongside the usual create/exercise nodes.
    6. Return the `FetchResponse` to the Daml engine so the choice body can
    branch on the data.

    If the connection fails, times out, or the signature is invalid, command
    interpretation fails and no transaction is submitted.

    Transaction Validation (All Other Participants)

    When a confirming participant or mediator processes a transaction containing
    pinned data nodes:

    1. Extract `(body, signature, signerKey, nonce)` from the pinned data node.
    2. Verify that `signerKey` is in the `signerKeys` declared in the original
    `FetchRequest`.
    3. Verify the signature over `SHA-256(nonce || body)`.
    4. Supply the verified `FetchResponse` to the Daml engine as a fixed input
    (no network call is made).
    5. Re-execute the choice body deterministically using the pinned data.

    If signature verification fails, the participant rejects the transaction.

    Nonce Generation

    The transaction-bound nonce is derived as:

    ```
    nonce = SHA-256(transaction_uuid || fetch_node_index)
    ```

    Where `transaction_uuid` is the unique identifier assigned during command
    interpretation and `fetch_node_index` is the zero-based index of the
    `fetchExternal` call within the transaction. This ensures each fetch within
    a transaction gets a unique nonce and that nonces cannot be predicted before
    transaction construction begins.

    External Service Protocol

    External services that wish to be callable via `fetchExternal` must implement
    the following TCP protocol:

    1. Accept a connection.
    2. Read the first 32 bytes as the nonce.
    3. Read the remaining bytes as the application-level request payload.
    4. Compute the response `body`.
    5. Sign `SHA-256(nonce || body)` with the service's private key.
    6. Return `length(body) as 4-byte big-endian || body || signature`.
    7. Close the connection.

    Services advertise their public keys out-of-band (e.g., via CNS metadata
    entries or CIP governance). Contract authors embed the accepted keys in
    their `FetchRequest.signerKeys`.

    Privacy

    Pinned data nodes inherit the same visibility rules as the transaction view
    they belong to. Only stakeholders and confirming participants of that view
    see the fetched data. The external service endpoint address is visible to
    all confirming participants of the view but not to non-stakeholder observers.

    Size and Cost Limits

    - `maxBytes` is capped at a protocol-level maximum (proposed: 64 KiB) to
    prevent pinned data from inflating transaction sizes unboundedly.
    - Pinned data counts toward the transaction's traffic weight for
    synchronizer fee calculation, using the same bytes-based accounting as
    other transaction nodes.
    - A transaction may contain at most 8 `fetchExternal` calls. This limit
    is a protocol parameter adjustable by CIP.

    Daml Surface Syntax

    To make the primitive ergonomic, a standard library module is provided:

    ```daml
    module DA.External (fetchExternal, FetchRequest(..), FetchResponse(..)) where
    ...
    ```

    Contract authors use it as:

    ```daml
    choice GetPrice : Decimal
    with
    oracle : FetchRequest
    controller owner
    do
    resp <- fetchExternal oracle
    let price = parseDecimal (toText resp.body)
    -- use price in contract logic
    pure price
    ```

    Motivation

    Canton's privacy and determinism guarantees are a core strength, but they come
    at the cost of isolation from external systems. Today, bringing off-chain data
    on-chain requires bespoke application-level plumbing:

    - Price feeds (CIP-0079): Super Validators ingest Kaiko prices and
    republish them via dedicated Daml contracts and automation triggers. Every
    new data source requires a new contract model, a new automation pipeline,
    and a new CIP to govern the feed.

    - KYC/AML attestations: Compliance data from providers like TRM
    (CIP-0043) or Elliptic (CIP-0044) must be manually bridged into the
    ledger, introducing latency and trust assumptions at the application layer.

    - Cross-chain state: Verifying state on other ledgers (e.g., confirming
    an L1 deposit before releasing Canton-side assets) requires off-chain
    relayers with no standard interface.

    Each of these follows the same pattern: fetch external data, attest to its
    authenticity, and feed it into contract logic. A general-purpose primitive
    would eliminate redundant infrastructure, reduce the governance overhead of
    adding new data sources, and let contract authors integrate external data
    without waiting for purpose-built pipelines.

    Furthermore, the TCP-level primitive (rather than HTTP) keeps the protocol
    layer lean and avoids encoding opinions about application-layer protocols
    into the consensus layer. Contract authors and library developers can build
    HTTP, gRPC, or any other protocol on top.


    Rationale

    Why pin-and-verify rather than re-execute?

    Re-executing the external call on every validating participant would require
    all participants to have network access to the same service, would leak
    information about which participants are validating which transactions, and
    would introduce non-determinism if the service returns different data across
    calls. Pinning the response and verifying the signature ensures deterministic
    replay while requiring only the submitter to have network access.

    Why TCP rather than HTTP?

    HTTP is an application-layer protocol with significant complexity (headers,
    content encoding, chunked transfer, TLS negotiation). Embedding an HTTP
    client in the Daml engine would increase the attack surface and create
    implicit dependencies on TLS certificate infrastructure. Raw TCP keeps the
    consensus-layer primitive minimal. Higher-level protocols can be implemented
    in Daml libraries or external service wrappers that speak the nonce-sign
    protocol over TCP.

    Why not use existing oracle patterns?

    Existing oracle patterns on Canton (e.g., CIP-0079 price feeds) require:

    1. A dedicated contract model per data type
    2. Off-chain automation to poll and publish
    3. A governance CIP to authorize each new feed

    This works for a small number of high-value feeds but does not scale to the
    long tail of external data integrations that application developers need. A
    protocol-level primitive shifts the trust boundary from "trust the oracle
    operator's automation" to "trust the oracle operator's signing key" -- a
    strictly smaller trust surface.

    Why limit to 64 KiB and 8 fetches per transaction?

    These limits prevent abuse while covering the vast majority of use cases
    (price quotes, attestation certificates, Merkle proofs). Larger or more
    frequent data needs should use the existing pattern of publishing data to
    dedicated contracts. The limits are protocol parameters and can be adjusted
    by future CIPs as the network matures.

    Alternatives considered

    Callback model: The external service pushes data on-chain proactively,
    and contracts read it from existing contracts. This is the status quo
    (CIP-0079). It works but requires per-source infrastructure and does not
    support request-response patterns where the query depends on contract state.

    Trusted execution enclaves (TEE): Run the fetch inside a TEE on the
    submitting participant and attest the result. This would avoid needing the
    external service to implement the signing protocol but introduces hardware
    dependencies and a much larger trust surface. It could be explored as a
    complementary mechanism in a future CIP.

    Threshold-signed fetches: Require multiple participants to independently
    fetch and co-sign the response before it is pinned. This provides stronger
    guarantees against a compromised submitter colluding with the external service
    but adds latency and complexity. It is a natural extension of this CIP and
    could be layered on top.


    Backwards Compatibility

    This is a hard fork change. All participants (Super Validators and
    Validators) must upgrade to a Canton version that understands pinned data
    nodes in the transaction format. Transactions containing `fetchExternal`
    calls submitted to participants running older software will be rejected.

    - Contracts that do not use `fetchExternal` are unaffected.
    - The transaction format is extended with a new node type; older parsers
    will fail on transactions containing pinned data nodes.
    - The Daml engine must be updated to support the new built-in.
    - Sequencer and mediator message formats are extended to carry pinned data.

    A phased rollout is recommended:

    1. Phase 1: Release Canton version with `fetchExternal` support.
    Contracts using the primitive can be deployed but not yet exercised on the
    Global Synchronizer.
    2. Phase 2: Once 2/3 of Super Validators have upgraded and vetted the
    new Daml packages, enable `fetchExternal` via an on-chain protocol
    parameter change.


    Reference Implementation

    To be provided. The implementation will span:

    - Canton protocol: New `PinnedDataNode` in the transaction tree
    representation, validation logic in the mediator and confirming participants.
    - Daml engine: New `fetchExternal` built-in with TCP execution during
    interpretation and pinned-data replay during validation.
    - Daml standard library: `DA.External` module exposing the types and
    primitive.
    - External service SDK: Reference implementation of the nonce-sign TCP
    protocol in Python and TypeScript.


    Copyright

    This CIP is licensed under CC0-1.0: [Creative Commons CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/)


    Changelog

    * 2026-04-13: Initial draft of the proposal.
  2. #2Wayne Collier15-04-2026source ↗
    Hi, Elliot
     
    This might be better positioned as a grant request. You don't need a separate CIP; rather if this were adopted it would be included the CIP for an upcoming protocol change after the work is complete. 
     
    Zenith has submitted a proposal for similar capabilities, but with a different technical implementation. You'll probably want to comment on and/or compare your approach with theirs in your proposal. 
  3. #3Elliott Dehnbostel16-04-2026source ↗
    Thanks Wayne,

    Do you happen to have a link to their proposal? I cannot find it immediately. Will look at submitting for a grant!

    Best,
    Elliott

    toggle quoted message Show quoted text

    On Wed, Apr 15, 2026 at 12:37 PM Wayne Collier via lists.sync.global <wayne.collier=digitalasset.com@...> wrote:
    Hi, Elliot
     
    This might be better positioned as a grant request. You don't need a separate CIP; rather if this were adopted it would be included the CIP for an upcoming protocol change after the work is complete. 
     
    Zenith has submitted a proposal for similar capabilities, but with a different technical implementation. You'll probably want to comment on and/or compare your approach with theirs in your proposal. 

  4. #4Heslin Kim17-04-2026source ↗
    Hi Elliott,
     
    You can find our CIP here at 0091: 
     
    We would be happy to discuss your architecture as well and ways to improve functionality and utility without overlapping.
     
    Feel free to email me at heslin.kim@... and we can coordinate a call with our solutions architect.
     
    Best,
    Heslin
  5. #5Elliott Dehnbostel17-04-2026source ↗
    Hi everyone,
     
    Thanks for the pointer to CIP-0091. I've read through it in detail
    and I think there's a natural convergence point between
    external_call() and our fetchExternal primitive.
     
    The core observation: external_call() assumes every participant
    can execute the call locally and arrive at the same result. That
    works when the external service is deterministic and universally
    deployed. But in Canton's multi-participant model — especially
    with multi-host topologies — you can't guarantee that every
    participant runs the same local service.
     
    fetchExternal solves an analogous problem for remote data with a
    pin-and-verify model: the submitter executes once, pins the
    cryptographically signed result, and validators verify the
    signature rather than re-executing.
     
    Here's what I think the unified model looks like:
     
      fetchExternal(request) -> PinnedResult
     
    If the request can be resolved to a locally-running service (e.g.
    0.0.0.0:8545 for an EVM node), the submitter's participant
    executes it locally, pins the result, and attaches it to the transaction.
    If the endpoint is strictly non-local (e.g. a price feed), same flow —
    fetch, pin, attach.
     
    This means that any "local service" used in a contract will need
    to expose its capabilities to remote fetchers.
     
    From the Daml contract's perspective, both cases look identical.
    From the validator's perspective, both cases look identical —
    verify the signature on the pinned result, done. The contract
    doesn't know or care whether the result came from a local EVM
    node or a remote API.
     
    This gives external_call() two properties it doesn't currently
    have:
      1. It works across heterogeneous participants. The submitter
         runs the EVM, everyone else verifies the pin. You don't need
         every participant to run every VM — just the submitter.
      2. It composes with Template-Bound Parties. A TBP contract
         hosted on multiple participants for liveness can use
         external_call() without requiring every host to run the
         same local services.
     
    This also simplifies Zenith's adoption path. Instead of "every
    participant on the synchronizer must run our VB stack," it becomes
    "one participant runs the VB stack, submits transactions, and
    pins the results." Much lower barrier.
     
    We have a working implementation of the pin-and-verify model in
    the Canton codebase (PR #491) and end-to-end integration tests
    for Template-Bound Parties including a constant-product AMM
    (PR #503). Happy to share the implementation details on a call.
     
    I think the unified primitive is a stronger CIP than either one
    alone. Interested to hear your thoughts.
     
    Best,
    Elliott Dehnbostel
  6. #6Norbert Vadas22-04-2026source ↗
    Hi Elliott,

    Thanks for the detailed write-up and for taking the time to read CIP-0091. I am sharing with you below our view on how external_call() and fetchExternal relate.

    We see fetchExternal and external_call() as targeting genuinely very different use cases. fetchExternal addresses non-deterministic remote data, such as price feeds, time-dependent API responses, where re-execution on every validator isn't viable and pin-and-verify is the natural model. For these use cases, there is clear value in a primitive designed for it.
    The external_call() was built for the opposite end of the spectrum: deterministic local computation, specifically EVM transaction execution and state root verification, where every confirming participant can and must reproduce the result identically.

    The design of external_call() was specifically discussed and agreed on with members of the Digital Asset team back in Q3 2025. To avoid introducing any additional trust assumptions beyond Canton's existing model, external_call() follows Canton's native two-step validation process:
    1. The submitter executes the function during interpretation and includes the result (and optionally its hash and metadata) as part of the transaction view.
    2. Each validator re-executes the same external_call() locally.
    3. If any validator obtains a different output, validation fails, mirroring the same determinism rule that applies to every existing Daml primitive.
    This is intentional. For local EVM execution producing new state roots, the integrity guarantee has to come from deterministic re-execution and result comparison, not from trusting a signed pin from the submitter. Moving to a pin-and-verify model for this use case would replace "trust via re-execution" with "trust via the submitter's signing key," which would be a meaningfully weaker property for the EVM state transition validation and isn't compatible with the trust model CIP-0091 is built on.

    Even though the primitive was built with EVM execution in mind, as requested by DA's engineering team in late 2025, the scope was extended so that results of the external_call() are included in the exercise node. This allows observers with no access to the external service to validate transactions. More specifically, confirming participants must re-execute and compare results while observers can re-execute or deterministically replay using recorded outputs. The external_call() results are embedded in exercise nodes, enabling observers to replay transactions deterministically, and validate them without trusting other participants.  No trust in submitter is required.
    Regarding the participants that don't run the EVM extension: the transaction preparation phase handles this cleanly. Before a transaction is committed on-chain, a Canton participant not running the external call service can call /prepare on a Zenith participant hosting the EVM and delegate preparation of the EVM payload. The prepared payload is shared back, and the submitting participant submits the user transaction with the wrapped payload included. On the validation side, confirming participants that host contracts using external_call() do need the required extension configured, this is the intended behavior, and it's how determinism via re-execution is preserved.
     
    As mentioned, we see fetchExternal and external_call() as targeting genuinely different use cases, but we are happy to follow the work around fetchExternal while we progress towards bringing EVM compatibility to Canton through external_call().
     
    Thank you and best regards,
    Norbert