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
- I think that we need to split the state into a
HistoricalandCurrentstate, or at least I was not able to come up with a way of enforcing the separation via anensure. - 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
ContractIdis simpler to fetch and probably better to avoid loading largeBusinessdata records. The time when you need the actual historical data are rarer. - 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
Unwindwhen two contracts with history are linked that respects the business logic that drives that link. - 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:
- Avoid rewriting the
Businesspayload by converting fromCurrenttoHistorical. Use completely immutableBusinessStatecontracts, and haveCurrentStateandHistoricStateonly as pointers toBusinessStates. - 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
HistoricStateyou don’t keep a ContractId reference but a SHA256 reference, and theUnwindchoice takes theBusinessStateas an argument. That way you get the full security, but don’t need to keep all theBusinessobjects “hot”.
Thank you,
On the
HistoricStateyou don’t keep a ContractId reference but a SHA256 reference, and theUnwindchoice takes theBusinessStateas an argument.
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.
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.