Skip to content
CCPEDIAby Unity Nodes
Discussions/App Development/How do you prove your app didn't do something?Forum ↗

How do you prove your app didn't do something?

App Development6 posts156 views4 likesLast activity 3d ago
OR
orumedominicOP
Sep 2026 2

Not what it did.
Logs cover that. What it tried and was stopped from doing.

I hit this building a spend-limited agent wallet. The refusals turned out to
matter more than the payments, and I could not find a good way to hand
someone proof that a refusal actually happened.

What I ended up with: every attempt, allowed or refused, becomes one
hash-chained receipt saying what was tried and which rule decided it.

The limits are assertMsg fences in the Daml, so the refusal is the ledger’s, not
my app saying so about itself. You can check a chain here with nothing
installed: Check a payment record | KYA Rails

But I might be solving something nobody else has.

Has anyone ever asked you to prove your app didn’t do something?
What did you show them?

FE
Federico_Rodriguez
Sep 2026 1

Hey @orumedominic , Is my understanding correct that the hash-chained receipts are produced by the application based on the response it gets from the participant/validator?

If so, one limitation is that you are still trusting the application to create the receipts and include every attempt. The hash chain makes existing receipts tamper-evident, but it doesn’t prove that a receipt wasn’t omitted or that its contents accurately represent the participant’s response.

An alternative worth considering would be to make the refusal itself a valid Canton transaction. For example, wrap the operation in a choice that checks the policy and either performs the action or creates a RejectedAttempt audit record. The transaction commits in both cases, but the underlying action only happens when allowed.

That would make the refusal part of the Canton ledger rather than evidence produced by the application. The trade-off is that rejection becomes explicitly modeled in Daml rather than using a failed transaction.

OR
orumedominic
Sep 2026

Yes, that is right. And the omission gap was the real problem.

I built what you described.

A choice called TryCharge checks the policy. If it passes, it pays. If it
fails, it creates a ChargeRefused contract and pays nothing. Both paths
commit. That is your RejectedAttempt under another name.

github.com/dominicrume/cantor8_kya_rails

step-1-mandate/daml/KyaMandate.daml

-- KyaMandate: extends the Cantor8 hackathon starter Mandate.daml (credit: Cantor8 toolkit)
-- Additions for KYA Rails: counterparty allow-list, charge memo for the audit trail.
-- The cap is enforced HERE. A cap checked in Python is a suggestion.
module KyaMandate where

import DA.Time (RelTime, addRelTime)

-- One contract per accepted charge, so the audit trail lives on the ledger and
-- not only in our receipt chain. Signed by the same two parties as the mandate;
-- the payee observes, so they can read what was paid to them without being able
-- to authorise anything.
template ChargeRecord
  with
    owner   : Party
    spender : Party
    payee   : Party
    amount  : Decimal
    memo    : Text
    chargedAt : Time
  where
This file has been truncated. show original

You called the trade-off right too. Rejection is modelled in Daml now.
That cost a second copy of the rules. They drifted once. I changed one
comparison. Every test stayed green. The two paths then disagreed about a
charge landing exactly on the cap. There is a test for that now.

Your caveat still stands. An attempt nobody submits still leaves
nothing. This only fixes refusals that reached the ledger.

That was better review than most code gets. Thank you.

OR
orumedominic
8d ago

Federico, an update, since your point was the one that mattered.

You said a chain of receipts written by my app cannot prove I left one out.
Right, and no hash fixes it, because I choose what to hash.

So I stopped hashing and started counting.

One contract per owner and actor. Every attempt consumes it and puts it back
with the count up by one. The auditor observes that contract, so they read the
count off the ledger instead of off what I hand them.

Fifty attempts, refusals at 3, 7 and 19. I show them two records. They know one
is missing and which position it was.

The work and the count commit in the same transaction, so skipping the count
means skipping the work. That is the part you described, reached from the other
side.

Two things I could not do. Daml 3 removed contract keys, so I cannot stop
someone opening a second journal. The auditor sees both, which is detection and
not prevention. And nothing forces an app to use the journal at all.

Still no credential, so all of it is Daml Script until it runs on DevNet.

Thank you for the review.

PR
PREDGE
5d ago

On the two gaps in your last post. Second journal: if the auditor is a signatory on the journal, not only an observer, nobody can create another one without the auditor’s authority, since a create needs every signatory. That moves it from detection to prevention without contract keys. Nothing forcing the app to use it: put the counter on the mandate itself. If TryCharge is a consuming choice on the contract that holds the spend authority and recreates it with count + 1, there is no way to pay that skips the count. The cost is contention, every attempt for one owner and actor serialises on that one contract.

OR
orumedominic
3d ago 1

Both built.

Auditor on the signatory line: the second journal is now impossible. Free at
runtime, since a choice that recreates the journal already carries its
signatories’ authority.

Contention: Sharded it. The test holds the original contract ID and uses it
twice. One journal refuses the second.
Two shards allow both.

All four properties survive.
The new failure mode is a hidden shard, so the set declares how many.

What would you break next?

← Back to Discussions