User aliases, a tale of two approaches. A developer grapples with DAML
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.
This is an excellent guide, moving to the #news:tutorials-and-guides section.
I don’t think that this is a guide. I’m presenting two different approaches and I have genuine concern about what is the best way forward.
I would opt for the second approach, LedgerAsMap for the following reasons:
- Performance: a single contract to maintain all alias mapping will grow indefinitely over time leading to very large transactions for simple operations. There’s also an issue with contention as you’ll have to sequentialize the operations on the singleton contract via a bot.
- Extensibility: the generalization of your problem is the maintenance of user profiles by an operator. As these profiles can change over time (new fields getting added etc.) it’s much cleaner to represent them in their own contracts, such that you’re able to follow the normal upgrade pattern. There could be users on old and new profiles concurrently, and the application could handle this just fine. In the
MapOnLedgerapproach you’re force to do a big-bang upgrade and force everyone to comply at the same time. - Functionality: with
LedgerAsMapthere’s a natural way to disclose other users’ aliases selectively. Also, if we think about the generalization of this into user profiles, there’s a natural way to disclose only part of the user information via specialized contracts. Both these points can’t be done as well in theLedgerAsMapapproach - if you end up creating contracts for disclosure you’re essentially in a mix between the two approaches.
These are just my 2c on the topic, interested to hear other opinions!
Depends on your use case.
-
For small scale PoCs, I would go for “Map on Ledger”, because you don’t need the lookup workflow and the alias map is visible to everyone. This is easiest to use.
-
If you have more advanced NFRs, I would definitely go for “Ledger is Map”. I.e.:
- Throughput: No contention caused by the global aliases contract.
- Privacy: You can disclose aliases selectively on a need to know basis.
- Scalability: No globally unique operator needs to process alias transactions.
- Composability: As aliases are not globally unique, you can combine two ledgers into a single one, even if they have allocated the same aliases.
- Upgrading: As pointed out by @georg
Thanks for the replies guys.
I’m not that concerned about alias visibility/privacy; only about uniqueness. When we name something, we don’t care as much about what we’re naming as making sure that everyone can refer to it.
From that perspective I find the lookup flow in the LedgerAsMap cumbersome.
-
Why should a Party need to create a lookup just to request this information? Maintaining the
observerfield on an alias contract is one possibility but it isn’t a primitive in the language, (an operator would have to act on the existence of new parties). I wish that there was an “escape hatch” and I could writeobserver everyone, to mean all current and future parties. -
Where should the curated lookup logic lie? If I’m building an autocomplete (ex. give me all the aliases starting with “Le”) feature to lookup aliases, should I query a specific contract or ask an off-ledger bot for that cached data?
If privacy is not a concern (eg. you specifically want global visibility of all aliases) and your lookups are far more frequent than write operations (add, delete, change aliases), then some of the concerns of MapOnLedger are not as grave - so it might be the better option then.
The LedgerAsMap option is better for when the disclosure of an alias is considered a business process, and each party has a set of aliases it interacts with that is much smaller than the global population. In this case a call to the ledger to query all known aliases is acceptable eg to populate a dropdown.
So I think it’s really a tradeoff and the ideal solution depends very much on the use case.
There are also mixed form, for example each party could also (besides the individual alias contracts it knows) maintain a directory contract for them to allow more efficient reads. You’d have to ensure that this is updated during the disclosure mechanism.
Regarding the observer everyone I think we all have wished at some point for the existence of that, and the related feature of “blinded observers” that don’t know about each other. Maybe @bernhard can comment if anything there is on the horizon as part of the read model overhaul.
Worth noting that you can now allocate parties in Daml with identifiers and hints. This is probably the approach anyone reading this will want to use in the future.