Let's take a look at both fetchByKey
and lookupByKey
in the context of a generic Sample
template:
template Sample
with
maints : [Party]
extraSigs : [Party]
obs : [Party]
where
signatory maints ++ extraSigs
observer obs
key this : Sample
maintainer key.maints
The key is the whole contract, maintained by a specified subset of the signatories. A simple proposal workflow can be used to instantiate such a contract:
template SampleProposal
with
sample : Sample
sigs : [Party]
where
signatory sigs
observer (signatory sample)
choice Sign
: ContractId SampleProposal
with
sig : Party
controller sig
do
assert (sig `elem` signatory sample)
assert (sig `notElem` sigs)
create this with sigs = sig :: sigs
choice Accept
: ContractId Sample
with
sig : Party
controller sig
do
assert (sig `elem` signatory sample)
create sample
makeSample = scenario do
maints <- mapA getParty ["main1", "main2", "maint3"]
extraSigs <- mapA getParty ["sig1", "sig2", "sig3"]
obs <- mapA getParty ["obs1", "obs2", "obs3"]
let sample = Sample with ..
prop <- submit (head maints) do
create SampleProposal with
sample
sigs = [head maints]
signedProp <- foldlA
(\p sig -> submit sig do exercise p Sign with sig)
prop
(tail maints ++ tail extraSigs)
submit (head extraSigs) do
exercise signedProp Accept with sig = head extraSigs
Now, who can fetchByKey
and who can lookupByKey
this sample?
The first condition for either is that the submitting party has to know of the contract. Ie they must be an stakeholder (ie signatory or observer), or the contract must have been divulged to them.
Secondly, the operation has to be authorised correctly. fetchByKey
is authorised like fetch
, meaning you have to have the authority of at least one stakeholder. Thus, an observer of Sample
can divulge the contract and delegate a fetchByKey
.
template FetchByKeyDelegation
with
sampleObserver : Party
agent : Party
where
signatory sampleObserver, agent
nonconsuming choice FetchSampleByKey
: (ContractId Sample, Sample)
with
ctl : Party
sample : Sample
controller ctl
do
fetchByKey @Sample sample
template FetchByKeyDelegationInvite
with
fbkd : FetchByKeyDelegation
where
signatory fbkd.sampleObserver
controller fbkd.agent can
AcceptFBKDI
: ContractId FetchByKeyDelegation
do
create fbkd
delegateFetch = scenario do
sample <- makeSample
let obs = head sample.obs
agent <- getParty "Agent"
prop <- submit obs do
create FetchByKeyDelegationInvite with
fbkd = FetchByKeyDelegation with
sampleObserver = obs
agent
fbkd <- submit agent do
exercise prop AcceptFBKDI
-- By calling FetchSampleByKey, `obs` divulges the contract to `agent`
submit obs do
exercise fbkd FetchSampleByKey with
ctl = obs
sample
-- Now `agent` can use the authority of `obs` to `fetchByKey`
submit agent do
exercise fbkd FetchSampleByKey with
ctl = agent
sample
lookupByKey
has stricter authorization rules. Rather than one stakeholder, you need the authority of all maintainers. Other than that, delegation works the same:
template LookupByKeyDelegation
with
maints : [Party]
agent : Party
where
signatory maints, agent
nonconsuming choice LookupSampleByKey
: Optional (ContractId Sample)
with
ctl : Party
sample : Sample
controller ctl
do
lookupByKey @Sample sample
template LookupByKeyDelegationInvite
with
lbkd : LookupByKeyDelegation
sigs : [Party]
where
signatory sigs
observer (signatory lbkd)
choice SignLBKDI
: ContractId LookupByKeyDelegationInvite
with
sig : Party
controller sig
do
assert (sig `elem` signatory lbkd)
assert (sig `notElem` sigs)
create this with sigs = sig :: sigs
controller lbkd.agent can
AcceptLBKDI
: ContractId LookupByKeyDelegation
do
create lbkd
delegateLookup = scenario do
sample <- makeSample
let maints = sample.maints
agent <- getParty "agent"
lbkdi <- submit (head maints) do
create LookupByKeyDelegationInvite with
lbkd = LookupByKeyDelegation with
maints
agent
sigs = [head maints]
signedlbkdi <- foldlA
(\p sig -> submit sig do exercise p SignLBKDI with sig)
lbkdi
(tail maints)
lbkd <- submit agent do
exercise signedlbkdi AcceptLBKDI
-- By calling LookupSampleByKey, a maintainer divulges the contract to `agent`
submit (head maints) do
exercise lbkd LookupSampleByKey with
ctl = head maints
sample
-- Now `agent` can use the authority of `obs` to `lookupByKey`
submit agent do
exercise lbkd LookupSampleByKey with
ctl = agent
sample
In your specific model, it seems that partyB
is an observer, but not a maintainer. Thus, they know the contract (the first conditions), and their actions are authorised by a stakeholder (the second condition for fetchByKey
). However, the lookupByKey
is not authorised by the maintainers so it fails.
The reason the for the difference in authorisation is the behaviour in case of negative lookups. fetchByKey
fails on the submitter node so negative lookups never hit the network. lookupByKey
allows negative lookups so they do hit the network.
DAML is designed around the principle that no party should ever have to do work unless they signed something. Validating key lookups is work so you should never have to do it unless you signed a contract. With fetchByKey
this is true. Unless you signed a contract on which you are maintainer, no honest node will ever submit a fetchByKey
on which you are a maintainer.
However, with lookupByKey
this is not true. First of all, if you have a negative lookup, the only information you have is who the maintainers are, as only those are part of the key. Thus to run an authorisation check on the submitter node, the authorisation rule has to be about maintainers, not stakeholders.
Now suppose the authority of one maintainer, rather than all of them, was enough. Then the following would be a perfectly legal thing to do:
badLookup = scenario do
frankie <- getParty "Frankie"
spammer <- getParty "spammer"
let
sample = Sample with
maints = [frankie, spammer]
extraSigs = []
obs = []
forA [1..1000] (\_ -> do
submit spammer do
lookupByKey @Sample sample
)
Ie a malicious party could legitimately spam you with expensive operations. This goes against DAMLs core principles so the authorisation rule has to be that all maintainers are needed.
The key take away is that one should consider very carefully whether lookupByKey
is actually needed. It's often advisable to design workflows so that all key lookups are positive.