Abstract. Canton commits a transaction as a whole, then shows each party a different slice of it. That arrangement is called sub-transaction privacy, and it is easy to flatten into one question — who can see this contract? — when the ledger is doing three different things. This article separates them, using a loan proposal, a KYC check, and a customer named Bob. Being a stakeholder means you are told. Witnessing a fetch means you saw a contract, which is divulgence, and seeing it does not let you use it. Being handed the contract blob off-ledger is explicit disclosure, and that is what lets you act. We then look at where those mechanisms actually stop, because the Ledger API is only one layer in the stack. For teams building Daml smart contracts on Canton Network, these are the points a privacy audit checks before go-live.
What does sub-transaction privacy promise?
Canton Network's own pitch starts from a tension, not a slogan. From What is Canton Network?:
Canton Network solves a fundamental tension in blockchain: transparency versus privacy. Traditional blockchains like Ethereum provide integrity and decentralization but expose all transaction data to every network participant. This makes them unsuitable for regulated financial markets, confidential business processes, and any application where data privacy is non-negotiable.
In place of that exposure, the pitch makes four claims: sub-transaction privacy ("different parties in the same transaction see only their relevant portions"), decentralized consensus, data that stays with the parties that own it, and scale without global state replication. This article is about the first of those, and about where it stops.
A Canton ledger has no public world state. A transaction is still atomic: it commits as a whole or not at all. What it does not do is broadcast the tree. In the detailed ledger model, each party receives the subtransaction of actions they are an informee of, and the consequences of those actions. A party who is not an informee of a parent still receives the child actions they are an informee of. Someone else, looking at the same commit, sees a different slice, or nothing. That is sub-transaction privacy: need-to-know, applied inside a single transaction rather than wrapped around it.
The easiest way to see what that buys you, and what it does not, is a loan. Bob is the customer. The bank will lend. A KYC provider has to verify Bob's identity, and has no business seeing the amount, the rate, or even that this bank is the lender. You split those facts across two contracts, because you have to. The split is not enough by itself. The ledger projects by action and informee, not by field, so one commit can still touch both.
Say the bank accepts. The bank is the controller of Accept, so the choice body runs with the authority of the bank and Bob, the signatories of LoanProposal. A fetch's actors are the authorizers who are also stakeholders of the fetched contract. KycRecord's stakeholders are Bob and the KYC provider, so the only actor is Bob, and the fetch's informees are Bob and the KYC provider. The bank is not one of them. It sees the fetch anyway, because it is an informee of Accept and keeps the exercise as-is, consequences included. The KYC provider is an informee of the fetch only, so its projection of Accept is just that fetch, which names Bob as its actor. Nothing in it points to the loan or the bank. Bob and the bank see the same tree, because both are informees of Accept.
| Node | Bank | Bob | KYC provider |
|---|---|---|---|
| Exercise Accepton LoanProposal | |||
| Fetch KycRecordnested inside Accept |
Hold onto this tree. Almost everything below follows from it.
The bank sees the fetch without being an informee of that node. It is not a stakeholder of KycRecord either, so that view is retroactive divulgence: the contract was shown to someone who never signed it, was never listed as an observer, and is not an informee of the node that used it. Accept submits only after the blob has already arrived off-ledger as a DisclosedContract. The fetch then writes a second copy into the view the bank's participant stores. Bob and the KYC provider are the signatories, so the payload is not news to them. A new party learns it only if Accept gains another informee. The KYC provider's projection includes neither the loan nor the bank.
The projection is a real guarantee, and it is easy to over-read. Data still crosses it in three ways that all look like "who can see this" and do not work the same way. A stakeholder is told about a contract. A party who witnessed a fetch saw a contract, and that is divulgence: a view, not a right to use it later. A party who was handed the blob off-ledger is allowed to use it, and that is explicit disclosure. Get them mixed up and the KYC provider learns the rate. A party you added to Accept receives Bob's identity. The check that was supposed to decide whether the bank may read that identity moves off the ledger, onto whatever channel carried the blob. The bank's own copy arrives on that channel. It has to, or the accept cannot be submitted.
The glossary calls divulgence "automatic disclosure," and it says a view you are not entitled to hides metadata as well as content. The detailed ledger model is the page that separates seeing, using, and shape.
Stakeholder visibility, divulgence and explicit disclosure compared
The business rule is simple. The KYC provider needs Bob's identity, and nothing else. The bank needs a loan it can accept. Bob is on both sides. Canton will project the commit. It will not check that the projection matches the rule.
So "who can see this?" is really three questions, with different rights, different lifetimes, and different ways the data leaks.
Three ways data crosses the projection
Told
Stakeholder visibility
Signatory or observer
- Told about later events
- Can use it later
Saw
Divulgence
Witnessed a fetch
- Told about later events
- Can use it later
Can use
Explicit disclosure
Blob handed over off‑ledger
- Told about later events
- Can use it later, while the contract is active
Two templates keep the facts apart. LoanProposal holds the amount, the rate, and the term. KycRecord holds Bob's identity. The proposal is signed by the bank and Bob. The KYC record is signed by Bob and the KYC provider. That split is the whole design. Everything after this is what happens when a workflow treats the split as optional.
The KYC provider as an observer
The tempting shortcut is to put the KYC provider on LoanProposal as an observer, so they can "see Bob" and verify him. They become a stakeholder. They are told.
They are told the wrong thing. An observer sees the contract, not a field inside it. The amount, the rate, and the fact that this bank is Bob's lender all arrive with the create. So does the rest of the stakeholder set. The detailed ledger model is blunt about the mutual-discovery half of that:
"all stakeholders learn about each other. This created a privacy problem when Alice actually does not want that Bob and Charlie know of each other."
The quote is about parties finding out about each other. The rate leaks for a duller reason, and it needs no quote: an observer sees every field. Put the KYC provider on LoanProposal and both happen at once.
Being told also has a memory, and it is narrower than people usually assume. The ledger model splits the notifications. Signatories see consuming and non-consuming choices. A contract observer is informed about creates and consuming exercises, and not about the rest:
Read the rows as "is this observer an informee of that action on the contract they observe?" They are not a claim about whether the node shows up somewhere else in the projection. An observer can be there at creation and still miss every non-consuming exercise and every fetch of that contract. They do hear about archival. A consuming exercise includes them, and because that exercise is kept as-is, a fetch nested under it lands in the projection even though they are not an informee of the fetch node.
preconsuming and postconsuming are the version of this that tends to surprise people in review. The detailed ledger model says Daml compiles both to a non-consuming choice whose first or last consequence is Archive. Observers hear about the archival. They do not hear about the exercise that caused it. The party who can see a create and never learn that the contract was archived is someone else: the non-stakeholder in the next section.
A choice observer is a different knob. They are an informee of that one exercise, and they see its consequences. Put one on Accept and they see the loan, plus whatever the choice fetches.
What a fetch actually shows
The next shortcut keeps the KYC provider off the loan and has Accept look the identity up. The choice below is consuming: it is written choice, not nonconsuming choice. The consuming part matters once observers show up. The bank can submit the fetch only after someone has already handed it the KycRecord blob. That handoff is explicit disclosure, which is the next section. What the fetch does is copy the blob into the commit. It is a second copy, not the first time the bank has seen Bob's identity.
choice Accept : ContractId Loan
controller bank
do
kyc <- fetch kycCid
-- witnesses of Accept who are not stakeholders of KycRecord see this contract
...
The guiding principle of the detailed ledger model is that contracts are shown to their stakeholders. Showing them to anyone else, through a projection, is divulgence. The privacy model treats it as automatic, and as a pattern to use carefully. It is a deliberate design choice, not a bug. A Fetch informs the signatories of the fetched contract and the actors of the fetch. In Daml, the actors are the authorizers of the fetch who are also stakeholders of the fetched contract. The informees of an ancestor exercise see that fetch as well, because they keep the ancestor as-is, consequences included. Those ancestor informees are the witnesses. If a witness is not a stakeholder of KycRecord, the record has been divulged to them.
Go back to the tree in the introduction. The bank is not an actor of the fetch; Bob is, because he is the only authorizer of Accept who is also a stakeholder of KycRecord. Bob's signature on LoanProposal is also what makes the fetch legal at all. A fetch needs at least one stakeholder of the fetched contract among its authorizers, so if Bob is removed from LoanProposal's signatories, Accept fails with FetchMissingAuthorization, blob or no blob. The bank sees the fetch only because it keeps Accept, the parent. It was never a signatory or an observer on KycRecord. That path is retroactive divulgence: an input shown to a witness who is not an informee of the node that used it. Immediate divulgence is the other form, a Create the witness is not an informee of.
In this tree the bank is the only such witness, and it already holds the blob it attached when it submitted. The party who actually learns Bob's identity from the fetch is one that Accept grows later. A contract observer of LoanProposal is an informee of this consuming exercise, so they see the fetch. They would not be an informee of a non-consuming Accept. A choice observer on Accept sees it either way. Both reach the fetch the same way the bank does, as witnesses of the parent. The difference is that they never had the blob, so for them the fetch is the first time they see Bob's identity.
Divulgence is a view, not a grant of access. The same section of the ledger model splits the two. Divulgence means you learned about a contract you are not a stakeholder of. Disclosure means you use that contract in a later transaction. Immediate divulgence does not entitle the next submit. Neither does this fetch. The bank's projection includes the input, and a later command that exercises or fetches KycRecord still fails unless the blob is attached again. The copy sits in participant history anyway. The bank is not a stakeholder, so it will not be told when the record is archived, and nothing in its projection retires that copy.
How does explicit disclosure work?
For the bank's Accept to get through interpretation, the bank already has to be able to read KycRecord. The choice typechecks either way. The bank is not a stakeholder, so the Ledger API will not serve the contract out of the bank's ACS. Someone — Bob, or the KYC provider — has to hand the bank the blob off-ledger, and the bank has to attach it to the command as a DisclosedContract. The created_event_blob on that CreatedEvent is filled in only when the event format asks for it.
That handoff is explicit disclosure. It is the right way for a non-stakeholder to use a contract. It is also a gate that no longer lives on the ledger. The docs describe the channel as "conventional means, such as HTTPS, SFTP, or e-mail." Whoever runs that channel decides whether the bank receives Bob's identity. The bank still has to be the controller of Accept; authorization stays on the ledger. While KycRecord is active, the same blob can be attached on every submit. Archival is how the ledger revokes use: confirming participants reject the command on the activeness check, and a recreated contract has a new id, so the old blob cannot be replayed. The copy sitting on the channel has no rotation and no expiry. It stays readable until the holder deletes it. The contract id is a hash of the arguments, the template id, the signatories, the keys, and the rest of the contract. Forge any of those and an honest participant rejects the command with DISCLOSED_CONTRACT_AUTHENTICATION_FAILED.
Bob or KYC provider
Signatories of KycRecord
KycRecord blobBank
Not a stakeholder. Attaches the blob to its command.
Confirming participants check
Contract ID matches the blob (hash)Contract still activeWhoever runs the channel decides who receives the data.Authorisation stays on the ledger: the bank still has to be the controller of Accept.
You can skip the handoff by adding the bank as an observer on KycRecord. Then the bank is a stakeholder of Bob's identity for as long as the contract lives, and every other stakeholder is told that the bank is there. That is the first mechanism again, which is what this workflow was trying to stay away from.
Put the three back on the same loan. The KYC provider as an observer is told about the deal. The DisclosedContract is what lets the bank read KycRecord and submit against it, on each command that attaches the blob, over a channel the ledger does not police. The fetch then shows that payload to every other witness of Accept. Mix the three up and a KYC check that was supposed to be need-to-know becomes a leak. The leak is a rights mistake, and it is also a question of which layer you thought was the wall.
Where the Data Boundary Actually Sits
How the data arrives and where it stops are different questions. The Ledger API is one layer in a stack. A design that only polices that layer has already lost the data.
- 01Contract stakeholdersWho is told, and stays told
- 02Transaction projectionWho witnessed this commit
- 03Ledger APIWho can act on what they saw
- Ledger API refuses the command
- 04Participant store and wireWhat is stored, and what can be inferred
The payload is still hereuntil pruning removes it
Why can't you edit an observer list?
Take the KYC provider back off LoanProposal and you have not edited a list. Contracts are immutable. The explicit disclosure docs are plain that changing who can see the data means recreating the contract with a different stakeholder set. Miss a reject path, or a withdraw path, and they stay on the deal. A single prospective counterparty belongs in Optional Party, not [Party]. Dropping the party still means archive and recreate either way. What Optional Party gives you is a cap: one extra party. A list can grow, and every negative path has to rebuild the contract without them.
There is a size cost on top of the privacy one. The detailed ledger model spells it out. A create with N observers is an action of size at least N, and that action is copied into N projections, so the total size is quadratic. Adding the observers one by one is cubic, because each step is an archive followed by a create.
Every fetch widens the witness set
Splitting the templates does not give you a private lookup. KycRecord keeps Bob's identity off LoanProposal. It does not keep it off Accept. The transaction lifecycle is why. The submitting participant sends views to participants that host an informee or a witness, and a parent view carries the encryption keys for its children. The bank is an informee of Accept, so the nested fetch arrives with the parent.
What the bank needed was a boolean, or an attestation that KYC had passed. Fetch that attestation and you divulge the attestation to the same witnesses, so the attestation has to be the boolean, not another copy of Bob. Witnesses of the fetch still learn who signed it. Those signatories are informees of the fetch, and the fetch node names its actors, so the KYC provider learns which of its stakeholders used it: here, Bob. Its projection does not say the bank was behind the command. Fetch KycRecord itself and the identity still ends up in the view the bank's participant persists.
What does the participant store keep?
Divulgence leaves a row. Ledger causality still uses the older stream names: the ACS delta, the flat stream, and the transaction tree stream. The flat stream is stakeholder creates and archives. The transaction tree omits Fetch nodes. A divulged contract shows up in neither the State Service nor the ACS delta, and the detailed ledger model offers no lookup by contract id for a retroactively divulged input. The current names are on the Ledger API update stream. TRANSACTION_SHAPE_ACS_DELTA returns stakeholder creates and archives. TRANSACTION_SHAPE_LEDGER_EFFECTS returns creates and exercises the party witnessed. Fetch nodes are in neither shape.
So the bank's next command on KycRecord fails unless a DisclosedContract comes with it. That is the API story. Storage is a different story. The same transaction lifecycle says each participant persists every sequencer message it receives, views included. The bank's participant has the fetch payload in that view.
Pruning deletes a prefix of that history. It is not a switch for divulged rows. It has to keep an active stakeholder contract, and a divulged view is not one of those, so the copy can go once the prefix moves past it. How far the prefix reaches is the operator's choice. The pruning page names storage growth, and regulations such as GDPR and HIPAA, as the reason that schedule exists. Another party hosted on the same validator does not see the contract through the Ledger API. The operator of that validator does.
The Participant Query Store is a sidecar over the update stream, not that view store. On a create the party witnessed without being a stakeholder, PQS can set divulged_only. true means only divulged. false is labeled "properly disclosed." The PQS page does not define that label. Read against the divulgence model, it means the party was a stakeholder of the create. The explicit-disclosure blob lives in a different column, metadata. The update stream omits fetches, so a fetch of an existing KycRecord should not create that row on the bank's PQS. That reading comes from the stream, not from a sentence in the PQS docs. An auditor, a backup, or a compromised operator is reading the persisted views, not the ACS. The API is not the boundary. On the ledger, knowing the contract id is not enough to read the contract or act on it. The CIP-56 article is about a different surface, where registry HTTP endpoints treat that same id as a bearer capability.
There is one layer under even that store. View payloads are encrypted, but two-phase commit still puts informees, encoding size, and required confirmers on the wire. The detailed ledger model says what an outsider can do with that:
"if Bank 2 can inspect the on-wire data of such communication, as is for example possible to some extent on the Global Synchronizer, then Bank 2 can infer some information about the shape of the original transaction and the parties involved, beyond what is visible in the projection."
On this loan, the amount and Bob's identity stay in the encrypted view. The fact that the bank, Bob, and the KYC provider shared a commit does not. Stakeholder lists decide who is told. Choice bodies decide who witnesses. The API decides who can act. The participant and the wire decide what is still there after the API has refused the command.
Daml privacy audit checklist
The loan was one workflow. The same rules apply to the next one.
- Who should be an observer? Name observers as the people who must be kept up to date, not everyone who might need a one-off look. The KYC provider needs a look at Bob's identity, not a subscription to the loan. A choice observer on Accept sees that exercise and whatever it fetches.
- Can the observer set grow? Prefer Optional Party over observer lists. Dropping the party still means archive and recreate, and every negative path — reject, withdraw — has to rebuild the contract without them. A list can accumulate. Each create grows with the list and is copied once per observer, so the projections add up to a quadratic total. A single Optional Party keeps that extra party at one.
- Does a widely witnessed choice fetch sensitive data? Do not fetch KycRecord from a widely witnessed loan choice. Identity and loan terms are different contracts for a reason. A boolean kycPassed, or an attestation that contains only that boolean, is what the bank needed. Witnesses of that fetch still learn who signed it, and the signatories learn which stakeholder fetched it.
- Does any party rely on divulgence to act? Treat divulgence as a leak, not as a grant. If a party has to act, disclose explicitly, on each submit, and know which off-ledger channel now holds that gate. The blob can be attached again while the contract is active. The copy on that channel outlives the archive.
- Is the confidentiality inventory written down? Write down what is confidential (payloads) and what is only partially hidden (informees, shape, confirmers). The API is not the inventory. Participant pruning deletes a prefix of history, and that prefix is what can drop a copy the projection never retires. Active stakeholder contracts stay.
Keep the three questions separate: who was told about a contract, who merely saw it, and who is allowed to use it. Canton will give you a correct projection of the commit. It will not check that the projection matches the rule you thought you wrote. Privacy on Canton is a design output. You build it in the stakeholder lists, the choice bodies, and the channels you are willing to trust with a blob.
None of these rules shows up as a failing test. They are found by reading stakeholder lists and choice bodies against the business rule, which is what a Daml smart contract audit covers before contracts reach a synchronizer. For related patterns, see Daml design patterns and security analysis and AI-assisted Daml audits.




