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
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 ()
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
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
Remove templates of LedgerIsMap are cumbersome.
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.