From 888ca46fa3a4ea4ec96d4bf046cb75489088e836 Mon Sep 17 00:00:00 2001 From: CryptoPascal31 Date: Sun, 12 Nov 2023 00:21:57 +0100 Subject: [PATCH 1/6] Introduction of safe time management --- pact/contracts/util-time.pact | 42 ++++++++++++++++++++++------- pact/tests_repl/util-time-test.repl | 4 +-- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/pact/contracts/util-time.pact b/pact/contracts/util-time.pact index 0edcbb2..2774b8e 100644 --- a/pact/contracts/util-time.pact +++ b/pact/contracts/util-time.pact @@ -20,11 +20,21 @@ (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 HASKELL-EPOCH:time (time "1858-11-17T00:00:00Z")) + + (defconst GENESIS:time (time "2019-10-30T00:01:00Z")) + + (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)) - (defconst GENESIS (time "2019-10-30T00:01:00Z")) (defconst BLOCK-TIME 30.0) @@ -41,6 +51,18 @@ "Returns the current time" (block-time)) + (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 add-time-safe:time (in:time delta:decimal) + (--enforce-safe-time in) + (--enforce-safe-delta delta) + (add-time in delta) + ) + (defun tomorrow:time () "Returns current time + 24 hours" (from-now (days 1)) @@ -53,6 +75,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 +86,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 +133,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,8 +141,10 @@ (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 diff --git a/pact/tests_repl/util-time-test.repl b/pact/tests_repl/util-time-test.repl index 0015b53..8b15fa8 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"))) From 487bc3051807fbe6ad41796613d21a113fb008b3 Mon Sep 17 00:00:00 2001 From: CryptoPascal31 Date: Fri, 17 Nov 2023 08:05:53 +0100 Subject: [PATCH 2/6] Safe time: improve comments --- pact/contracts/util-time.pact | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/pact/contracts/util-time.pact b/pact/contracts/util-time.pact index 2774b8e..dfc59a1 100644 --- a/pact/contracts/util-time.pact +++ b/pact/contracts/util-time.pact @@ -24,18 +24,10 @@ (defconst EPOCH:time (time "1970-01-01T00:00:00Z")) - (defconst HASKELL-EPOCH:time (time "1858-11-17T00:00:00Z")) (defconst GENESIS:time (time "2019-10-30T00:01:00Z")) - (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)) - - (defconst BLOCK-TIME 30.0) ; General functions @@ -51,6 +43,27 @@ "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 time functions never overflow. + ; + ; When a Pact programmer uses (add-time) with user provided inputs, it should + ; better use (add-time-safe) to avoid non-expected behaviour that could yield to + ; a security issue + (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")) From ac97feca2dcf1176d9bcc55510a2964dd9d663f6 Mon Sep 17 00:00:00 2001 From: CryptoPascal31 Date: Wed, 24 Jan 2024 18:32:57 +0100 Subject: [PATCH 3/6] Typo --- pact/contracts/util-time.pact | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pact/contracts/util-time.pact b/pact/contracts/util-time.pact index dfc59a1..bc74cf4 100644 --- a/pact/contracts/util-time.pact +++ b/pact/contracts/util-time.pact @@ -53,7 +53,7 @@ ; - 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 time functions never overflow. + ; By enforcing such limits, we can guarantee that time functions never overflow. ; ; When a Pact programmer uses (add-time) with user provided inputs, it should ; better use (add-time-safe) to avoid non-expected behaviour that could yield to From 8af9bc6be3eb435c835f1f687006e2e91cd7f32b Mon Sep 17 00:00:00 2001 From: CryptoPascal31 Date: Tue, 4 Mar 2025 14:30:02 +0100 Subject: [PATCH 4/6] Add (parse-time-safe), (diff-time-safe) and update comments --- pact/contracts/util-time.pact | 39 +++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/pact/contracts/util-time.pact b/pact/contracts/util-time.pact index bc74cf4..8024730 100644 --- a/pact/contracts/util-time.pact +++ b/pact/contracts/util-time.pact @@ -55,9 +55,12 @@ ; ; By enforcing such limits, we can guarantee that time functions never overflow. ; - ; When a Pact programmer uses (add-time) with user provided inputs, it should - ; better use (add-time-safe) to avoid non-expected behaviour that could yield to - ; a security issue + ; 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))) @@ -70,12 +73,36 @@ (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)) @@ -163,16 +190,16 @@ ;; 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) ) ) From 79f421919b86d371aa225490dc68046ccb7090a7 Mon Sep 17 00:00:00 2001 From: CryptoPascal31 Date: Tue, 4 Mar 2025 14:30:08 +0100 Subject: [PATCH 5/6] Add safe-date tests --- pact/tests_repl/util-time-test.repl | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pact/tests_repl/util-time-test.repl b/pact/tests_repl/util-time-test.repl index 8b15fa8..89480a2 100644 --- a/pact/tests_repl/util-time-test.repl +++ b/pact/tests_repl/util-time-test.repl @@ -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) From a735be8079039dfd181d7d63fe315ae47d8607ca Mon Sep 17 00:00:00 2001 From: CryptoPascal31 Date: Tue, 4 Mar 2025 14:45:26 +0100 Subject: [PATCH 6/6] Add docs for fae-time --- README.md | 4 +++ docs/source/util-time.rst | 60 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/README.md b/README.md index f5badae..127b4ee 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,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 ----------------