Repository: pq-code-package/mldsa-native
Pull request #179: CMBC: Refactor, add contract and proof for invntt_tomont
Author: login: jakemas
Created: 2025-04-22T18:57:22Z | Merged: 2025-04-29T03:46:09Z

https://github.com/pq-code-package/mldsa-native/issues/103



Diff:
diff --git a/mldsa/ntt.c b/mldsa/ntt.c
index 03d75ea5c..ac2e44c34 100644
--- a/mldsa/ntt.c
+++ b/mldsa/ntt.c
@@ -141,6 +141,44 @@ void ntt(int32_t a[MLDSA_N])
   /* are bounded in magnitude by 9 * MLDSA_Q                 */
 }
 
+/* Reference: Embedded into `invntt_tomont()` in the reference implementation
+ * [@REF] */
+static void mld_invntt_layer(int32_t r[MLDSA_N], unsigned layer)
+__contract__(
+  requires(memory_no_alias(r, sizeof(int32_t) * MLDSA_N))
+  requires(1 <= layer && layer <= 8)
+  requires(array_abs_bound(r, 0, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q))
+  assigns(memory_slice(r, sizeof(int32_t) * MLDSA_N))
+  ensures(array_abs_bound(r, 0, MLDSA_N, (MLDSA_N >> (layer - 1)) * MLDSA_Q)))
+{
+  unsigned start, k, len;
+  len = (MLDSA_N >> layer);
+  k = (1u << layer) - 1;
+  for (start = 0; start < MLDSA_N; start += 2 * len)
+  __loop__(
+    invariant(start <= MLDSA_N && k <= 255)
+    invariant(2 * len * k + start == 2 * MLDSA_N - 2 * len)
+    invariant(array_abs_bound(r, 0, start, (MLDSA_N >> (layer - 1)) * MLDSA_Q))
+    invariant(array_abs_bound(r, start, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q)))
+  {
+    unsigned j;
+    int32_t zeta = -zetas[k--];
+
+    for (j = start; j < start + len; j++)
+    __loop__(
+      invariant(start <= j && j <= start + len)
+      invariant(array_abs_bound(r, 0, start, (MLDSA_N >> (layer - 1)) * MLDSA_Q))
+      invariant(array_abs_bound(r, start, j, (MLDSA_N >> (layer - 1)) * MLDSA_Q))
+      invariant(array_abs_bound(r, j, start + len, (MLDSA_N >> layer) * MLDSA_Q))
+      invariant(array_abs_bound(r, start + len, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q)))
+    {
+      int32_t t = r[j];
+      r[j] = t + r[j + len];
+      r[j + len] = t - r[j + len];
+      r[j + len] = mld_fqmul(r[j + len], zeta);
+    }
+  }
+}
 
 /*************************************************
  * Name:        invntt_tomont
@@ -149,33 +187,32 @@ void ntt(int32_t a[MLDSA_N])
  *              In-place. No modular reductions after additions or
  *              subtractions; input coefficients need to be smaller than
  *              MLDSA_Q in absolute value. Output coefficient are smaller than
- *MLDSA_Q in absolute value.
+ *              MLDSA_Q in absolute value.
  *
- * Arguments:   - uint32_t p[MLDSA_N]: input/output coefficient array
+ * Arguments:   - uint32_t a[MLDSA_N]: input/output coefficient array
  **************************************************/
 void invntt_tomont(int32_t a[MLDSA_N])
