CIP-TBD: Pinned External Data Fetches for Daml Contracts - Elliott Dehnbostel
cip-discuss6 messagesstarted 13-04-2026
- Pinned External Data Fetches for Daml ContractsCIP: ?Layer: Consensus (hard fork)Title: Pinned External Data FetchesAuthor: Elliott G. DehnbostelDiscussions-To: https://lists.sync.global/g/cip-discussComments-Summary: No comments yetStatus: DraftType: Standards TrackCreated: 2026-04-13License: CC0-1.0
Abstract
This CIP introduces a mechanism for Daml contract choices to issue TCP-basedrequests to external services during transaction construction and pin thecryptographically signed response data to the transaction. On validation andreplay, participants verify the pinned signature rather than re-executing theexternal call, preserving Canton's deterministic execution model while enablingsmart contracts to consume off-chain data natively.
Specification
Overview
Today, Daml contracts are purely deterministic: given the same inputs, everyparticipant that validates a transaction arrives at the same result. Externaldata (e.g., price feeds, KYC attestations, off-chain state) must be broughton-chain through application-level orchestration outside the contract model.
This CIP adds a new ledger-level primitive that allows a choice body to declarean external data fetch. During transaction construction the submittingparticipant executes the fetch; during validation all other participants verifythe pinned response instead of repeating the call.
External Data Fetch Primitive
A new Daml built-in is introduced:
```damlfetchExternal : FetchRequest -> Update FetchResponse```
Where:
```damldata 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 commandinterpretation on the submitting participant:
1. Open a TCP connection to `endpoint`.2. Send `payload` prefixed with a 32-byte transaction-bound nonce generatedby the engine. The nonce binds the response to this specific transactionto 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 datanode** in the transaction view alongside the usual create/exercise nodes.6. Return the `FetchResponse` to the Daml engine so the choice body canbranch on the data.
If the connection fails, times out, or the signature is invalid, commandinterpretation fails and no transaction is submitted.
Transaction Validation (All Other Participants)
When a confirming participant or mediator processes a transaction containingpinned 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 commandinterpretation and `fetch_node_index` is the zero-based index of the`fetchExternal` call within the transaction. This ensures each fetch withina transaction gets a unique nonce and that nonces cannot be predicted beforetransaction construction begins.
External Service Protocol
External services that wish to be callable via `fetchExternal` must implementthe 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 metadataentries or CIP governance). Contract authors embed the accepted keys intheir `FetchRequest.signerKeys`.
Privacy
Pinned data nodes inherit the same visibility rules as the transaction viewthey belong to. Only stakeholders and confirming participants of that viewsee the fetched data. The external service endpoint address is visible toall 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) toprevent pinned data from inflating transaction sizes unboundedly.- Pinned data counts toward the transaction's traffic weight forsynchronizer fee calculation, using the same bytes-based accounting asother transaction nodes.- A transaction may contain at most 8 `fetchExternal` calls. This limitis a protocol parameter adjustable by CIP.
Daml Surface Syntax
To make the primitive ergonomic, a standard library module is provided:
```damlmodule DA.External (fetchExternal, FetchRequest(..), FetchResponse(..)) where...```
Contract authors use it as:
```damlchoice GetPrice : Decimalwithoracle : FetchRequestcontroller ownerdoresp <- fetchExternal oraclelet price = parseDecimal (toText resp.body)-- use price in contract logicpure price```
Motivation
Canton's privacy and determinism guarantees are a core strength, but they comeat the cost of isolation from external systems. Today, bringing off-chain dataon-chain requires bespoke application-level plumbing:
republish them via dedicated Daml contracts and automation triggers. Everynew 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 TRMledger, introducing latency and trust assumptions at the application layer.
- Cross-chain state: Verifying state on other ledgers (e.g., confirmingan L1 deposit before releasing Canton-side assets) requires off-chainrelayers with no standard interface.
Each of these follows the same pattern: fetch external data, attest to itsauthenticity, and feed it into contract logic. A general-purpose primitivewould eliminate redundant infrastructure, reduce the governance overhead ofadding new data sources, and let contract authors integrate external datawithout waiting for purpose-built pipelines.
Furthermore, the TCP-level primitive (rather than HTTP) keeps the protocollayer lean and avoids encoding opinions about application-layer protocolsinto the consensus layer. Contract authors and library developers can buildHTTP, 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 requireall participants to have network access to the same service, would leakinformation about which participants are validating which transactions, andwould introduce non-determinism if the service returns different data acrosscalls. Pinning the response and verifying the signature ensures deterministicreplay 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 HTTPclient in the Daml engine would increase the attack surface and createimplicit dependencies on TLS certificate infrastructure. Raw TCP keeps theconsensus-layer primitive minimal. Higher-level protocols can be implementedin Daml libraries or external service wrappers that speak the nonce-signprotocol 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 type2. Off-chain automation to poll and publish3. A governance CIP to authorize each new feed
This works for a small number of high-value feeds but does not scale to thelong tail of external data integrations that application developers need. Aprotocol-level primitive shifts the trust boundary from "trust the oracleoperator's automation" to "trust the oracle operator's signing key" -- astrictly 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 morefrequent data needs should use the existing pattern of publishing data todedicated contracts. The limits are protocol parameters and can be adjustedby 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 notsupport request-response patterns where the query depends on contract state.
Trusted execution enclaves (TEE): Run the fetch inside a TEE on thesubmitting participant and attest the result. This would avoid needing theexternal service to implement the signing protocol but introduces hardwaredependencies and a much larger trust surface. It could be explored as acomplementary mechanism in a future CIP.
Threshold-signed fetches: Require multiple participants to independentlyfetch and co-sign the response before it is pinned. This provides strongerguarantees against a compromised submitter colluding with the external servicebut adds latency and complexity. It is a natural extension of this CIP andcould be layered on top.
Backwards Compatibility
This is a hard fork change. All participants (Super Validators andValidators) must upgrade to a Canton version that understands pinned datanodes 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 parserswill 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 theGlobal Synchronizer.2. Phase 2: Once 2/3 of Super Validators have upgraded and vetted thenew Daml packages, enable `fetchExternal` via an on-chain protocolparameter change.
Reference Implementation
To be provided. The implementation will span:
- Canton protocol: New `PinnedDataNode` in the transaction treerepresentation, validation logic in the mediator and confirming participants.- Daml engine: New `fetchExternal` built-in with TCP execution duringinterpretation and pinned-data replay during validation.- Daml standard library: `DA.External` module exposing the types andprimitive.- External service SDK: Reference implementation of the nonce-sign TCPprotocol 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. - Hi, ElliotThis 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.
- toggle quoted message Show quoted textThanks Wayne,Do you happen to have a link to their proposal? I cannot find it immediately. Will look at submitting for a grant!Best,ElliottOn Wed, Apr 15, 2026 at 12:37 PM Wayne Collier via lists.sync.global <wayne.collier=digitalasset.com@...> wrote:Hi, ElliotThis 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.
- 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
- 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) -> PinnedResultIf the request can be resolved to a locally-running service (e.g.
0.0.0.0:8545 for an EVM node), the submitter's participantexecutes 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 - 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:- The submitter executes the function during interpretation and includes the result (and optionally its hash and metadata) as part of the transaction view.
- Each validator re-executes the same external_call() locally.
- 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