Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ Contains some time utilities
* ```(defun epoch:time ()``` : Returns Unix EPOCH
* ```(defun genesis:time ()``` : Returns Kadena Genesis time
* ```(defun now:time ()``` : Returns the current time
* ```(defun time-safe:time (in:string)``` : Safe version of time native
* ```(defun parse-time-safe:time (fmt:string in:string)``` : Safe version of the parse-time native
* ```(defun add-time-safe:time (in:time delta:decimal)``` : Safe version of the add-time native
* ```(defun diff-time-safe:decimal (x:time y:time)``` : Safe version of the diff-time native
* ```(defun from-now:time (delta:decimal)``` : Returns the delta time taking now as a reference
* ```(defun tomorrow:time ()```: Returns current time + 24 hours
* ```(defun yesterday:time ()```: Returns current time - 24 hours
Expand Down
60 changes: 60 additions & 0 deletions docs/source/util-time.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,66 @@ Compute a time from an Unix timestamp.
"2022-12-05T00:08:53Z"


Safe functions
---------------

Pact native time functions are not safe when they accept externally supplied arguments.
They can overflow and give weird results (eg A date in the past, while we expect a date in the future).

See:
https://github.com/kadena-io/pact-5/issues/84

https://github.com/kadena-io/pact/issues/1301

That's why these wrappers are necessary to handle corner cases and keep contracts sure.
I recommend to always and only use these functions instead of native when your module API allows users to supply time or
delta values.

time-safe
~~~~~~~~~
*in* ``string`` *→* ``time``

Equivalent of the ``(time)`` native but ensure that no overflow occurred.

.. code:: lisp

(time-safe "2025-03-03T17:56:48Z")
"2025-03-03T17:56:48Z"

parse-time-safe
~~~~~~~~~~~~~~~
*fmt* ``string`` *in* ``string`` *→* ``time``

Equivalent of the ``(parse-time)`` native but ensure that no overflow occurred.

.. code:: lisp

(parse-time-safe "%F" "2024-11-06")
"2024-11-06T00:00:00Z"

add-time-safe
~~~~~~~~~~~~~
*in* ``time`` *delta* ``decimal`` *→* ``time``

Equivalent of the ``(add-time)`` native but ensure that no overflow occurred.

.. code:: lisp

(add-time-safe (time "2022-12-04T14:54:24Z") (hours 2.0))
"2022-12-04T16:54:24Z"

diff-time-safe
~~~~~~~~~~~~~~
*t1* ``time`` *t2* ``time`` *→* ``decimal``

Equivalent of the ``(diff-time)`` native but ensure that no overflow occurred.

.. code:: lisp

(diff-time-safe (time "2022-12-04T16:54:24Z") (time "2022-12-04T14:54:24Z"))
7200.0


Compare function
----------------

Expand Down
88 changes: 76 additions & 12 deletions pact/contracts/util-time.pact
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
(enforce-keyset "free.util-lib"))

(use util-chain-data [block-time block-height])
(use util-math [between])
(use util-math [between pow10])

(defconst EPOCH (time "1970-01-01T00:00:00Z"))
(defconst EPOCH:time (time "1970-01-01T00:00:00Z"))

(defconst GENESIS (time "2019-10-30T00:01:00Z"))
(defconst HASKELL-EPOCH:time (time "1858-11-17T00:00:00Z"))

(defconst GENESIS:time (time "2019-10-30T00:01:00Z"))

(defconst BLOCK-TIME 30.0)

Expand All @@ -41,6 +43,66 @@
"Returns the current time"
(block-time))

;; Safe time computation management
;
; (add-time) uses Haskell time library and can overflow
; Haskell computes time from the TAI EPOCH ("1858-11-17T00:00:00Z") is useconds.
; in signed int64 (min = - 2^63, max = 2 ^63 -1)
;
; To be sure, we never overflowwe limits:
; - Every usable time to (TAI EPOCH +/- 2^62/1e6 -1)
; - Every usable offset to (+/- 2^62/1e6 -1)
;
; By enforcing such limits, we can guarantee that time functions never overflow.
;
; When a Smart contract developer uses (add-time), (diff-time), (time) or (parse-time) with
; user supplied inputs, he should preferably use safe counterparts to avoid non-expected
; behaviour that could yield to a security issue.
;
; For parsing functions: ie (time) and (parse-time), we compare the input string with
; the stringified parsed date. If there is a difference, it means that an overflow probably occured
(defconst SAFE-DELTA:decimal (- (/ (^ 2.0 62.0) (pow10 6)) 1.0))

