Managing state with a history

I think that in general, when we create Daml models, we encourage a separation between what is active and historical data. The ACS reflects exactly that and if one wants to see historical data, and how it changed, access it via the transaction API.

But what if we want to explicitly enable the ability to go back to a previous state? I could think of a way to do that via off-ledger actions with appropriate request/response mechanism. Or we could try to do it on ledger. What do y’all think about:

type Business = Text

template Historical
  with
    p : Party
    created : Time
    archived : Time       -- What would be a better name?
    previous : Optional (ContractId Historical)
    business : Business
  where
    signatory p
    ensure archived >= created

template Current
  with
    p : Party
    created : Time
    previous : Optional (ContractId Historical)
    business : Business
  where
    signatory  p

    controller p can
      New : ContractId Current
        with
          newBusiness : Business
        do
          now <- getTime
          h <- create Historical with
                archived = now
                ..

          create Current with
            created = now
            previous = Some h
            ..

      nonconsuming Unwind : Optional (ContractId Current)
        with
          to : Time
        do
          if created <= to then do
            return $ Some self
          else do
            archive self
            case previous of
              None -> return None
              Some hId -> do
                -- A bit sneaky but now the context has the correct state.
                Historical {..}  <- fetch hId
                archive hId
                createAndExercise (Current with ..) (Unwind with ..)

      DoWork : Business
        do
          when <- getTime
          return $ "We did " <> show business <> " at " <> show when

t : Script ()
t = do
  p <- allocateParty "p"
  created <- getTime

  c1 <- p `submit` do
    createCmd Current with
      p
      created
      previous = None
      business = "work 1"

  passTime $ days 2

  c2 <- p `submit` do
        exerciseCmd c1 New with
          newBusiness = "work 2"

  passTime $ days 2

  c3 <- p `submit` do
        exerciseCmd c2 New with
          newBusiness = "work 3"

  passTime $ days 2

  c4 <- p `submit` do
        exerciseCmd c3 New with
          newBusiness = "work 4"

  Some c5 <- p `submit` do
    exerciseCmd c4 Unwind with
      to = addRelTime created (days 5)

  w <- p `submit` do
    exerciseCmd c5 DoWork

  debug $ w
  1. I think that we need to split the state into a Historical and Current state, or at least I was not able to come up with a way of enforcing the separation via an ensure.
  2. Should I even separate the historical data to a different template or store it as a list on the current one? My thinking here is that a ContractId is simpler to fetch and probably better to avoid loading large Business data records. The time when you need the actual historical data are rarer.
  3. This approach would need to be replicated for each template that I want to have this feature, which isn’t great but probably manageable. The hard part is that, is that one would have to have a custom Unwind when two contracts with history are linked that respects the business logic that drives that link.
  4. Any suggestions or thoughts?

This is fairly reasonable if you need such a simple rollback experience. You can optimize it a little in two ways:

  1. Avoid rewriting the Business payload by converting from Current to Historical. Use completely immutable BusinessState contracts, and have CurrentState and HistoricState only as pointers to BusinessStates.
  2. If you are willing to do a bit more work client-side, you could reduce your active contract set by not keeping historic states on ledger at all. On the HistoricState you don’t keep a ContractId reference but a SHA256 reference, and the Unwind choice takes the BusinessState as an argument. That way you get the full security, but don’t need to keep all the Business objects “hot”.

Thank you,

sha256 of the previous BusinessState that we’re recreating?

Yes, remove the Business object (which I expect is large) for historic data. Just store a SHA256 of that or the containing contract so that you can validate whether a presented payload is passed in it’s the original one.

1 Like

This is a good idea, but then in order to know what historical state one should rewind to, one would have to query something off ledger? … Unless we explicitly track that in a different contract.

You can keep the timestamps on-ledger. But you do have to keep the Business payload somewhere, of course. If you use Ledger pruning, that means you have to keep them in some off-ledger database.

@bernhard Is this what you have in mind?

template SomeTemplate
  with
    p : Party
    created : Time
    business : Business
    previous : [Text]
  where
    signatory  p

    controller p can
      New : ContractId SomeTemplate
        with
          newBusiness : Business
        do
          let currentHash = sha256 $ show business
          now <- getTime
          create SomeTemplate with
            created = now
            business = newBusiness
            previous = currentHash :: previous
            ..

      Unwind : ContractId SomeTemplate
        with
          pastBusiness : Business
        do
          let pastHash = sha256 $ show pastBusiness
              (statesToDrop, historyAtState) = break (pastHash ==) previous
          case historyAtState of
            [] -> abort "Did not find past state"
            (_ :: previous') ->
              create SomeTemplate with
                business = pastBusiness
                previous = previous'
                ..

Yes, exactly something like that.
If your previous chain is large, you can keep that in a side-template.

template SomeTemplate
  with
    p : Party
    created : Time
    business : Business
    previous : ContractId Previous
  where
    signatory  p
...

template Previous
  with
    p : Party
    businessHash : Text
    previous : ContractId Previous

That way it’s pretty much an append-only schema, and contracts don’t grow in size over time.