When creating a DAML application, one useful functionality is to allow Party
's to create aliases for themselves. Instead of referring to other users via some painful encoded string we can say “Tom”. We need to maintain an association between Text
's and Party
's; and there are two routes that we can take: a map on ledger (in a contract) or using the ledger as the map.
1. Map On Ledger
Users would create
template Request
with
user : Party
alias : Text
operator : Party
where
signatory user
ensure alias /= ""
key (operator, user) : (Party, Party)
maintainer key._2
controller operator can
Acknowledge : ()
do return ()
Taken : ()
do return ()
and an operator
party would maintain a contract of the known aliases and enforce rules such as allowing only one Party
to have an alias:
template Aliases
with
partyMap : PartyMap -- type PartyMap = Map Text Party
aliasMap : AliasMap -- type AliasMap = Map Party Text
operator : Party
where
signatory operator
observer fmap fst $ toList aliasMap -- Only those who have an alias can know other aliases.
key operator : Party
maintainer key
controller operator can
ProcessRequest : ContractId Aliases
with
requestId : ContractId Request
do
request <- fetch requestId
assert $ not $ isEmpty request.alias
case M.lookup request.alias partyMap of
None -> do
let (wPartyMap, wAliasMap) = withoutUser request.user partyMap aliasMap
(partyMap', aliasMap') = insertAlias request.user request.alias wPartyMap wAliasMap
exercise requestId Acknowledge
create Aliases with partyMap = partyMap', aliasMap = aliasMap' , ..
Some _ -> do
exercise requestId Taken
create Aliases with ..
Once a user has an alias they can see the Aliases contract and perform necessary lookups on the map there to figure out who is who. This works, but it feels wrong; if we’re working with a traditional DB, we would store the aliases directly there.
2. Ledger is Map
In this scenario the user would issue almost identical Request
's. But the operator
would create an Alias
contracts for the user:
template Alias
with
user : Party
alias : Text
operator : Party
where
signatory operator
observer user -- Can't add others here after the fact.
key (operator, alias) : (Party, Text)
maintainer key._1
ensure alias /= ""
controller operator can
New : ContractId Alias
with newAlias : Text
do
create Alias with alias = newAlias, ..
controller user can
Remove : ()
do return ()
Here the association and uniqueness of aliases is maintained by the contract key, which the operator would maintain via:
template Operator
with
operator : Party
where
signatory operator
controller operator can
nonconsuming ProcessRequest : Optional (ContractId Alias)
with
requestId : ContractId Request
do
request <- fetch requestId
doesItExistId <- lookupByKey @Alias (operator, request.alias)
case doesItExistId of
None ->
do
aliasId <- exercise requestId Acknowledge
return $ Some aliasId
Some _occupiedId ->
do
exercise requestId Taken
return None
To perform lookups (and deletes) the user can issue new contracts:
template Lookup
with
user : Party -- who is making the request
alias : Text
operator : Party
where
signatory user
controller operator can
Respond : ContractId Response
with
responseId : ContractId Response
do return responseId
which the operator would
nonconsuming ProcessLookup : ContractId Response
with
requestId : ContractId Lookup
do
request <- fetch requestId
doesItExistId <- lookupByKey @Alias (operator, request.alias)
responseId <-
case doesItExistId of
Some occupiedId ->
do
occupiedAlias <- fetch occupiedId
create Response with response = Some occupiedAlias.user, operator, user = request.user
None ->
do
create Response with response = None, operator, user = request.user
exercise requestId Respond with ..
return responseId
Questions
I have a multitude of questions and thoughts about these two approaches, and haven’t figured out a concrete argument for one over the other. TL;DR: which approach is better?
Simplicity of operations
I think the MapOnLedger is easier to work with from a programming perspective. There are fewer contracts to work with, maps have a simple API, the operator controls what aliases are stored but does not have to worry about how others see the data.
Representation on the persistence layer.
As previously mentioned, LedgerIsMap seems like the more natural usage of our persistence layer. The analogy that I think about is against a tradition DB. Even though I could, I wouldn’t encode the map
via something like a JSON field in a DB row. At the same time the resulting Lookup
, Remove
templates of LedgerIsMap are cumbersome.
Performance
Probably related to the previous question, but what if we have several million aliases? There are two questions, one about storage and one about latency. Presumably, LedgerIsMap is better for storing a large number of Aliases. But MapOnLedger is potentially better for scaling? I could write my own map implementation in DAML to optimize for this use case (ex. use a trie, partition the data …etc). Or can I have multiple parallel bots processing requests in the LedgerIsMap case?
Writing good DAML
- Don’t use the ledger for orchestration, might be used as an argument against LedgerIsMap.
- Avoid race conditions Maybe a good argument against LedgerIsMap?
- Don’t use status variables in smart contracts An argument against MapOnLedger, in particular the arguments wrt complexity and errors. Notice how there’s a bug when a user has more than one alias!
- Legal perspective I see this as arguing against MapOnLedger in favor of LedgerIsMap.
- Data synchronization Again, more in favor of MapOnLedger.
Thanks for reading, I’d would enjoy your feedback.
Lastly, code for the two implementations and scenario’s are here.