+
 {
-  unsigned int start, len, j, k;
-  int32_t t, zeta;
+  unsigned int layer, j;
   const int32_t f = 41978; /* mont^2/256 */
 
-  k = 256;
-  for (len = 1; len < MLDSA_N; len <<= 1)
+  for (layer = 8; layer >= 1; layer--)
+  __loop__(
+    invariant(layer <= 8)
+    /* Absolute bounds increase from 1Q before layer 8 */
+    /* up to 256Q after layer 1                        */
+    invariant(array_abs_bound(a, 0, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q)))
   {
-    for (start = 0; start < MLDSA_N; start = j + len)
-    {
-      zeta = -zetas[--k];
-      for (j = start; j < start + len; ++j)
-      {
-        t = a[j];
-        a[j] = t + a[j + len];
-        a[j + len] = t - a[j + len];
-        a[j + len] = mld_fqmul(a[j + len], zeta);
-      }
-    }
+    mld_invntt_layer(a, layer);
   }
 
+  /* Coefficient bounds are now at 256Q. We now invert and reduce  */
+  /* each coefficient to bring them back to be bounded by 1Q       */
   for (j = 0; j < MLDSA_N; ++j)
+  __loop__(
+    invariant(j <= MLDSA_N)
+    invariant(array_abs_bound(a, 0, j, MLDSA_Q)))
   {
     a[j] = mld_fqmul(a[j], f);
   }
diff --git a/mldsa/ntt.h b/mldsa/ntt.h
index df4ef4281..15459f6d5 100644
--- a/mldsa/ntt.h
+++ b/mldsa/ntt.h
@@ -51,8 +51,14 @@ __contract__(
  *              MLDSA_Q in absolute value. Output coefficient are smaller than
  *              MLDSA_Q in absolute value.
  *
- * Arguments:   - uint32_t p[MLDSA_N]: input/output coefficient array
+ * Arguments:   - uint32_t a[MLDSA_N]: input/output coefficient array
  **************************************************/
-void invntt_tomont(int32_t a[MLDSA_N]);
+void invntt_tomont(int32_t a[MLDSA_N])
+__contract__(
+  requires(memory_no_alias(a, MLDSA_N * sizeof(int32_t)))
+  requires(array_abs_bound(a, 0, MLDSA_N, MLDSA_Q))
+  assigns(memory_slice(a, MLDSA_N * sizeof(int32_t)))
+  ensures(array_abs_bound(a, 0, MLDSA_N, MLDSA_Q))
+);
 
 #endif /* !MLD_NTT_H */
diff --git a/proofs/cbmc/invntt_layer/Makefile b/proofs/cbmc/invntt_layer/Makefile
new file mode 100644
index 000000000..08f0c0647
--- /dev/null
+++ b/proofs/cbmc/invntt_layer/Makefile
@@ -0,0 +1,54 @@
+# SPDX-License-Identifier: Apache-2.0
+
+include ../Makefile_params.common
+
+HARNESS_ENTRY = harness
+HARNESS_FILE = invntt_layer_harness
+
+# This should be a unique identifier for this proof, and will appear on the
+# Litani dashboard. It can be human-readable and contain spaces if you wish.
+PROOF_UID = mld_invntt_layer
+
+DEFINES +=
+INCLUDES +=
+
+REMOVE_FUNCTION_BODY +=
+UNWINDSET +=
+
+PROOF_SOURCES += $(PROOFDIR)/$(HARNESS_FILE).c
+PROJECT_SOURCES += $(SRCDIR)/mldsa/ntt.c
+
+CHECK_FUNCTION_CONTRACTS=mld_invntt_layer
+USE_FUNCTION_CONTRACTS=mld_fqmul
+APPLY_LOOP_CONTRACTS=on
+USE_DYNAMIC_FRAMES=1
+
+# Disable any setting of EXTERNAL_SAT_SOLVER, and choose SMT backend instead
+EXTERNAL_SAT_SOLVER=
+CBMCFLAGS=--bitwuzla
+
+FUNCTION_NAME = mld_invntt_layer
+
+# If this proof is found to consume huge amounts of RAM, you can set the
+# EXPENSIVE variable. With new enough versions of the proof tools, this will
+# restrict the number of EXPENSIVE CBMC jobs running at once. See the
+# documentation in Makefile.common under the "Job Pools" heading for details.
+# EXPENSIVE = true
+
+# This function is large enough to need...
+CBMC_OBJECT_BITS = 9
+
+# If you require access to a file-local ("static") function or object to conduct
+# your proof, set the following (and do not include the original source file
+# ("mldsa/poly.c") in PROJECT_SOURCES).
+# REWRITTEN_SOURCES = $(PROOFDIR)/<__SOURCE_FILE_BASENAME__>.i
+# include ../Makefile.common
+# $(PROOFDIR)/<__SOURCE_FILE_BASENAME__>.i_SOURCE = $(SRCDIR)/mldsa/poly.c
+# $(PROOFDIR)/<__SOURCE_FILE_BASENAME__>.i_FUNCTIONS = foo bar
+# $(PROOFDIR)/<__SOURCE_FILE_BASENAME__>.i_OBJECTS = baz
+# Care is required with variables on the left-hand side: REWRITTEN_SOURCES must
+# be set before including Makefile.common, but any use of variables on the
+# left-hand side requires those variables to be defined. Hence, _SOURCE,
+# _FUNCTIONS, _OBJECTS is set after including Makefile.common.
+
+include ../Makefile.common
diff --git a/proofs/cbmc/invntt_layer/invntt_layer_harness.c b/proofs/cbmc/invntt_layer/invntt_layer_harness.c
new file mode 100644
index 000000000..99fce09fe
--- /dev/null
+++ b/proofs/cbmc/invntt_layer/invntt_layer_harness.c
@@ -0,0 +1,14 @@
+// Copyright (c) 2025 The mldsa-native project authors
+// SPDX-License-Identifier: Apache-2.0
+
+#include <stdint.h>
+#include "params.h"
+
+void mld_invntt_layer(int32_t r[MLDSA_N], unsigned layer);
+
+void harness(void)
+{
+  int32_t *r;
+  unsigned layer;
+  mld_invntt_layer(r, layer);
+}
diff --git a/proofs/cbmc/invntt_tomont/Makefile b/proofs/cbmc/invntt_tomont/Makefile
new file mode 100644
index 000000000..4291e18da
--- /dev/null
+++ b/proofs/cbmc/invntt_tomont/Makefile
@@ -0,0 +1,54 @@
+# SPDX-License-Identifier: Apache-2.0
+
+include ../Makefile_params.common
+
+HARNESS_ENTRY = harness
+HARNESS_FILE = invntt_tomont_harness
+
+# This should be a unique identifier for this proof, and will appear on the
+# Litani dashboard. It can be human-readable and contain spaces if you wish.
+PROOF_UID = invntt_tomont
+
+DEFINES +=
+INCLUDES +=
+
+REMOVE_FUNCTION_BODY +=
+UNWINDSET +=
+
+PROOF_SOURCES += $(PROOFDIR)/$(HARNESS_FILE).c
+PROJECT_SOURCES += $(SRCDIR)/mldsa/ntt.c
+
+CHECK_FUNCTION_CONTRACTS=$(MLD_NAMESPACE)invntt_tomont
+USE_FUNCTION_CONTRACTS=mld_invntt_layer mld_fqmul
+APPLY_LOOP_CONTRACTS=on
+USE_DYNAMIC_FRAMES=1
+
+# Disable any setting of EXTERNAL_SAT_SOLVER, and choose SMT backend instead
+EXTERNAL_SAT_SOLVER=
+CBMCFLAGS=--bitwuzla
+
+FUNCTION_NAME = invntt_tomont
+
+# If this proof is found to consume huge amounts of RAM, you can set the
+# EXPENSIVE variable. With new enough versions of the proof tools, this will
+# restrict the number of EXPENSIVE CBMC jobs running at once. See the
+# documentation in Makefile.common under the "Job Pools" heading for details.
+# EXPENSIVE = true
+
+# This function is large enough to need...
+CBMC_OBJECT_BITS = 9
+
+# If you require access to a file-local ("static") function or object to conduct
+# your proof, set the following (and do not include the original source file
+# ("mldsa/poly.c") in PROJECT_SOURCES).
+# REWRITTEN_SOURCES = $(PROOFDIR)/<__SOURCE_FILE_BASENAME__>.i
+# include ../Makefile.common
+# $(PROOFDIR)/<__SOURCE_FILE_BASENAME__>.i_SOURCE = $(SRCDIR)/mldsa/poly.c
+# $(PROOFDIR)/<__SOURCE_FILE_BASENAME__>.i_FUNCTIONS = foo bar
+# $(PROOFDIR)/<__SOURCE_FILE_BASENAME__>.i_OBJECTS = baz
+# Care is required with variables on the left-hand side: REWRITTEN_SOURCES must
+# be set before including Makefile.common, but any use of variables on the
+# left-hand side requires those variables to be defined. Hence, _SOURCE,
+# _FUNCTIONS, _OBJECTS is set after including Makefile.common.
+
+include ../Makefile.common
diff --git a/proofs/cbmc/invntt_tomont/invntt_tomont_harness.c b/proofs/cbmc/invntt_tomont/invntt_tomont_harness.c
new file mode 100644
index 000000000..09021feb9
--- /dev/null
+++ b/proofs/cbmc/invntt_tomont/invntt_tomont_harness.c
@@ -0,0 +1,14 @@
+// Copyright (c) 2025 The mldsa-native project authors
+// SPDX-License-Identifier: Apache-2.0
+
+#include <stdint.h>
+#include "ntt.h"
+#include "params.h"
+
+void invntt_tomont(int32_t a[MLDSA_N]);
+
+void harness(void)
+{
+  int32_t *a;
+  invntt_tomont(a);
+}


Reviews:
user: login: mkannwischer
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T00:49:21Z
user: login: mkannwischer
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T01:41:45Z
user: login: jakemas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T06:48:52Z
user: login: jakemas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T06:49:04Z
user: login: mkannwischer
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T08:36:43Z
user: login: rod-chapman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T09:43:39Z
user: login: mkannwischer
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T09:46:34Z
user: login: jakemas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T17:13:24Z
user: login: mkannwischer
body: Thus far this is only a proof for `invntt_layer`. Could you please add the proof for `invntt_tomont` as well? 
Alternatively, if you really want to merge this separately, the contract for `invntt_tomont` needs to be removed.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:55:45Z
user: login: rod-chapman
body: Please see comments on individual files. Nearly there!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T18:49:34Z
user: login: rod-chapman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T18:50:01Z
user: login: jakemas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T19:09:50Z
user: login: jakemas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T19:09:57Z
user: login: jakemas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T19:10:21Z
user: login: jakemas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T19:10:31Z
user: login: jakemas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T19:10:35Z
user: login: jakemas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T19:10:40Z
user: login: rod-chapman
body: Looks good now.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T20:08:06Z
user: login: hanno-becker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T03:24:55Z
user: login: hanno-becker
body: LGTM, thanks @jakemas!
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T03:26:01Z

Review comments:
diff_hunk: @@ -51,8 +51,14 @@ __contract__(
  *              MLDSA_Q in absolute value. Output coefficient are smaller than
  *              MLDSA_Q in absolute value.
  *
- * Arguments:   - uint32_t p[MLDSA_N]: input/output coefficient array
+ * Arguments:   - uint32_t a[MLDSA_N]: input/output coefficient array
  **************************************************/
-void invntt_tomont(int32_t a[MLDSA_N]);
+void invntt_tomont(int32_t a[MLDSA_N])
+__contract__(
+  requires(memory_no_alias(a, MLDSA_N * sizeof(int32_t)))
+  requires(array_abs_bound(a, 0, MLDSA_N, MLD_NTT_BOUND))
path: mldsa/ntt.h
user: login: mkannwischer
body: The comment above the function says `input coefficients need to be smaller than MLDSA_Q`. `MLD_NTT_BOUND` certainly looks wrong as the outputs of an NTT are never directly fed into an INTT (but go through a basemul instead). 
created_at: 2025-04-23T00:49:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -141,6 +141,41 @@ void ntt(int32_t a[MLDSA_N])
   /* are bounded in magnitude by 9 * MLDSA_Q                 */
 }
 
+/* Reference: Embedded into `invntt_tomont()` in the reference implementation
+ * [@REF] */
+static void mld_invntt_layer(int32_t r[MLDSA_N], unsigned layer)
+__contract__(
+  requires(memory_no_alias(r, sizeof(int32_t) * MLDSA_N))
+  requires(1 <= layer && layer <= 8)
+  requires(array_abs_bound(r, 0, MLDSA_N, MLDSA_Q))
+  assigns(memory_slice(r, sizeof(int32_t) * MLDSA_N))
+  ensures(array_abs_bound(r, 0, MLDSA_N, MLDSA_Q)))
path: mldsa/ntt.c
user: login: mkannwischer
body: This post-condition cannot hold as is. 
It is computing `r[j + len] =  r[j]; - r[j + len];`. If inputs are (-q, q), this can be (-2q+1, 2q-1).

Note that in ML-DSA the prime is only 23-bits, so there is a lot of headroom before it overflows the `int32_t`. That is why we need a lot fewer reductions after additions/subtractions than in ML-KEM.
created_at: 2025-04-23T01:41:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,8 +51,14 @@ __contract__(
  *              MLDSA_Q in absolute value. Output coefficient are smaller than
  *              MLDSA_Q in absolute value.
  *
- * Arguments:   - uint32_t p[MLDSA_N]: input/output coefficient array
+ * Arguments:   - uint32_t a[MLDSA_N]: input/output coefficient array
  **************************************************/
-void invntt_tomont(int32_t a[MLDSA_N]);
+void invntt_tomont(int32_t a[MLDSA_N])
+__contract__(
+  requires(memory_no_alias(a, MLDSA_N * sizeof(int32_t)))
+  requires(array_abs_bound(a, 0, MLDSA_N, MLD_NTT_BOUND))
path: mldsa/ntt.h
user: login: jakemas
body: Thank you!
created_at: 2025-04-23T06:48:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -141,6 +141,41 @@ void ntt(int32_t a[MLDSA_N])
   /* are bounded in magnitude by 9 * MLDSA_Q                 */
 }
 
+/* Reference: Embedded into `invntt_tomont()` in the reference implementation
+ * [@REF] */
+static void mld_invntt_layer(int32_t r[MLDSA_N], unsigned layer)
+__contract__(
+  requires(memory_no_alias(r, sizeof(int32_t) * MLDSA_N))
+  requires(1 <= layer && layer <= 8)
+  requires(array_abs_bound(r, 0, MLDSA_N, MLDSA_Q))
+  assigns(memory_slice(r, sizeof(int32_t) * MLDSA_N))
+  ensures(array_abs_bound(r, 0, MLDSA_N, MLDSA_Q)))
path: mldsa/ntt.c
user: login: jakemas
body: Thank you, this helped me get it!
created_at: 2025-04-23T06:49:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -141,6 +143,44 @@ void ntt(int32_t a[MLDSA_N])
   /* are bounded in magnitude by 9 * MLDSA_Q                 */
 }
 
+/* Reference: Embedded into `invntt_tomont()` in the reference implementation
+ * [@REF] */
+static void mld_invntt_layer(int32_t r[MLDSA_N], unsigned layer)
+__contract__(
+  requires(memory_no_alias(r, sizeof(int32_t) * MLDSA_N))
+  requires(1 <= layer && layer <= 8)
+  requires(array_abs_bound(r, 0, MLDSA_N, MLDSA_Q))
+  assigns(memory_slice(r, sizeof(int32_t) * MLDSA_N))
+  ensures(array_abs_bound(r, 0, MLDSA_N, MLDSA_INVNTT_BOUND)))
path: mldsa/ntt.c
user: login: mkannwischer
body: This won't work either.
The post-condition of the i-th layer needs to imply the pre-condition of the (i+1)-th layer. 
created_at: 2025-04-23T08:36:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -141,6 +143,44 @@ void ntt(int32_t a[MLDSA_N])
   /* are bounded in magnitude by 9 * MLDSA_Q                 */
 }
 
+/* Reference: Embedded into `invntt_tomont()` in the reference implementation
+ * [@REF] */
+static void mld_invntt_layer(int32_t r[MLDSA_N], unsigned layer)
+__contract__(
+  requires(memory_no_alias(r, sizeof(int32_t) * MLDSA_N))
+  requires(1 <= layer && layer <= 8)
+  requires(array_abs_bound(r, 0, MLDSA_N, MLDSA_Q))
+  assigns(memory_slice(r, sizeof(int32_t) * MLDSA_N))
+  ensures(array_abs_bound(r, 0, MLDSA_N, MLDSA_INVNTT_BOUND)))
path: mldsa/ntt.c
user: login: rod-chapman
body: I think the pre-condition is
```
requires(array_abs_bound(r, 0, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q))
```
while the post-condition is:
```
ensures(array_abs_bound(r, 0, MLDSA_N, (MLDSA_N >> (layer - 1)) * MLDSA_Q))
```
so that when layer=8, the bounds go from Q to 2Q. When layer=7, the bounds go from 2Q to 4Q and so on. When layer=1, the postconditions says that the final bound is 256Q.

The loop-invariant will need to be updated similarly.
created_at: 2025-04-23T09:43:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -141,6 +143,44 @@ void ntt(int32_t a[MLDSA_N])
   /* are bounded in magnitude by 9 * MLDSA_Q                 */
 }
 
+/* Reference: Embedded into `invntt_tomont()` in the reference implementation
+ * [@REF] */
+static void mld_invntt_layer(int32_t r[MLDSA_N], unsigned layer)
+__contract__(
+  requires(memory_no_alias(r, sizeof(int32_t) * MLDSA_N))
+  requires(1 <= layer && layer <= 8)
+  requires(array_abs_bound(r, 0, MLDSA_N, MLDSA_Q))
+  assigns(memory_slice(r, sizeof(int32_t) * MLDSA_N))
+  ensures(array_abs_bound(r, 0, MLDSA_N, MLDSA_INVNTT_BOUND)))
path: mldsa/ntt.c
user: login: mkannwischer
body: Sound about right.
created_at: 2025-04-23T09:46:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -141,6 +143,44 @@ void ntt(int32_t a[MLDSA_N])
   /* are bounded in magnitude by 9 * MLDSA_Q                 */
 }
 
