diff --git a/README.md b/README.md index ab8b22b..5f1a2c1 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/source/util-time.rst b/docs/source/util-time.rst index 2d5b964..2725755 100644 --- a/docs/source/util-time.rst +++ b/docs/source/util-time.rst @@ -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 ---------------- diff --git a/pact/contracts/util-time.pact b/pact/contracts/util-time.pact index b954699..c7133d4 100644 --- a/pact/contracts/util-time.pact +++ b/pact/contracts/util-time.pact @@ -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) @@ -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)) @@ -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) ) @@ -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) ) @@ -112,6 +173,7 @@ (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)) @@ -119,23 +181,25 @@ (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) ) ) diff --git a/pact/tests_repl/util-time-test.repl b/pact/tests_repl/util-time-test.repl index fabee5d..2eb1e2e 100644 --- a/pact/tests_repl/util-time-test.repl +++ b/pact/tests_repl/util-time-test.repl @@ -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"))) @@ -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)