(defconst MIN-SAFE-TIME:time (add-time HASKELL-EPOCH (- SAFE-DELTA)))

(defconst MAX-SAFE-TIME:time (add-time HASKELL-EPOCH SAFE-DELTA))

(defun --enforce-safe-time:bool (in:time)
(enforce (time-between MIN-SAFE-TIME MAX-SAFE-TIME in) "Time out of safe bounds"))

(defun --enforce-safe-delta:bool (in:decimal)
(enforce (between (- SAFE-DELTA) SAFE-DELTA in) "Delta out of safe bounds"))

(defun time-safe:time (in:string)
"Do a (time) without any risk of overflow"
(let ((t (time in)))
(enforce (= in (format-time "%Y-%m-%dT%H:%M:%SZ" t)) "Unsafe time conversion")
(--enforce-safe-time t)
t)
)

(defun parse-time-safe:time (fmt:string in:string)
"Do a (parse-time) without any risk of overflow"
(let ((t (parse-time fmt in)))
(enforce (= in (format-time fmt t)) "Unsafe time conversion")
(--enforce-safe-time t)
t)
)

(defun add-time-safe:time (in:time delta:decimal)
"Do a (add-time) without any risk of overflow"
(--enforce-safe-time in)
(--enforce-safe-delta delta)
(add-time in delta)
)

(defun diff-time-safe:decimal (x:time y:time)
"Do a (diff-time) without any risk of overflow"
(--enforce-safe-time x)
(--enforce-safe-time y)
(diff-time x y)
)

(defun tomorrow:time ()
"Returns current time + 24 hours"
(from-now (days 1))
Expand All @@ -53,6 +115,7 @@

(defun from-now:time (delta:decimal)
"Returns the delta time taking now as a reference"
(--enforce-safe-delta delta)
(add-time (now) delta)
)

Expand All @@ -63,15 +126,13 @@

(defun to-timestamp:decimal (in:time)
"Computes an Unix timestamp of the input date"
(--enforce-safe-time in)
(diff-time in (epoch))
)

(defconst TIMESTAMP-LIMIT:decimal 3155695200000.0)

(defun from-timestamp:time (timestamp:decimal)
"Computes a time from an Unix timestamp"
; Since add-time is not safe for big numbers we enforce a min/max of 100kyears
(enforce (between (- TIMESTAMP-LIMIT) TIMESTAMP-LIMIT timestamp) "Timestamp out of bounds")
(--enforce-safe-delta timestamp)
(add-time (epoch) timestamp)
)

Expand Down Expand Up @@ -112,30 +173,33 @@

(defun est-height-at-time:integer (target-time:time)
"Estimates the block height at a target-time"
(--enforce-safe-time target-time)
(let ((delta (diff-time target-time (now)))
(est-block (+ (block-height) (round (/ delta BLOCK-TIME)))))
(if (> est-block 0 ) est-block 0))
)

(defun est-time-at-height:time (target-block:integer)
"Estimates the time of the target-block height"
(let ((delta (- target-block (block-height))))
(add-time (now) (* BLOCK-TIME (dec delta))))
(let* ((delta-blocks (- target-block (block-height)))
(delta (* BLOCK-TIME (dec delta-blocks))))
(--enforce-safe-delta delta)
(add-time (now) delta))
)

;; Diff time functions
(defun diff-time-minutes:decimal (time1:time time2:time)
"Computes difference between TIME1 and TIME2 in minutes"
(/ (diff-time time1 time2) 60.0)
(/ (diff-time-safe time1 time2) 60.0)
)

(defun diff-time-hours:decimal (time1:time time2:time)
"Computes difference between TIME1 and TIME2 in hours"
(/ (diff-time time1 time2) 3600.0)
(/ (diff-time-safe time1 time2) 3600.0)
)

(defun diff-time-days:decimal (time1:time time2:time)
"Computes difference between TIME1 and TIME2 in days"
(/ (diff-time time1 time2) 86400.0)
(/ (diff-time-safe time1 time2) 86400.0)
)
)
35 changes: 33 additions & 2 deletions pact/tests_repl/util-time-test.repl
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@
;;; (from-timestamp)
(expect "from-timestamp must be UNIX EPOCH for ZERO" (epoch) (from-timestamp 0.0))
(expect "from-timestamp must be accurate for this example" (time "2022-12-05T00:08:53Z") (from-timestamp 1670198933.0))
(expect-failure "Out of bounds timestamp" "Timestamp out of bounds" (from-timestamp 6311390400000.0))
(expect-failure "Out of bounds timestamp" "Timestamp out of bounds" (from-timestamp -6311390400000.0))
(expect-failure "Out of bounds timestamp" "Delta out of safe bounds" (from-timestamp 6311390400000.0))
(expect-failure "Out of bounds timestamp" "Delta out of safe bounds" (from-timestamp -6311390400000.0))