+/* Reference: Embedded into `invntt_tomont()` in the reference implementation
+ * [@REF] */
+static void mld_invntt_layer(int32_t r[MLDSA_N], unsigned layer)
+__contract__(
+  requires(memory_no_alias(r, sizeof(int32_t) * MLDSA_N))
+  requires(1 <= layer && layer <= 8)
+  requires(array_abs_bound(r, 0, MLDSA_N, MLDSA_Q))
+  assigns(memory_slice(r, sizeof(int32_t) * MLDSA_N))
+  ensures(array_abs_bound(r, 0, MLDSA_N, MLDSA_INVNTT_BOUND)))
path: mldsa/ntt.c
user: login: jakemas
body: Addressed within [8049ec5](https://github.com/pq-code-package/mldsa-native/pull/179/commits/8049ec571905431dc8618c61e41c5ee31bfb60a5)
created_at: 2025-04-23T17:13:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -141,6 +141,44 @@ void ntt(int32_t a[MLDSA_N])
   /* are bounded in magnitude by 9 * MLDSA_Q                 */
 }
 
+/* Reference: Embedded into `invntt_tomont()` in the reference implementation
+ * [@REF] */
+static void mld_invntt_layer(int32_t r[MLDSA_N], unsigned layer)
+__contract__(
+  requires(memory_no_alias(r, sizeof(int32_t) * MLDSA_N))
+  requires(1 <= layer && layer <= 8)
+  requires(array_abs_bound(r, 0, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q))
+  assigns(memory_slice(r, sizeof(int32_t) * MLDSA_N))
+  ensures(array_abs_bound(r, 0, MLDSA_N, (MLDSA_N >> (layer - 1)) * MLDSA_Q)))
+{
+  unsigned start, k, len;
+  len = (MLDSA_N >> layer);
+  k = (1u << layer) - 1;
+  for (start = 0; start < MLDSA_N; start += 2 * len)
+  __loop__(
+    invariant(start <= MLDSA_N && k <= 255)
+    invariant(2 * len * k + start == 2 * MLDSA_N - 2 * len)
+    invariant(array_abs_bound(r, 0, start, (MLDSA_N >> (layer - 1)) * MLDSA_Q))
+    invariant(array_abs_bound(r, start, MLDSA_N, (MLDSA_N >> (layer - 1)) * MLDSA_Q)))
path: mldsa/ntt.c
user: login: rod-chapman
body: This should be `(MLDSA_N >> layer)` not `(MLDSA >> (layer - 1))`

created_at: 2025-04-28T18:44:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,31 +189,25 @@ void ntt(int32_t a[MLDSA_N])
  *              MLDSA_Q in absolute value. Output coefficient are smaller than
  *MLDSA_Q in absolute value.
  *
- * Arguments:   - uint32_t p[MLDSA_N]: input/output coefficient array
+ * Arguments:   - uint32_t a[MLDSA_N]: input/output coefficient array
  **************************************************/
 void invntt_tomont(int32_t a[MLDSA_N])
+
 {
-  unsigned int start, len, j, k;
-  int32_t t, zeta;
+  unsigned int layer, j;
   const int32_t f = 41978; /* mont^2/256 */
 
-  k = 256;
-  for (len = 1; len < MLDSA_N; len <<= 1)
+  for (layer = 8; layer >= 1; layer--)
+  __loop__(
+    invariant(layer <= 8)
+    invariant(array_abs_bound(a, 0, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q)))
path: mldsa/ntt.c
user: login: rod-chapman
body: Please add comment explaining invariant here as per @rod-chapman's version of the code
created_at: 2025-04-28T18:44:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,31 +189,25 @@ void ntt(int32_t a[MLDSA_N])
  *              MLDSA_Q in absolute value. Output coefficient are smaller than
  *MLDSA_Q in absolute value.
  *
- * Arguments:   - uint32_t p[MLDSA_N]: input/output coefficient array
+ * Arguments:   - uint32_t a[MLDSA_N]: input/output coefficient array
  **************************************************/
 void invntt_tomont(int32_t a[MLDSA_N])
+
 {
-  unsigned int start, len, j, k;
-  int32_t t, zeta;
+  unsigned int layer, j;
   const int32_t f = 41978; /* mont^2/256 */
 
-  k = 256;
-  for (len = 1; len < MLDSA_N; len <<= 1)
+  for (layer = 8; layer >= 1; layer--)
+  __loop__(
+    invariant(layer <= 8)
+    invariant(array_abs_bound(a, 0, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q)))
   {
-    for (start = 0; start < MLDSA_N; start = j + len)
-    {
-      zeta = -zetas[--k];
-      for (j = start; j < start + len; ++j)
-      {
-        t = a[j];
-        a[j] = t + a[j + len];
-        a[j + len] = t - a[j + len];
-        a[j + len] = mld_fqmul(a[j + len], zeta);
-      }
-    }
+    mld_invntt_layer(a, layer);
   }
-
   for (j = 0; j < MLDSA_N; ++j)
+  __loop__(
path: mldsa/ntt.c
user: login: rod-chapman
body: please add comment here explaining final inversion and reduction step as per rod-chapman's version of the code.

created_at: 2025-04-28T18:45:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,31 +189,25 @@ void ntt(int32_t a[MLDSA_N])
  *              MLDSA_Q in absolute value. Output coefficient are smaller than
  *MLDSA_Q in absolute value.
path: mldsa/ntt.c
user: login: rod-chapman
body: Fix indenting of this comment please.
created_at: 2025-04-28T18:45:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,55 @@
+# SPDX-License-Identifier: Apache-2.0
+
+include ../Makefile_params.common
+
+HARNESS_ENTRY = harness
+HARNESS_FILE = invntt_tomont_harness
+
+# This should be a unique identifier for this proof, and will appear on the
+# Litani dashboard. It can be human-readable and contain spaces if you wish.
+PROOF_UID = invntt_tomont
+
+DEFINES +=
+INCLUDES +=
+
+REMOVE_FUNCTION_BODY +=
+UNWINDSET +=
+
+PROOF_SOURCES += $(PROOFDIR)/$(HARNESS_FILE).c
+PROJECT_SOURCES += $(SRCDIR)/mldsa/ntt.c
+PROJECT_SOURCES += $(SRCDIR)/mldsa/reduce.c
path: proofs/cbmc/invntt_tomont/Makefile
user: login: rod-chapman
body: reduce.c should not be needed here, since no bodies from that translation unit are inlined in this function.
created_at: 2025-04-28T18:48:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,55 @@
+# SPDX-License-Identifier: Apache-2.0
+
+include ../Makefile_params.common
+
+HARNESS_ENTRY = harness
+HARNESS_FILE = invntt_tomont_harness
+
+# This should be a unique identifier for this proof, and will appear on the
+# Litani dashboard. It can be human-readable and contain spaces if you wish.
+PROOF_UID = invntt_tomont
+
+DEFINES +=
+INCLUDES +=
+
+REMOVE_FUNCTION_BODY +=
+UNWINDSET +=
+
+PROOF_SOURCES += $(PROOFDIR)/$(HARNESS_FILE).c
+PROJECT_SOURCES += $(SRCDIR)/mldsa/ntt.c
+PROJECT_SOURCES += $(SRCDIR)/mldsa/reduce.c
+
+CHECK_FUNCTION_CONTRACTS=$(MLD_NAMESPACE)invntt_tomont
+USE_FUNCTION_CONTRACTS=mld_invntt_layer
path: proofs/cbmc/invntt_tomont/Makefile
user: login: rod-chapman
body: Should also include mld_fqmul in this list to use the contracts, not inline, this function call.
created_at: 2025-04-28T18:49:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -141,6 +141,44 @@ void ntt(int32_t a[MLDSA_N])
   /* are bounded in magnitude by 9 * MLDSA_Q                 */
 }
 
+/* Reference: Embedded into `invntt_tomont()` in the reference implementation
+ * [@REF] */
+static void mld_invntt_layer(int32_t r[MLDSA_N], unsigned layer)
+__contract__(
+  requires(memory_no_alias(r, sizeof(int32_t) * MLDSA_N))
+  requires(1 <= layer && layer <= 8)
+  requires(array_abs_bound(r, 0, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q))
+  assigns(memory_slice(r, sizeof(int32_t) * MLDSA_N))
+  ensures(array_abs_bound(r, 0, MLDSA_N, (MLDSA_N >> (layer - 1)) * MLDSA_Q)))
+{
+  unsigned start, k, len;
+  len = (MLDSA_N >> layer);
+  k = (1u << layer) - 1;
+  for (start = 0; start < MLDSA_N; start += 2 * len)
+  __loop__(
+    invariant(start <= MLDSA_N && k <= 255)
+    invariant(2 * len * k + start == 2 * MLDSA_N - 2 * len)
+    invariant(array_abs_bound(r, 0, start, (MLDSA_N >> (layer - 1)) * MLDSA_Q))
+    invariant(array_abs_bound(r, start, MLDSA_N, (MLDSA_N >> (layer - 1)) * MLDSA_Q)))
path: mldsa/ntt.c
user: login: rod-chapman
body: but only in the second invariant...

created_at: 2025-04-28T18:50:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,55 @@
+# SPDX-License-Identifier: Apache-2.0
+
+include ../Makefile_params.common
+
+HARNESS_ENTRY = harness
+HARNESS_FILE = invntt_tomont_harness
+
+# This should be a unique identifier for this proof, and will appear on the
+# Litani dashboard. It can be human-readable and contain spaces if you wish.
+PROOF_UID = invntt_tomont
+
+DEFINES +=
+INCLUDES +=
+
+REMOVE_FUNCTION_BODY +=
+UNWINDSET +=
+
+PROOF_SOURCES += $(PROOFDIR)/$(HARNESS_FILE).c
+PROJECT_SOURCES += $(SRCDIR)/mldsa/ntt.c
+PROJECT_SOURCES += $(SRCDIR)/mldsa/reduce.c
+
+CHECK_FUNCTION_CONTRACTS=$(MLD_NAMESPACE)invntt_tomont
+USE_FUNCTION_CONTRACTS=mld_invntt_layer
path: proofs/cbmc/invntt_tomont/Makefile
user: login: jakemas
body: added in [6c53ce6](https://github.com/pq-code-package/mldsa-native/pull/179/commits/6c53ce6c53e2d6a6ebc4c95087f4ab28b0d0708a)
created_at: 2025-04-28T19:09:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,55 @@
+# SPDX-License-Identifier: Apache-2.0
+
+include ../Makefile_params.common
+
+HARNESS_ENTRY = harness
+HARNESS_FILE = invntt_tomont_harness
+
+# This should be a unique identifier for this proof, and will appear on the
+# Litani dashboard. It can be human-readable and contain spaces if you wish.
+PROOF_UID = invntt_tomont
+
+DEFINES +=
+INCLUDES +=
+
+REMOVE_FUNCTION_BODY +=
+UNWINDSET +=
+
+PROOF_SOURCES += $(PROOFDIR)/$(HARNESS_FILE).c
+PROJECT_SOURCES += $(SRCDIR)/mldsa/ntt.c
+PROJECT_SOURCES += $(SRCDIR)/mldsa/reduce.c
path: proofs/cbmc/invntt_tomont/Makefile
user: login: jakemas
body: removed in [6c53ce6](https://github.com/pq-code-package/mldsa-native/pull/179/commits/6c53ce6c53e2d6a6ebc4c95087f4ab28b0d0708a)
created_at: 2025-04-28T19:09:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,31 +189,25 @@ void ntt(int32_t a[MLDSA_N])
  *              MLDSA_Q in absolute value. Output coefficient are smaller than
  *MLDSA_Q in absolute value.
path: mldsa/ntt.c
user: login: jakemas
body: done (this was auto generated when we changed variable names with the prefix and ran linting)
created_at: 2025-04-28T19:10:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,31 +189,25 @@ void ntt(int32_t a[MLDSA_N])
  *              MLDSA_Q in absolute value. Output coefficient are smaller than
  *MLDSA_Q in absolute value.
  *
- * Arguments:   - uint32_t p[MLDSA_N]: input/output coefficient array
+ * Arguments:   - uint32_t a[MLDSA_N]: input/output coefficient array
  **************************************************/
 void invntt_tomont(int32_t a[MLDSA_N])
+
 {
-  unsigned int start, len, j, k;
-  int32_t t, zeta;
+  unsigned int layer, j;
   const int32_t f = 41978; /* mont^2/256 */
 
-  k = 256;
-  for (len = 1; len < MLDSA_N; len <<= 1)
+  for (layer = 8; layer >= 1; layer--)
+  __loop__(
+    invariant(layer <= 8)
+    invariant(array_abs_bound(a, 0, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q)))
   {
-    for (start = 0; start < MLDSA_N; start = j + len)
-    {
-      zeta = -zetas[--k];
-      for (j = start; j < start + len; ++j)
-      {
-        t = a[j];
-        a[j] = t + a[j + len];
-        a[j + len] = t - a[j + len];
-        a[j + len] = mld_fqmul(a[j + len], zeta);
-      }
-    }
+    mld_invntt_layer(a, layer);
   }
-
   for (j = 0; j < MLDSA_N; ++j)
+  __loop__(
path: mldsa/ntt.c
user: login: jakemas
body: added in [6c53ce6](https://github.com/pq-code-package/mldsa-native/pull/179/commits/6c53ce6c53e2d6a6ebc4c95087f4ab28b0d0708a)
created_at: 2025-04-28T19:10:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,31 +189,25 @@ void ntt(int32_t a[MLDSA_N])
  *              MLDSA_Q in absolute value. Output coefficient are smaller than
  *MLDSA_Q in absolute value.
  *
- * Arguments:   - uint32_t p[MLDSA_N]: input/output coefficient array
+ * Arguments:   - uint32_t a[MLDSA_N]: input/output coefficient array
  **************************************************/
 void invntt_tomont(int32_t a[MLDSA_N])
+
 {
-  unsigned int start, len, j, k;
-  int32_t t, zeta;
+  unsigned int layer, j;
   const int32_t f = 41978; /* mont^2/256 */
 
-  k = 256;
-  for (len = 1; len < MLDSA_N; len <<= 1)
+  for (layer = 8; layer >= 1; layer--)
+  __loop__(
+    invariant(layer <= 8)
+    invariant(array_abs_bound(a, 0, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q)))
path: mldsa/ntt.c
user: login: jakemas
body: added in [6c53ce6](https://github.com/pq-code-package/mldsa-native/pull/179/commits/6c53ce6c53e2d6a6ebc4c95087f4ab28b0d0708a)
created_at: 2025-04-28T19:10:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -141,6 +141,44 @@ void ntt(int32_t a[MLDSA_N])
   /* are bounded in magnitude by 9 * MLDSA_Q                 */
 }
 
+/* Reference: Embedded into `invntt_tomont()` in the reference implementation
+ * [@REF] */
+static void mld_invntt_layer(int32_t r[MLDSA_N], unsigned layer)
+__contract__(
+  requires(memory_no_alias(r, sizeof(int32_t) * MLDSA_N))
+  requires(1 <= layer && layer <= 8)
+  requires(array_abs_bound(r, 0, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q))
+  assigns(memory_slice(r, sizeof(int32_t) * MLDSA_N))
+  ensures(array_abs_bound(r, 0, MLDSA_N, (MLDSA_N >> (layer - 1)) * MLDSA_Q)))
+{
+  unsigned start, k, len;
+  len = (MLDSA_N >> layer);
+  k = (1u << layer) - 1;
+  for (start = 0; start < MLDSA_N; start += 2 * len)
+  __loop__(
+    invariant(start <= MLDSA_N && k <= 255)
+    invariant(2 * len * k + start == 2 * MLDSA_N - 2 * len)
+    invariant(array_abs_bound(r, 0, start, (MLDSA_N >> (layer - 1)) * MLDSA_Q))
+    invariant(array_abs_bound(r, start, MLDSA_N, (MLDSA_N >> (layer - 1)) * MLDSA_Q)))
path: mldsa/ntt.c
user: login: jakemas
body: [6c53ce6](https://github.com/pq-code-package/mldsa-native/pull/179/commits/6c53ce6c53e2d6a6ebc4c95087f4ab28b0d0708a)
created_at: 2025-04-28T19:10:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -141,6 +141,44 @@ void ntt(int32_t a[MLDSA_N])
   /* are bounded in magnitude by 9 * MLDSA_Q                 */
 }
 
+/* Reference: Embedded into `invntt_tomont()` in the reference implementation
+ * [@REF] */
+static void mld_invntt_layer(int32_t r[MLDSA_N], unsigned layer)
+__contract__(
+  requires(memory_no_alias(r, sizeof(int32_t) * MLDSA_N))
+  requires(1 <= layer && layer <= 8)
+  requires(array_abs_bound(r, 0, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q))
+  assigns(memory_slice(r, sizeof(int32_t) * MLDSA_N))
+  ensures(array_abs_bound(r, 0, MLDSA_N, (MLDSA_N >> (layer - 1)) * MLDSA_Q)))
+{
+  unsigned start, k, len;
+  len = (MLDSA_N >> layer);
+  k = (1u << layer) - 1;
+  for (start = 0; start < MLDSA_N; start += 2 * len)
+  __loop__(
+    invariant(start <= MLDSA_N && k <= 255)
+    invariant(2 * len * k + start == 2 * MLDSA_N - 2 * len)
+    invariant(array_abs_bound(r, 0, start, (MLDSA_N >> (layer - 1)) * MLDSA_Q))
+    invariant(array_abs_bound(r, start, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q)))
+  {
+    unsigned j;
+    int32_t zeta = -zetas[k--];
+
+    for (j = start; j < start + len; j++)
+    __loop__(
+      invariant(start <= j && j <= start + len)
+      invariant(array_abs_bound(r, 0, start, (MLDSA_N >> (layer - 1)) * MLDSA_Q))
+      invariant(array_abs_bound(r, start, j, (MLDSA_N >> (layer - 1)) * MLDSA_Q))
+      invariant(array_abs_bound(r, j, start + len, (MLDSA_N >> layer) * MLDSA_Q))
+      invariant(array_abs_bound(r, start + len, MLDSA_N, (MLDSA_N >> layer) * MLDSA_Q)))
path: mldsa/ntt.c
user: login: hanno-becker
body: That's a clever simplification.
created_at: 2025-04-29T03:24:54Z
author_association: CONTRIBUTOR

PR comments:
user: login: jakemas
created_at: 2025-04-25T20:07:46Z
body: Ok, added proof for `invntt_tomont`. Need to work on tighter contracts as the ones I want to add are causing the proof to fail.
author_association: CONTRIBUTOR