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

Thanks for reading, I’d would enjoy your feedback.

Lastly, code for the two implementations and scenario’s are here.

1 Like

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.

1 Like

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 MapOnLedger approach you’re force to do a big-bang upgrade and force everyone to comply at the same time.
  • Functionality: with LedgerAsMap there’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 the LedgerAsMap approach - 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!

3 Likes

Depends on your use case.

  1. 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.

  2. 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
2 Likes

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.

  1. Why should a Party need to create a lookup just to request this information? Maintaining the observer field 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 write observer everyone, to mean all current and future parties.

  2. 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.

2 Likes