;;; (earliest ...)
(expect "Test earliest" (time "2022-12-04T14:44:24Z") (earliest (time "2022-12-04T14:54:24Z") (time "2022-12-04T14:44:24Z")))
Expand Down Expand Up @@ -146,5 +146,36 @@
(expect "Negative 2 days delta" -2.0 (diff-time-days (time "2022-12-04T14:54:24Z") (time "2022-12-06T14:54:24Z")))


;;; Safe time
(time-safe "2025-03-03T17:56:48Z")
(expect-failure "Unsafe" "Unsafe" (time-safe "-390419-11-07T19:59:05Z"))
(expect-failure "Unsafe" "Unsafe" (time-safe "390419-11-07T19:59:05Z"))
(expect-failure "Unsafe" "out of safe bounds" (time-safe "200100-11-06T19:59:05Z"))

(expect-that "Safe" (= (parse-time "%F" "2024-11-06")) (parse-time-safe "%F" "2024-11-06"))
(expect-failure "Unsafe" "Unsafe" (parse-time-safe "%F" "350000-11-06"))
(expect-failure "Unsafe" "out of safe bounds" (parse-time-safe "%F" "200100-11-06"))

;;; (add-time-safe)
(expect "Should work" (time "2022-12-04T16:54:24Z") (add-time-safe (time "2022-12-04T14:54:24Z") (hours 2.0)))
(expect "Should work" (time "2022-12-04T12:54:24Z") (add-time-safe (time "2022-12-04T14:54:24Z") (hours -2.0)))

(expect-failure "Too much in the future" "Delta out of safe bounds" (add-time-safe (time "2022-12-04T14:54:24Z") (days 109500000.0)))
(expect-failure "Too much in the past" "Delta out of safe bounds" (add-time-safe (time "2022-12-04T14:54:24Z") (days -109500000.0)))

(expect-failure "Too much in the future" "Time out of safe bounds" (add-time-safe (time "300001-12-04T14:54:24Z") (days 2.0)))
(expect-failure "Too much in the past" "Time out of safe bounds" (add-time-safe (time "-300001-12-04T14:54:24Z") (days -2.0)))

;;; (diff-time-safe)
(expect "Should work" 7200.0 (diff-time-safe (time "2022-12-04T16:54:24Z") (time "2022-12-04T14:54:24Z")))
(expect-failure "Too much in the future" "Time out of safe bounds" (diff-time-safe (time "300001-12-04T14:54:24Z") (time "2022-12-04T14:54:24Z")))
(expect-failure "Too much in the future" "Time out of safe bounds" (diff-time-safe (time "2022-12-04T14:54:24Z") (time "300001-12-04T14:54:24Z") ))
(expect-failure "Too much in the future" "Time out of safe bounds" (diff-time-safe (time "300001-12-04T14:54:24Z") (time "300001-12-04T14:54:24Z")))
(expect-failure "Too much in the past" "Time out of safe bounds" (diff-time-safe (time "-300001-12-04T14:54:24Z") (time "2022-12-04T14:54:24Z")))
(expect-failure "Too much in the past" "Time out of safe bounds" (diff-time-safe (time "2022-12-04T14:54:24Z") (time "-300001-12-04T14:54:24Z") ))
(expect-failure "Too much in the past" "Time out of safe bounds" (diff-time-safe (time "-300001-12-04T14:54:24Z") (time "-300001-12-04T14:54:24Z")))



(print "Tests of util-time ended")
(commit-tx)