The work on PR #233 uncovered strange behaviours of functions from plutus-ledger-api and cardano-ledger with regards to the handling of time intervals.
Introductory example
For context, imagine we have a validator that checks that some transaction happens before a deadline like this:
import qualified Plutus.V1.Ledger.Interval as Interval
import qualified Plutus.V2.Ledger.Contexts as PV2
... Interval.to deadline `Interval.contains` PV2.txInfoValidRange txi
(Here is the actual line that led to this excursion.)
Here are two expectations I have about this code:
The check succeeds if and only if the last millisecond of the PV2.txInfoValidRange txi is at most deadline.
The PV2.txInfoValidRange txi will contain no millisecond outside the time range covered by the slot range of the Cardano transaction corresponding to txi.
Both of these expectations are violated. Let's start with the first one.
Strange behaviour of Interval.contains
Assume PV2.txInfoValidRange txi is of the form Interval (LowerBound a True) (UpperBound b False), that is, a half-open interval $[a,b)$. Since we're talking about a discrete interval milliseconds here, we have $[a,b-1] = [a,b)$. In particular, $[a,b-1]\supseteq[a,b)$.
However, Interval.contains disagrees: Following its implementation, and in particular the Ord instance of UpperBound, and noting that the interval $[a,b-1]$ is represented as Interval (LowerBound a True) (UpperBound (b-1) True), it computes
Interval (LowerBound a True) (UpperBound (b-1) True)
`Interval.contains` Interval (LowerBound a True) (UpperBound b False)
== (LowerBound a True <= LowerBound a True) && (UpperBound b False <= UpperBound (b-1) True)
== True && False
== False
The wrong step here is (UpperBound b False <= UpperBound (b-1) True) == False. It arises because the Ord instance of UpperBound first compares the bounds, and only if they are equal checks whether they are inclusive or not. The wrong assumption it makes is that "if b is bigger than b-1, an interval ending at b cannot be contained in an interval ending at b-1". This holds in the continuous case, but not in the discrete case.
Strange txInfoValidRange for left-unbounded transaction validity ranges
Cardano transactions have a validity range in terms of slots, while the TxInfo presents the validity range as an Interval of milliseconds. For Babbage, the function babbageTxInfo consumes a transaction, whose validity range is described in terms of slots, and produces a TxInfo, which describes the validity range of the transaction as an interval of milliseconds (the txInfoValidRange). Drilling down, the function that does the translation of slot ranges to milliseconds is transVITime.
For most slot intervals, transVITime does what I would expect, but for left-unbounded intervals like ValidityIntervel SNothing (SJust i) I don't understand its behaviour. Depending on some condition on the protocol version (I think?) it'll either return
a right-closed interval ending at the first millisecond of the first slot after i (the else case). I think this behaviour is incorrect, as it violates the expectation 2 from the introduction
a right-open interval ending at this point (the then case). I think this is correct, and it's also the behaviour for all other right-bounded intervals.
Can someone please
validate what I wrote above (I know that I'm seeing this behaviour when I actually run such examples and print the txInfoValidRange, but not whether the functions I'm pointing at acutally are the relevant ones and do what I claim they do), and
explain the reasoning behind this?
The upshot for the introductory example
In combination, the two behaviours I described mean that the two expectations outlined at the beginning are violated, if deadline is the last millisecond of a slot:
If the txInfoValidRange is of the first kind described (right closed, and including the first millisecond of the next slot), the check fails because expectation 2 does not hold. The first millisecond of the next slot after deadline is greater than deadline.
If the txInfoValidRange is of the second kind described (right-open, ending at the first millisecond of the next slot), the check fails because of the problem with Interval.contains. That is, even if the txInfoValidRange is what it should be, the comparison with Interval.to fails, because Interval.to is the closed interval ending at deadline, and txInfoValidRange is the open interval ending at deadline+1.
With regard to the second problem, @ak3n has kindly pointed me to this issue on cardano-ledger. It seems the solution for us is to make sure we run a protocol version of the ledger that is at least 9.
The work on PR #233 uncovered strange behaviours of functions from
plutus-ledger-api
andcardano-ledger
with regards to the handling of time intervals.Introductory example
For context, imagine we have a validator that checks that some transaction happens before a deadline like this:
(Here is the actual line that led to this excursion.)
Here are two expectations I have about this code:
PV2.txInfoValidRange txi
is at mostdeadline
.PV2.txInfoValidRange txi
will contain no millisecond outside the time range covered by the slot range of the Cardano transaction corresponding totxi
.Both of these expectations are violated. Let's start with the first one.
Strange behaviour of
Interval.contains
Assume
PV2.txInfoValidRange txi
is of the formInterval (LowerBound a True) (UpperBound b False)
, that is, a half-open interval $[a,b)$. Since we're talking about a discrete interval milliseconds here, we have $[a,b-1] = [a,b)$. In particular, $[a,b-1]\supseteq[a,b)$.However,
Interval.contains
disagrees: Following its implementation, and in particular theOrd
instance ofUpperBound
, and noting that the interval $[a,b-1]$ is represented asInterval (LowerBound a True) (UpperBound (b-1) True)
, it computesThe wrong step here is
(UpperBound b False <= UpperBound (b-1) True) == False
. It arises because theOrd
instance ofUpperBound
first compares the bounds, and only if they are equal checks whether they are inclusive or not. The wrong assumption it makes is that "ifb
is bigger thanb-1
, an interval ending atb
cannot be contained in an interval ending atb-1
". This holds in the continuous case, but not in the discrete case.Strange
txInfoValidRange
for left-unbounded transaction validity rangesCardano transactions have a validity range in terms of slots, while the
TxInfo
presents the validity range as anInterval
of milliseconds. For Babbage, the functionbabbageTxInfo
consumes a transaction, whose validity range is described in terms of slots, and produces aTxInfo
, which describes the validity range of the transaction as an interval of milliseconds (thetxInfoValidRange
). Drilling down, the function that does the translation of slot ranges to milliseconds istransVITime
.For most slot intervals,
transVITime
does what I would expect, but for left-unbounded intervals likeValidityIntervel SNothing (SJust i)
I don't understand its behaviour. Depending on some condition on the protocol version (I think?) it'll either returni
(theelse
case). I think this behaviour is incorrect, as it violates the expectation 2 from the introductionthen
case). I think this is correct, and it's also the behaviour for all other right-bounded intervals.Can someone please
txInfoValidRange
, but not whether the functions I'm pointing at acutally are the relevant ones and do what I claim they do), andThe upshot for the introductory example
In combination, the two behaviours I described mean that the two expectations outlined at the beginning are violated, if
deadline
is the last millisecond of a slot:txInfoValidRange
is of the first kind described (right closed, and including the first millisecond of the next slot), the check fails because expectation 2 does not hold. The first millisecond of the next slot afterdeadline
is greater thandeadline
.txInfoValidRange
is of the second kind described (right-open, ending at the first millisecond of the next slot), the check fails because of the problem withInterval.contains
. That is, even if thetxInfoValidRange
is what it should be, the comparison withInterval.to
fails, becauseInterval.to
is the closed interval ending atdeadline
, andtxInfoValidRange
is the open interval ending atdeadline+1
.