val lemma_optimized_barrett_reduce2:
  a:nat{a < pow2 512} ->
  Lemma (a - ((a * (pow2 512 / S.q)) / pow2 512) * S.q < pow2 264 /\
         a - ((a * (pow2 512 / S.q)) / pow2 512) * S.q >= 0)
let lemma_optimized_barrett_reduce2 a =
  assert_norm (pow2 248 = 0x100000000000000000000000000000000000000000000000000000000000000);
  assert_norm (pow2 264 = 0x1000000000000000000000000000000000000000000000000000000000000000000);
  assert_norm (S.q == 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed);
  assert_norm (0x100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 == pow2 512)
val lemma_optimized_barrett_reduce2:
  a:nat{a < pow2 512} ->
  Lemma (a - ((a * (pow2 512 / S.q)) / pow2 512) * S.q < pow2 264 /\
         a - ((a * (pow2 512 / S.q)) / pow2 512) * S.q >= 0)
let lemma_optimized_barrett_reduce2 a =
assert_norm (pow2 248 = 0x100000000000000000000000000000000000000000000000000000000000000);
assert_norm (pow2 264 = 0x1000000000000000000000000000000000000000000000000000000000000000000);
assert_norm (S.q == 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed);
assert_norm (0x100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 ==
    pow2 512)
Hacl.Spec.BignumQ.Lemmas.fst.checked
Spec.Ed25519.fst.checked prims.fst.checked Lib.IntTypes.fsti.checked Hacl.Spec.BignumQ.Definitions.fst.checked FStar.UInt32.fsti.checked FStar.Tactics.Effect.fsti.checked FStar.Tactics.CanonCommSemiring.fst.checked FStar.Tactics.fst.checked FStar.Pervasives.Native.fst.checked FStar.Pervasives.fsti.checked FStar.Mul.fst.checked FStar.Math.Lemmas.fst.checked FStar.Calc.fsti.checked
Hacl.Spec.BignumQ.Lemmas.fst
Prims.nat Prims.b2t Prims.op_LessThan Prims.pow2 FStar.Pervasives.assert_norm Prims.eq2 Prims.int Prims.unit Spec.Ed25519.q Prims.op_Equality
module Hacl.Spec.BignumQ.Lemmas

open FStar.Tactics.CanonCommSemiring
open FStar.Mul
open Lib.IntTypes

module S = Spec.Ed25519

include Hacl.Spec.BignumQ.Definitions

let feq (#a #b:Type) (f:(a -> b)) (x y:a) :
  Lemma (requires x == y) (ensures f x == f y) = ()

let eq_eq2 (#a:eqtype) (x y:a) :
  Lemma (requires x = y) (ensures x == y) = ()

val lemma_mul_lt:a:nat -> b:nat -> c:nat -> d:nat ->
  Lemma
  (requires a < b /\ c < d)
  (ensures  a * c < b * d)

let lemma_mul_lt a b c d = ()


val lemma_as_nat5: f:qelem5 ->
  Lemma
  (requires qelem_fits5 f (1, 1, 1, 1, 1))
  (ensures  as_nat5 f < pow2 280)

let lemma_as_nat5 f =
  //let (f0, f1, f2, f3, f4) = f in
  //assert (as_nat5 f == v f0 + v f1 * pow56 + v f2 * pow112 + v f3 * pow168 + v f4 * pow224);
  assert_norm (pow2 56 * pow2 56 = pow2 112);
  assert_norm (pow2 56 * pow2 112 = pow2 168);
  assert_norm (pow2 56 * pow2 168 = pow2 224);
  assert_norm (pow2 56 * pow2 224 = pow2 280)


val lemma_choose_step:
    bit:uint64{v bit <= 1}
  -> x:uint64
  -> y:uint64
  -> Lemma
     (let mask = bit -. u64 1 in
      let z = x ^. (mask &. (x ^. y)) in
      if v bit = 1 then z == x else z == y)

let lemma_choose_step bit p1 p2 =
  let mask = bit -. u64 1 in
  assert (v bit == 0 ==> v mask == pow2 64 - 1);
  assert (v bit == 1 ==> v mask == 0);
  let dummy = mask &. (p1 ^. p2) in
  logand_lemma mask (p1 ^. p2);
  assert (v bit == 1 ==> v dummy == 0);
  assert (v bit == 0 ==> v dummy ==  v (p1 ^. p2));
  let p1' = p1 ^. dummy in
  assert (v dummy == v (if v bit = 1 then u64 0 else (p1 ^. p2)));
  logxor_lemma p1 p2


val lemma_subm_conditional:
    x0:nat -> x1:nat -> x2:nat -> x3:nat -> x4:nat
  -> y0:nat -> y1:nat -> y2:nat -> y3:nat -> y4:nat
  -> b0:nat -> b1:nat -> b2:nat -> b3:nat -> b4:nat ->
  Lemma (
    x0 - y0 + b0 * pow2 56 +
    (x1 - y1 - b0 + b1 * pow2 56) * pow2 56 +
    (x2 - y2 - b1 + b2 * pow2 56) * pow2 112 +
    (x3 - y3 - b2 + b3 * pow2 56) * pow2 168 +
    (x4 - y4 - b3 + b4 * pow2 56) * pow2 224 ==
    (x0 + x1 * pow2 56 + x2 * pow2 112 + x3 * pow2 168 + x4 * pow2 224) -
    (y0 + y1 * pow2 56 + y2 * pow2 112 + y3 * pow2 168 + y4 * pow2 224) + b4 * pow2 280)

#push-options "--z3rlimit 50"
let lemma_subm_conditional x0 x1 x2 x3 x4 y0 y1 y2 y3 y4 b0 b1 b2 b3 b4 =
  assert_norm (pow2 56 * pow2 56 = pow2 112);
  assert_norm (pow2 56 * pow2 112 = pow2 168);
  assert_norm (pow2 56 * pow2 168 = pow2 224);
  assert_norm (pow2 56 * pow2 224 = pow2 280)
#pop-options

val lemma_div224: x:qelem_wide5 ->
  Lemma
  (requires
    qelem_wide_fits5 x (1, 1, 1, 1, 1, 1, 1, 1, 1, 1))
  (ensures
   (let (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) = x in
    wide_as_nat5 x / pow2 224 ==
    v x4 + v x5 * pow2 56 + v x6 * pow2 112 + v x7 * pow2 168 + v x8 * pow2 224 + v x9 * pow2 280))

#push-options "--z3rlimit 50"
let lemma_div224 x =
  let (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) = x in
  assert
    (wide_as_nat5 x ==
     v x0 + v x1 * pow2 56 + v x2 * pow2 112 + v x3 * pow2 168 + v x4 * pow2 224 +
     v x5 * pow2 280 + v x6 * pow2 336 + v x7 * pow2 392 + v x8 * pow2 448 + v x9 * pow2 504);
  assert_norm (pow2 56 * pow2 224 == pow2 280);
  assert_norm (pow2 112 * pow2 224 == pow2 336);
  assert_norm (pow2 168 * pow2 224 == pow2 392);
  assert_norm (pow2 224 * pow2 224 == pow2 448);
  assert_norm (pow2 280 * pow2 224 == pow2 504);
  calc (==) {
    wide_as_nat5 x / pow2 224;
    (==) { }
    (v x0 + v x1 * pow2 56 + v x2 * pow2 112 + v x3 * pow2 168 +
    (v x4 + v x5 * pow2 56 + v x6 * pow2 112 + v x7 * pow2 168 + v x8 * pow2 224 + v x9 * pow2 280) * pow2 224) / pow2 224;
    (==) {
      FStar.Math.Lemmas.lemma_div_plus
        (v x0 + v x1 * pow2 56 + v x2 * pow2 112 + v x3 * pow2 168)
        (v x4 + v x5 * pow2 56 + v x6 * pow2 112 + v x7 * pow2 168 + v x8 * pow2 224 + v x9 * pow2 280) (pow2 224) }
    (v x0 + v x1 * pow2 56 + v x2 * pow2 112 + v x3 * pow2 168) / pow2 224 +
    v x4 + v x5 * pow2 56 + v x6 * pow2 112 + v x7 * pow2 168 + v x8 * pow2 224 + v x9 * pow2 280;
    (==) { FStar.Math.Lemmas.small_division_lemma_1 (v x0 + v x1 * pow2 56 + v x2 * pow2 112 + v x3 * pow2 168) (pow2 224) }
      v x4 + v x5 * pow2 56 + v x6 * pow2 112 + v x7 * pow2 168 + v x8 * pow2 224 + v x9 * pow2 280;
    }
#pop-options

val lemma_div248_aux: x:qelem_wide5 ->
  Lemma
  (requires
    qelem_wide_fits5 x (1, 1, 1, 1, 1, 1, 1, 1, 1, 1) /\
    wide_as_nat5 x < pow2 512)
  (ensures
   (let (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) = x in
    wide_as_nat5 x / pow2 248 ==
      v x4 / pow2 24 + v x5 * pow2 32 + v x6 * pow2 88 + v x7 * pow2 144 + v x8 * pow2 200 + v x9 * pow2 256))

open FStar.Tactics.CanonCommSemiring

#push-options "--z3cliopt smt.arith.nl=false"
let lemma_div248_aux x =
  let (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) = x in
  assert_norm (pow2 248 == pow2 224 * pow2 24);
  assert_norm (pow2 56 == pow2 32 * pow2 24);
  assert_norm (pow2 112 == pow2 88 * pow2 24);
  assert_norm (pow2 168 == pow2 144 * pow2 24);
  assert_norm (pow2 224 == pow2 200 * pow2 24);
  assert_norm (pow2 280 == pow2 256 * pow2 24);
  assert_norm (0 < pow2 24);

  calc (==) {
    wide_as_nat5 x / pow2 248;
  (==) { FStar.Math.Lemmas.division_multiplication_lemma (wide_as_nat5 x) (pow2 224) (pow2 24) }
    (wide_as_nat5 x / pow2 224) / pow2 24;
  (==) { lemma_div224 x }
    (v x4 + v x5 * pow2 56 + v x6 * pow2 112 + v x7 * pow2 168 + v x8 * pow2 224 + v x9 * pow2 280) / pow2 24;
  (==) { _ by (Tactics.mapply (`feq #int #int (fun x -> x / pow2 24)); int_semiring ()) }    (v x4 + (v x5 * pow2 32 + v x6 * pow2 88 + v x7 * pow2 144 + v x8 * pow2 200 + v x9 * pow2 256) * pow2 24) / pow2 24;
  (==) { FStar.Math.Lemmas.lemma_div_plus (v x4) (v x5 * pow2 32 + v x6 * pow2 88 + v x7 * pow2 144 + v x8 * pow2 200 + v x9 * pow2 256) (pow2 24) }
    v x4 / pow2 24 + v x5 * pow2 32 + v x6 * pow2 88 + v x7 * pow2 144 + v x8 * pow2 200 + v x9 * pow2 256;
  }
#pop-options

val lemma_div248_x5: x5:uint64 ->
  Lemma ( pow2 32 * (v x5 % pow2 24) + v x5 / pow2 24 * pow2 56 == v x5 * pow2 32)
let lemma_div248_x5 x5 =
  assert_norm (pow2 32 * pow2 24 = pow2 56)

val lemma_div248_x6: x6:uint64 ->
  Lemma (pow2 32 * (v x6 % pow2 24) * pow2 56 + v x6 / pow2 24 * pow2 112 == v x6 * pow2 88)
let lemma_div248_x6 x6 =
  calc (==) {
    pow2 32 * (v x6 % pow2 24) * pow2 56 + v x6 / pow2 24 * pow2 112;
    (==) { _ by (Tactics.norm [delta_only [`%pow2]; primops]; int_semiring ()) }
    ((v x6 / pow2 24) * pow2 24 + v x6 % pow2 24) * pow2 88;
    (==) { FStar.Math.Lemmas.euclidean_division_definition (v x6) (pow2 24) }
    v x6 * pow2 88;
  }


val lemma_div248_x7: x7:uint64 ->
  Lemma (pow2 32 * (v x7 % pow2 24) * pow2 112 + v x7 / pow2 24 * pow2 168 == v x7 * pow2 144)
let lemma_div248_x7 x7 =
  calc (==) {
    pow2 32 * (v x7 % pow2 24) * pow2 112 + v x7 / pow2 24 * pow2 168;
    (==) { _ by (Tactics.norm [delta_only [`%pow2]; primops]; int_semiring ()) }
    ((v x7 / pow2 24) * pow2 24 + v x7 % pow2 24) * pow2 144;
    (==) { FStar.Math.Lemmas.euclidean_division_definition (v x7) (pow2 24) }
    v x7 * pow2 144;
  }


val lemma_div248_x8: x8:uint64 ->
  Lemma (pow2 32 * (v x8 % pow2 24) * pow2 168 + v x8 / pow2 24 * pow2 224 == v x8 * pow2 200)
let lemma_div248_x8 x8 =
  calc (==) {
    pow2 32 * (v x8 % pow2 24) * pow2 168 + v x8 / pow2 24 * pow2 224;
    (==) { _ by (Tactics.norm [delta_only [`%pow2]; primops]; int_semiring ()) }
    ((v x8 / pow2 24) * pow2 24 + v x8 % pow2 24) * pow2 200;
    (==) { FStar.Math.Lemmas.euclidean_division_definition (v x8) (pow2 24) }
    v x8 * pow2 200;
  }


val lemma_div248_x9: x9:uint64{v x9 < pow2 24} ->
  Lemma (pow2 32 * (v x9 % pow2 24) * pow2 224 == v x9 * pow2 256)
let lemma_div248_x9 x9 =
  calc (==) {
    pow2 32 * (v x9 % pow2 24) * pow2 224;
    (==) { Math.Lemmas.small_mod (v x9) (pow2 24) }
    pow2 32 * v x9 * pow2 224;
    (==) { _ by (Tactics.norm [delta; primops]; int_semiring ()) }
    v x9 * pow2 256;
  }


val lemma_wide_as_nat_pow512: x:qelem_wide5 ->
  Lemma
  (requires
    qelem_wide_fits5 x (1, 1, 1, 1, 1, 1, 1, 1, 1, 1) /\
    wide_as_nat5 x < pow2 512)
  (ensures
   (let (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) = x in
    v x9 < pow2 24))

let lemma_wide_as_nat_pow512 x =
  let (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) = x in
  assert_norm (pow2 504 * pow2 8 = pow2 512);
  FStar.Math.Lemmas.pow2_minus 512 504;
  assert (v x9 < pow2 8);
  assert_norm (pow2 8 < pow2 24)


val lemma_div248: x:qelem_wide5 ->
  Lemma
  (requires
    qelem_wide_fits5 x (1, 1, 1, 1, 1, 1, 1, 1, 1, 1) /\
    wide_as_nat5 x < pow2 512)
  (ensures
   (let (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) = x in
    let z0 = v x4 / pow2 24 + pow2 32 * (v x5 % pow2 24) in
    let z1 = v x5 / pow2 24 + pow2 32 * (v x6 % pow2 24) in
    let z2 = v x6 / pow2 24 + pow2 32 * (v x7 % pow2 24) in
    let z3 = v x7 / pow2 24 + pow2 32 * (v x8 % pow2 24) in
    let z4 = v x8 / pow2 24 + pow2 32 * (v x9 % pow2 24) in

    wide_as_nat5 x / pow2 248 == z0 + z1 * pow2 56 + z2 * pow2 112 + z3 * pow2 168 + z4 * pow2 224))

#push-options "--z3rlimit 50"
let lemma_div248 x =
  let (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) = x in
  lemma_wide_as_nat_pow512 x;
  assert (v x9 < pow2 24);

  calc (==) {
    (let z0 = v x4 / pow2 24 + pow2 32 * (v x5 % pow2 24) in
    let z1 = v x5 / pow2 24 + pow2 32 * (v x6 % pow2 24) in
    let z2 = v x6 / pow2 24 + pow2 32 * (v x7 % pow2 24) in
    let z3 = v x7 / pow2 24 + pow2 32 * (v x8 % pow2 24) in
    let z4 = v x8 / pow2 24 + pow2 32 * (v x9 % pow2 24) in
    z0 + z1 * pow2 56 + z2 * pow2 112 + z3 * pow2 168 + z4 * pow2 224);
  (==) { _ by (Tactics.norm [delta; primops]; int_semiring ()) }
    v x4 / pow2 24 + pow2 32 * (v x5 % pow2 24) +
    v x5 / pow2 24 * pow2 56 + pow2 32 * (v x6 % pow2 24) * pow2 56 +
    v x6 / pow2 24 * pow2 112 + pow2 32 * (v x7 % pow2 24) * pow2 112 +
    v x7 / pow2 24 * pow2 168 + pow2 32 * (v x8 % pow2 24) * pow2 168 +
    v x8 / pow2 24 * pow2 224 + pow2 32 * (v x9 % pow2 24) * pow2 224;
  (==) { lemma_div248_x5 x5; lemma_div248_x6 x6 }
    v x4 / pow2 24 + v x5 * pow2 32 + v x6 * pow2 88 +
    pow2 32 * (v x7 % pow2 24) * pow2 112 +
    v x7 / pow2 24 * pow2 168 + pow2 32 * (v x8 % pow2 24) * pow2 168 +
    v x8 / pow2 24 * pow2 224 + pow2 32 * (v x9 % pow2 24) * pow2 224;
  (==) { lemma_div248_x7 x7; lemma_div248_x8 x8 }
    v x4 / pow2 24 + v x5 * pow2 32 + v x6 * pow2 88 + v x7 * pow2 144 + v x8 * pow2 200 +
    pow2 32 * (v x9 % pow2 24) * pow2 224;
  (==) { lemma_div248_x9 x9 }
    v x4 / pow2 24 + v x5 * pow2 32 + v x6 * pow2 88 + v x7 * pow2 144 + v x8 * pow2 200 + v x9 * pow2 256;
  (==) { lemma_div248_aux x }
    wide_as_nat5 x / pow2 248;
  }
#pop-options

val lemma_add_modq5:
    x:qelem5
  -> y:qelem5
  -> t:qelem5 ->
  Lemma
  (requires
    qelem_fits5 x (1, 1, 1, 1, 1) /\
    qelem_fits5 y (1, 1, 1, 1, 1) /\
    qelem_fits5 t (1, 1, 1, 1, 1) /\
    as_nat5 x < S.q /\ as_nat5 y < S.q /\
    as_nat5 t == as_nat5 x + as_nat5 y)
  (ensures
   (let res = if as_nat5 t >= S.q then as_nat5 t - S.q else as_nat5 t in
    res < S.q /\ res == (as_nat5 x + as_nat5 y) % S.q))

let lemma_add_modq5 x y t =
  assert (as_nat5 t == as_nat5 x + as_nat5 y);
  let res = if as_nat5 t >= S.q then as_nat5 t - S.q else as_nat5 t in
  assert (res < S.q);

  if as_nat5 t >= S.q then (
    FStar.Math.Lemmas.sub_div_mod_1 (as_nat5 t) S.q;
    assert (res % S.q == as_nat5 t % S.q))
  else
    assert (res % S.q == as_nat5 t % S.q);
  FStar.Math.Lemmas.small_mod res S.q


val lemma_wide_as_nat_pow528: x:qelem_wide5 ->
  Lemma
  (requires
    qelem_wide_fits5 x (1, 1, 1, 1, 1, 1, 1, 1, 1, 1) /\
    wide_as_nat5 x < pow2 528)
  (ensures
   (let (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) = x in
    v x9 < pow2 40))

let lemma_wide_as_nat_pow528 x =
  let (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) = x in
  assert_norm (pow2 504 * pow2 24 = pow2 528);
  FStar.Math.Lemmas.pow2_minus 528 504;
  assert (v x9 < pow2 24);
  assert_norm (pow2 24 < pow2 40)

#push-options "--z3cliopt smt.arith.nl=false"
val lemma_div264_aux: x:qelem_wide5 ->
  Lemma
  (requires
    qelem_wide_fits5 x (1, 1, 1, 1, 1, 1, 1, 1, 1, 1) /\
    wide_as_nat5 x < pow2 528)
  (ensures
   (let (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) = x in
    wide_as_nat5 x / pow2 264 ==
      v x4 / pow2 40 + v x5 * pow2 16 + v x6 * pow2 72 + v x7 * pow2 128 + v x8 * pow2 184 + v x9 * pow2 240))

let lemma_div264_aux x =
  let (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) = x in
  assert_norm (pow2 264 == pow2 224 * pow2 40);
  assert_norm (pow2 56 == pow2 16 * pow2 40);
  assert_norm (pow2 112 == pow2 72 * pow2 40);
  assert_norm (pow2 168 == pow2 128 * pow2 40);
  assert_norm (pow2 224 == pow2 184 * pow2 40);
  assert_norm (pow2 280 == pow2 240 * pow2 40);
  assert_norm (0 < pow2 40);

  calc (==) {
    wide_as_nat5 x / pow2 264;
  (==) { FStar.Math.Lemmas.division_multiplication_lemma (wide_as_nat5 x) (pow2 224) (pow2 40) }
    (wide_as_nat5 x / pow2 224) / pow2 40;
  (==) { lemma_div224 x }
    (v x4 + v x5 * pow2 56 + v x6 * pow2 112 + v x7 * pow2 168 + v x8 * pow2 224 + v x9 * pow2 280) / pow2 40;
  (==) { _ by (Tactics.mapply (`feq #int #int (fun x -> x / pow2 40)); int_semiring ()) }
    (v x4 + (v x5 * pow2 16 + v x6 * pow2 72 + v x7 * pow2 128 + v x8 * pow2 184 + v x9 * pow2 240) * pow2 40) / pow2 40;
  (==) { FStar.Math.Lemmas.lemma_div_plus (v x4) (v x5 * pow2 16 + v x6 * pow2 72 + v x7 * pow2 128 + v x8 * pow2 184 + v x9 * pow2 240) (pow2 40) }
    v x4 / pow2 40 + v x5 * pow2 16 + v x6 * pow2 72 + v x7 * pow2 128 + v x8 * pow2 184 + v x9 * pow2 240;
  }


val lemma_div264_x5: x5:uint64 ->
  Lemma (pow2 16 * (v x5 % pow2 40) + v x5 / pow2 40 * pow2 56 == v x5 * pow2 16)
let lemma_div264_x5 x5 =
  assert_norm (0 < pow2 24);
  calc (==) {
    pow2 16 * (v x5 % pow2 40) + v x5 / pow2 40 * pow2 56;
    (==) { _ by (Tactics.norm [delta_only [`%pow2]; primops]; int_semiring ()) }
    ((v x5 / pow2 40) * pow2 40 + v x5 % pow2 40) * pow2 16;
    (==) { FStar.Math.Lemmas.euclidean_division_definition (v x5) (pow2 40) }
    v x5 * pow2 16;
  }


val lemma_div264_x6: x6:uint64 ->
  Lemma (pow2 16 * (v x6 % pow2 40) * pow2 56 + v x6 / pow2 40 * pow2 112 == v x6 * pow2 72)
let lemma_div264_x6 x6 =
  calc (==) {
    pow2 16 * (v x6 % pow2 40) * pow2 56 + v x6 / pow2 40 * pow2 112;
    (==) { _ by (Tactics.norm [delta; primops]; int_semiring ()) }
    ((v x6 / pow2 40) * pow2 40 + v x6 % pow2 40) * pow2 72;
    (==) { Math.Lemmas.euclidean_division_definition (v x6) (pow2 40) }
    v x6 * pow2 72;
  }


val lemma_div264_x7: x7:uint64 ->
  Lemma (pow2 16 * (v x7 % pow2 40) * pow2 112 + v x7 / pow2 40 * pow2 168 == v x7 * pow2 128)
let lemma_div264_x7 x7 =
  calc (==) {
    pow2 16 * (v x7 % pow2 40) * pow2 112 + v x7 / pow2 40 * pow2 168;
    (==) { _ by (Tactics.norm [delta; primops]; int_semiring ()) }
    ((v x7 / pow2 40) * pow2 40 + v x7 % pow2 40) * pow2 128;
    (==) { Math.Lemmas.euclidean_division_definition (v x7) (pow2 40) }
    v x7 * pow2 128;
  }


val lemma_div264_x8: x8:uint64 ->
  Lemma (pow2 16 * (v x8 % pow2 40) * pow2 168 + v x8 / pow2 40 * pow2 224 == v x8 * pow2 184)
let lemma_div264_x8 x8 =
  calc (==) {
    pow2 16 * (v x8 % pow2 40) * pow2 168 + v x8 / pow2 40 * pow2 224;
    (==) { _ by (Tactics.norm [delta; primops]; int_semiring ()) }
    ((v x8 / pow2 40) * pow2 40 + v x8 % pow2 40) * pow2 184;
    (==) { Math.Lemmas.euclidean_division_definition (v x8) (pow2 40) }
    v x8 * pow2 184;
  }

val lemma_div264_x9: x9:uint64{v x9 < pow2 40} ->
  Lemma (pow2 16 * (v x9 % pow2 40) * pow2 224 == v x9 * pow2 240)
let lemma_div264_x9 x9 =
  calc (==) {
    pow2 16 * (v x9 % pow2 40) * pow2 224;
    (==) { Math.Lemmas.small_mod (v x9) (pow2 40) }
    pow2 16 * v x9 * pow2 224;
    (==) { _ by (Tactics.norm [delta; primops]; int_semiring ()) }
    v x9 * pow2 240;
  }

val lemma_div264: x:qelem_wide5 ->
  Lemma
  (requires
    qelem_wide_fits5 x (1, 1, 1, 1, 1, 1, 1, 1, 1, 1) /\
    wide_as_nat5 x < pow2 528)
  (ensures
   (let (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) = x in
    let z0 = v x4 / pow2 40 + pow2 16 * (v x5 % pow2 40) in
    let z1 = v x5 / pow2 40 + pow2 16 * (v x6 % pow2 40) in
    let z2 = v x6 / pow2 40 + pow2 16 * (v x7 % pow2 40) in
    let z3 = v x7 / pow2 40 + pow2 16 * (v x8 % pow2 40) in
    let z4 = v x8 / pow2 40 + pow2 16 * (v x9 % pow2 40) in

    wide_as_nat5 x / pow2 264 == z0 + z1 * pow2 56 + z2 * pow2 112 + z3 * pow2 168 + z4 * pow2 224))

#push-options "--z3rlimit 50"
let lemma_div264 x =
  let (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) = x in
  lemma_wide_as_nat_pow528 x;
  assert (v x9 < pow2 40);

  calc (==) {
   (let z0 = v x4 / pow2 40 + pow2 16 * (v x5 % pow2 40) in
    let z1 = v x5 / pow2 40 + pow2 16 * (v x6 % pow2 40) in
    let z2 = v x6 / pow2 40 + pow2 16 * (v x7 % pow2 40) in
    let z3 = v x7 / pow2 40 + pow2 16 * (v x8 % pow2 40) in
    let z4 = v x8 / pow2 40 + pow2 16 * (v x9 % pow2 40) in
    z0 + z1 * pow2 56 + z2 * pow2 112 + z3 * pow2 168 + z4 * pow2 224);
  (==) { _ by (Tactics.norm [delta; primops]; int_semiring ()) }
    v x4 / pow2 40 + pow2 16 * (v x5 % pow2 40) +
    v x5 / pow2 40 * pow2 56 + pow2 16 * (v x6 % pow2 40) * pow2 56 +
    v x6 / pow2 40 * pow2 112 + pow2 16 * (v x7 % pow2 40) * pow2 112 +
    v x7 / pow2 40 * pow2 168 + pow2 16 * (v x8 % pow2 40) * pow2 168 +
    v x8 / pow2 40 * pow2 224 + pow2 16 * (v x9 % pow2 40) * pow2 224;
  (==) { lemma_div264_x5 x5; lemma_div264_x6 x6 }
    v x4 / pow2 40 + v x5 * pow2 16 + v x6 * pow2 72 +
    pow2 16 * (v x7 % pow2 40) * pow2 112 +
    v x7 / pow2 40 * pow2 168 + pow2 16 * (v x8 % pow2 40) * pow2 168 +
    v x8 / pow2 40 * pow2 224 + pow2 16 * (v x9 % pow2 40) * pow2 224;
  (==) { lemma_div264_x7 x7; lemma_div264_x8 x8 }
    v x4 / pow2 40 + v x5 * pow2 16 + v x6 * pow2 72 + v x7 * pow2 128 + v x8 * pow2 184 +
    pow2 16 * (v x9 % pow2 40) * pow2 224;
  (==) { lemma_div264_x9 x9 }
    v x4 / pow2 40 + v x5 * pow2 16 + v x6 * pow2 72 + v x7 * pow2 128 + v x8 * pow2 184 + v x9 * pow2 240;
  (==) { lemma_div264_aux x }
    wide_as_nat5 x / pow2 264;
  }
#pop-options

#pop-options // "--z3cliopt smt.arith.nl=false"

val lemma_mod_264_aux: t:qelem_wide5 ->
  Lemma
  (requires
    qelem_wide_fits5 t (1, 1, 1, 1, 1, 1, 1, 1, 1, 1))
  (ensures
   (let (t0, t1, t2, t3, t4, t5, t6, t7, t8, t9) = t in
   (wide_as_nat5 t) % pow2 264 ==
   (v t0 + v t1 * pow2 56 + v t2 * pow2 112 + v t3 * pow2 168 + v t4 * pow2 224) % pow2 264))

#push-options "--z3rlimit 150"
let lemma_mod_264_aux t =
  let (t0, t1, t2, t3, t4, t5, t6, t7, t8, t9) = t in
  let res = (t0, t1, t2, t3, t4 &. u64 0xffffffffff) in

  assert_norm (pow2 16 * pow2 264 == pow2 280);
  assert_norm (pow2 72 * pow2 264 == pow2 336);
  assert_norm (pow2 128 * pow2 264 == pow2 392);
  assert_norm (pow2 184 * pow2 264 == pow2 448);
  assert_norm (pow2 240 * pow2 264 == pow2 504);

  calc (==) {
    (wide_as_nat5 t) % pow2 264;
  (==) { }
    (v t0 + v t1 * pow2 56 + v t2 * pow2 112 + v t3 * pow2 168 + v t4 * pow2 224 +
     (v t5 * pow2 16 + v t6 * pow2 72 + v t7 * pow2 128 + v t8 * pow2 184 + v t9 * pow2 240) * pow2 264) % pow2 264;
  (==) { FStar.Math.Lemmas.lemma_mod_add_distr (v t0 + v t1 * pow2 56 + v t2 * pow2 112 + v t3 * pow2 168 + v t4 * pow2 224)
    ((v t5 * pow2 16 + v t6 * pow2 72 + v t7 * pow2 128 + v t8 * pow2 184 + v t9 * pow2 240) * pow2 264) (pow2 264)}
    ((v t0 + v t1 * pow2 56 + v t2 * pow2 112 + v t3 * pow2 168 + v t4 * pow2 224) +
     ((v t5 * pow2 16 + v t6 * pow2 72 + v t7 * pow2 128 + v t8 * pow2 184 + v t9 * pow2 240) * pow2 264) % pow2 264) % pow2 264;
  (==) { FStar.Math.Lemmas.cancel_mul_mod (v t5 * pow2 16 + v t6 * pow2 72 + v t7 * pow2 128 + v t8 * pow2 184 + v t9 * pow2 240) (pow2 264) }
    (v t0 + v t1 * pow2 56 + v t2 * pow2 112 + v t3 * pow2 168 + v t4 * pow2 224) % pow2 264;
  }
#pop-options

val lemma_as_nat_pow264: x:qelem5 ->
  Lemma
  (requires
   (let (x0, x1, x2, x3, x4) = x in
    qelem_fits5 x (1, 1, 1, 1, 1) /\
    v x4 < pow2 40))
  (ensures as_nat5 x < pow2 264)

let lemma_as_nat_pow264 x =
  let (x0, x1, x2, x3, x4) = x in
  assert_norm (pow2 40 * pow2 224 = pow2 264)


val lemma_mod_264: t:qelem_wide5 ->
  Lemma
  (requires
    qelem_wide_fits5 t (1, 1, 1, 1, 1, 1, 1, 1, 1, 1))
  (ensures
   (let (t0, t1, t2, t3, t4, t5, t6, t7, t8, t9) = t in
    let res = (t0, t1, t2, t3, t4 &. u64 0xffffffffff) in
    qelem_fits5 res (1, 1, 1, 1, 1) /\
    as_nat5 res == (wide_as_nat5 t) % pow2 264))

let lemma_mod_264 t =
  let (t0, t1, t2, t3, t4, t5, t6, t7, t8, t9) = t in
  let t4' = t4 &. u64 0xffffffffff in
  let res = (t0, t1, t2, t3, t4') in
  assert_norm (pow2 40 < pow2 64);
  assert_norm (pow2 40 - 1 == 0xffffffffff);
  mod_mask_lemma t4 40ul;
  assert (v (mod_mask #U64 #SEC 40ul) == 0xffffffffff);
  assert (v (t4 &. u64 0xffffffffff) == v t4 % pow2 40);

  calc (==) {
    (wide_as_nat5 t) % pow2 264;
    (==) { lemma_mod_264_aux t }
    (v t0 + v t1 * pow2 56 + v t2 * pow2 112 + v t3 * pow2 168 + v t4 * pow2 224) % pow2 264;
    (==) { FStar.Math.Lemmas.lemma_mod_add_distr (v t0 + v t1 * pow2 56 + v t2 * pow2 112 + v t3 * pow2 168) (v t4 * pow2 224) (pow2 264) }
    (v t0 + v t1 * pow2 56 + v t2 * pow2 112 + v t3 * pow2 168 + (v t4 * pow2 224) % pow2 264) % pow2 264;
    (==) { FStar.Math.Lemmas.pow2_multiplication_modulo_lemma_2 (v t4) 264 224 }
    (v t0 + v t1 * pow2 56 + v t2 * pow2 112 + v t3 * pow2 168 + (v t4 % pow2 40) * pow2 224) % pow2 264;
    (==) { lemma_as_nat_pow264 res; FStar.Math.Lemmas.modulo_lemma (as_nat5 res) (pow2 264) }
    v t0 + v t1 * pow2 56 + v t2 * pow2 112 + v t3 * pow2 168 + (v t4 % pow2 40) * pow2 224;
    }


val lemma_as_nat_pow264_x4: x:qelem5 ->
  Lemma
  (requires
    qelem_fits5 x (1, 1, 1, 1, 1) /\
    as_nat5 x < pow2 264)
  (ensures
   (let (x0, x1, x2, x3, x4) = x in
    v x4 < pow2 40))

let lemma_as_nat_pow264_x4 x =
  let (x0, x1, x2, x3, x4) = x in
  assert_norm (pow2 40 * pow2 224 = pow2 264)


val lemma_sub_mod_264_aux:
    x0:nat -> x1:nat -> x2:nat -> x3:nat -> x4:nat
  -> y0:nat -> y1:nat -> y2:nat -> y3:nat -> y4:nat
  -> c1:nat -> c2:nat -> c3:nat -> c4:nat -> c5:nat ->
  Lemma (
    x0 - y0 + c1 * pow56 +
    (x1 - y1 - c1 + c2 * pow56) * pow56 +
    (x2 - y2 - c2 + c3 * pow56) * pow112 +
    (x3 - y3 - c3 + c4 * pow56) * pow168 +
    (x4 - y4 - c4 + pow2 40 * c5) * pow224 ==
    (x0 + x1 * pow2 56 + x2 * pow2 112 + x3 * pow2 168 + x4 * pow2 224) -
    (y0 + y1 * pow2 56 + y2 * pow2 112 + y3 * pow2 168 + y4 * pow2 224) + c5 * pow2 264)

#push-options "--z3rlimit 50"
let lemma_sub_mod_264_aux x0 x1 x2 x3 x4 y0 y1 y2 y3 y4 b0 b1 b2 b3 b4 =
  assert_norm (pow2 56 * pow2 56 = pow2 112);
  assert_norm (pow2 56 * pow2 112 = pow2 168);
  assert_norm (pow2 56 * pow2 168 = pow2 224);
  assert_norm (pow2 40 * pow2 224 = pow2 264)
#pop-options

val lemma_sub_mod_264:
    x:qelem5
  -> y:qelem5
  -> t:qelem5
  -> c5:uint64 ->
  Lemma
  (requires
    qelem_fits5 x (1, 1, 1, 1, 1) /\
    qelem_fits5 y (1, 1, 1, 1, 1) /\
    qelem_fits5 t (1, 1, 1, 1, 1) /\
    as_nat5 x < pow2 264 /\
    as_nat5 y < pow2 264 /\
    as_nat5 t == as_nat5 x - as_nat5 y + v c5 * pow2 264 /\ v c5 <= 1 /\
   (if v c5 = 0 then as_nat5 x >= as_nat5 y else as_nat5 x < as_nat5 y))
  (ensures
   (if as_nat5 x >= as_nat5 y then
     as_nat5 t == as_nat5 x - as_nat5 y
    else as_nat5 t == as_nat5 x - as_nat5 y + pow2 264))

#push-options "--z3rlimit 50"
let lemma_sub_mod_264 x y t c5 =
  assert (if v c5 = 0 then as_nat5 x >= as_nat5 y else as_nat5 x < as_nat5 y);
  assert (as_nat5 t == as_nat5 x - as_nat5 y + v c5 * pow2 264);
  if as_nat5 x >= as_nat5 y then
    assert (v c5 == 0 /\ as_nat5 t == as_nat5 x - as_nat5 y)
  else
    assert (v c5 == 1 /\ as_nat5 t == as_nat5 x - as_nat5 y + pow2 264)
#pop-options

let lemma_mul_qelem5 (x0 x1 x2 x3 x4 y0 y1 y2 y3 y4:nat) : Lemma
  ((x0 + x1 * pow2 56 + x2 * pow2 112 + x3 * pow2 168 + x4 * pow2 224) *
    (y0 + y1 * pow2 56 + y2 * pow2 112 + y3 * pow2 168 + y4 * pow2 224) ==
    x0 * y0 +
    (x0 * y1 + x1 * y0) * pow56 +
    (x0 * y2 + x1 * y1 + x2 * y0) * pow112 +
    (x0 * y3 + x1 * y2 + x2 * y1 + x3 * y0) * pow168 +
    (x0 * y4 + x1 * y3 + x2 * y2 + x3 * y1 + x4 * y0) * pow224 +
    (x1 * y4 + x2 * y3 + x3 * y2 + x4 * y1) * pow280 +
    (x2 * y4 + x3 * y3 + x4 * y2) * pow336 +
    (x3 * y4 + x4 * y3) * pow392 +
    (x4 * y4) * pow448)
  =
  assert ((x0 + x1 * pow2 56 + x2 * pow2 112 + x3 * pow2 168 + x4 * pow2 224) *
    (y0 + y1 * pow2 56 + y2 * pow2 112 + y3 * pow2 168 + y4 * pow2 224) ==
    x0 * y0 +
    (x0 * y1 + x1 * y0) * pow56 +
    (x0 * y2 + x1 * y1 + x2 * y0) * pow112 +
    (x0 * y3 + x1 * y2 + x2 * y1 + x3 * y0) * pow168 +
    (x0 * y4 + x1 * y3 + x2 * y2 + x3 * y1 + x4 * y0) * pow224 +
    (x1 * y4 + x2 * y3 + x3 * y2 + x4 * y1) * pow280 +
    (x2 * y4 + x3 * y3 + x4 * y2) * pow336 +
    (x3 * y4 + x4 * y3) * pow392 +
    (x4 * y4) * pow448)
  by (Tactics.norm [zeta; iota; delta; primops]; int_semiring ())

val lemma_mul_5_low_264:
  x1:nat -> x2:nat -> x3:nat -> x4:nat -> x5:nat ->
  y1:nat -> y2:nat -> y3:nat -> y4:nat -> y5:nat ->
  Lemma (
    (x1 * y1) >= 0
    /\ (x2 * y1 + x1 * y2 + ((x1 * y1) / pow2 56)) >= 0
    /\ (x3 * y1 + x2 * y2 + x1 * y3 + ((x2 * y1 + x1 * y2 + ((x1 * y1) / pow2 56)) / pow2 56)) >= 0
    /\ (x4 * y1 + x3 * y2 + x2 * y3 + x1 * y4 + ((x3 * y1 + x2 * y2 + x1 * y3 + ((x2 * y1 + x1 * y2 + ((x1 * y1) / pow2 56)) / pow2 56)) / pow2 56)) >= 0
    /\ (
    let a0 = (x1 * y1) % pow2 56 in
    let a1 = ((x2 * y1 + x1 * y2 + ((x1 * y1) / pow2 56)) % pow2 56) in
    let a2 = ((x3 * y1 + x2 * y2 + x1 * y3 + ((x2 * y1 + x1 * y2 + ((x1 * y1) / pow2 56)) / pow2 56)) % pow2 56) in
    let a3 = ((x4 * y1 + x3 * y2 + x2 * y3 + x1 * y4 + ((x3 * y1 + x2 * y2 + x1 * y3 + ((x2 * y1 + x1 * y2 + ((x1 * y1) / pow2 56)) / pow2 56)) / pow2 56)) % pow2 56) in
    let a4 = (x5 * y1 + x4 * y2 + x3 * y3 + x2 * y4 + x1 * y5 + ((x4 * y1 + x3 * y2 + x2 * y3 + x1 * y4 + ((x3 * y1 + x2 * y2 + x1 * y3 + ((x2 * y1 + x1 * y2 + ((x1 * y1) / pow2 56)) / pow2 56)) / pow2 56)) / pow2 56)) in
    ((x1 + pow2 56 * x2 + pow2 112 * x3 + pow2 168 * x4 + pow2 224 * x5)
         * (y1 + pow2 56 * y2 + pow2 112 * y3 + pow2 168 * y4 + pow2 224 * y5)) % pow2 264
    == a0 + pow2 56 * a1 + pow2 112 * a2 + pow2 168 * a3 + pow2 224 * (a4 % pow2 40)))

private let lemma_mul_nat_is_nat (a:nat) (b:nat) : Lemma (a*b >= 0) = ()

private let lemma_div_nat_is_nat (a:nat) (b:pos) : Lemma (a/b >= 0) = ()

private
val lemma_mul_5''':
  x1:nat -> x2:nat -> x3:nat -> x4:nat -> x5:nat ->
  y1:nat -> y2:nat -> y3:nat -> y4:nat -> y5:nat ->
  Lemma (((x1 + pow2 56 * x2 + pow2 112 * x3 + pow2 168 * x4 + pow2 224 * x5)
         * (y1 + pow2 56 * y2 + pow2 112 * y3 + pow2 168 * y4 + pow2 224 * y5)) % pow2 264
         ==
       (x1 * y1
       + pow2 56 * (x2 * y1 + x1 * y2)
       + pow2 112 * (x3 * y1 + x2 * y2 + x1 * y3)
       + pow2 168 * (x4 * y1 + x3 * y2 + x2 * y3 + x1 * y4)
       + pow2 224 * (x5 * y1 + x4 * y2 + x3 * y3 + x2 * y4 + x1 * y5)) % pow2 264)


let lemma_mul_5''' x1 x2 x3 x4 x5 y1 y2 y3 y4 y5 =

  calc (==) {
    ((x1 + pow2 56 * x2 + pow2 112 * x3 + pow2 168 * x4 + pow2 224 * x5)
    * (y1 + pow2 56 * y2 + pow2 112 * y3 + pow2 168 * y4 + pow2 224 * y5)) % pow2 264;
  (==) { _ by (Tactics.mapply (`feq #int #int (fun x -> x % pow2 264));
              Tactics.norm [zeta; iota; delta; primops];
              int_semiring ()) }
    (x1 * y1
    + pow2 56 * (x2 * y1 + x1 * y2)
    + pow2 112 * (x3 * y1 + x2 * y2 + x1 * y3)
    + pow2 168 * (x4 * y1 + x3 * y2 + x2 * y3 + x1 * y4)
    + pow2 224 * (x5 * y1 + x4 * y2 + x3 * y3 + x2 * y4 + x1 * y5)
    + (pow2 16 * x2 * y5 +
       pow2 16 * x3 * y4 + pow2 72 * x3 * y5 +
       pow2 16 * x4 * y3 + pow2 72 * x4 * y4 + pow2 128 * x4 * y5 +
       pow2 16 * x5 * y2 + pow2 72 * x5 * y3 + pow2 128 * x5 * y4 + pow2 184 * x5 * y5) * pow2 264) % pow2 264;
  (==) { _ by (Tactics.mapply (`eq_eq2); Tactics.mapply (`Math.Lemmas.lemma_mod_plus)) }
    (x1 * y1
    + pow2 56 * (x2 * y1 + x1 * y2)
    + pow2 112 * (x3 * y1 + x2 * y2 + x1 * y3)
    + pow2 168 * (x4 * y1 + x3 * y2 + x2 * y3 + x1 * y4)
    + pow2 224 * (x5 * y1 + x4 * y2 + x3 * y3 + x2 * y4 + x1 * y5)) % pow2 264;
  }


private val lemma_mod_264'':
  a0:nat -> a1:nat-> a2:nat -> a3:nat -> a4:nat ->
  Lemma
    (requires a0 < pow56 /\ a1 < pow56 /\ a2 < pow56 /\ a3 < pow56)
    (ensures a0 + pow2 56 * a1 + pow2 112 * a2 + pow2 168 * a3 + pow2 224 * (a4 % pow2 40) < pow2 264)
let lemma_mod_264'' a0 a1 a2 a3 a4 =
  assert_norm(pow2 40 = 0x10000000000);
  assert_norm(pow2 56 = 0x100000000000000);
  assert_norm(pow2 112 = 0x10000000000000000000000000000);
  assert_norm(pow2 168 = 0x1000000000000000000000000000000000000000000);
  assert_norm(pow2 224 = 0x100000000000000000000000000000000000000000000000000000000);
  assert_norm(pow2 264 = 0x1000000000000000000000000000000000000000000000000000000000000000000)


private val lemma_mod_264':
  a0:nat -> a1:nat-> a2:nat -> a3:nat -> a4:nat ->
  Lemma
    (requires a0 < pow56 /\ a1 < pow56 /\ a2 < pow56 /\ a3 < pow56)
    (ensures (a0
       + pow2 56 * a1
       + pow2 112 * a2
       + pow2 168 * a3
       + pow2 224 * a4) % pow2 264 =
       a0
       + pow2 56 * a1
       + pow2 112 * a2
       + pow2 168 * a3
       + pow2 224 * (a4 % pow2 40) )
let lemma_mod_264' a0 a1 a2 a3 a4 =
  assert_norm(pow2 56 = 0x100000000000000);
  assert_norm(pow2 112 = 0x10000000000000000000000000000);
  assert_norm(pow2 168 = 0x1000000000000000000000000000000000000000000);
  assert_norm(pow2 224 = 0x100000000000000000000000000000000000000000000000000000000);
  Math.Lemmas.lemma_mod_plus_distr_l (pow2 224 * a4) (a0 + pow2 56 * a1 + pow2 112 * a2 + pow2 168 * a3) (pow2 264);
  Math.Lemmas.pow2_multiplication_modulo_lemma_2 a4 264 224;
  lemma_mod_264'' a0 a1 a2 a3 a4;
  Math.Lemmas.modulo_lemma (a0 + pow2 56 * a1 + pow2 112 * a2 + pow2 168 * a3 + pow2 224 * (a4 % pow2 40)) (pow2 264)

private let lemma_aux_0 (a:nat) (b:nat) (n:nat) : Lemma
  (pow2 n * a + pow2 (n+56) * b = pow2 n * (a % pow2 56) + pow2 (n+56) * (b + a / pow2 56))
  = Math.Lemmas.lemma_div_mod a (pow2 56);
    Math.Lemmas.pow2_plus n 56;
    assert(a = pow2 56 * (a / pow2 56) + (a % pow2 56));
    Math.Lemmas.distributivity_add_right (pow2 n) (pow2 56 * (a / pow2 56)) (a % pow2 56);
    Math.Lemmas.paren_mul_right (pow2 n) (pow2 56) (a / pow2 56);
    Math.Lemmas.distributivity_add_right (pow2 (n+56)) b (a / pow2 56)

private
val lemma_mod_264_small:
  a0:nat -> a1:nat -> a2:nat -> a3:nat -> a4:nat ->
  Lemma ( (a0 + pow2 56 * a1 + pow2 112 * a2 + pow2 168 * a3 + pow2 224 * a4)
     = (a0 % pow2 56)
       + pow2 56 * ((a1 + (a0 / pow2 56)) % pow2 56)
       + pow2 112 * ((a2 + ((a1 + (a0 / pow2 56)) / pow2 56)) % pow2 56)
       + pow2 168 * ((a3 + ((a2 + ((a1 + (a0 / pow2 56)) / pow2 56)) / pow2 56)) % pow2 56)
       + pow2 224 * (a4 + ((a3 + ((a2 + ((a1 + (a0 / pow2 56)) / pow2 56)) / pow2 56)) / pow2 56)))

(* These silly lemmas needed to guide the proof below... *)
private let aux_nat_over_pos (p : nat) (q : pos) : Lemma (p / q >= 0) = ()
private let aux_nat_plus_nat (p : nat) (q : nat) : Lemma (p + q >= 0) = ()

let lemma_mod_264_small a0 a1 a2 a3 a4 =
  Math.Lemmas.lemma_div_mod a0 (pow2 56);
  Math.Lemmas.distributivity_add_right (pow2 56) a1 (a0 / pow2 56);
  (**) aux_nat_over_pos a0 (pow2 56);
  (**) aux_nat_plus_nat a1 (a0 / pow2 56);
  let a1':nat = (a1 + (a0 / pow2 56)) in
  (**) aux_nat_over_pos a1' (pow2 56);
  (**) aux_nat_plus_nat a2 (a1' / pow2 56);
  let a2':nat = (a2 + (a1' / pow2 56)) in
  (**) aux_nat_over_pos a2' (pow2 56);
  (**) aux_nat_plus_nat a3 (a2' / pow2 56);
  let a3':nat = (a3 + (a2' / pow2 56)) in
  lemma_aux_0 a1' a2 56;
  lemma_aux_0 a2' a3 112;
  lemma_aux_0 a3' a4 168

private
val lemma_mod_264_:
  a0:nat -> a1:nat -> a2:nat -> a3:nat -> a4:nat ->
  Lemma ((a0 + pow2 56 * a1 + pow2 112 * a2 + pow2 168 * a3 + pow2 224 * a4) % pow2 264 =
       (a0 % pow2 56)
       + pow2 56 * ((a1 + (a0 / pow2 56)) % pow2 56)
       + pow2 112 * ((a2 + ((a1 + (a0 / pow2 56)) / pow2 56)) % pow2 56)
       + pow2 168 * ((a3 + ((a2 + ((a1 + (a0 / pow2 56)) / pow2 56)) / pow2 56)) % pow2 56)
       + pow2 224 * ((a4 + ((a3 + ((a2 + ((a1 + (a0 / pow2 56)) / pow2 56)) / pow2 56)) / pow2 56)) % pow2 40))

let lemma_mod_264_ a0 a1 a2 a3 a4 =
  lemma_mod_264_small a0 a1 a2 a3 a4;
  let x0 = a0 % pow2 56 in
  assert (x0 < pow56);
  let x1 = ((a1 + (a0 / pow2 56)) % pow2 56) in
  assert (x1 < pow56);
  let x2 = ((a2 + ((a1 + (a0 / pow2 56)) / pow2 56)) % pow2 56) in
  assert (x2 < pow56);
  let x3 =  ((a3 + ((a2 + ((a1 + (a0 / pow2 56)) / pow2 56)) / pow2 56)) % pow2 56) in
  assert (x3 < pow56);
  lemma_mod_264' x0 x1 x2 x3 (a4 + ((a3 + ((a2 + ((a1 + (a0 / pow2 56)) / pow2 56)) / pow2 56)) / pow2 56))


#push-options "--z3rlimit 50"
let lemma_mul_5_low_264 x1 x2 x3 x4 x5 y1 y2 y3 y4 y5 =
  lemma_div_nat_is_nat (x1 * y1) (pow2 56);
  lemma_div_nat_is_nat (x2 * y1 + x1 * y2 + ((x1 * y1) / pow2 56)) (pow2 56);
  lemma_div_nat_is_nat (x3 * y1 + x2 * y2 + x1 * y3 + ((x2 * y1 + x1 * y2 + ((x1 * y1) / pow2 56)) / pow2 56)) (pow2 56);
  lemma_div_nat_is_nat (x4 * y1 + x3 * y2 + x2 * y3 + x1 * y4 + ((x3 * y1 + x2 * y2 + x1 * y3 + ((x2 * y1 + x1 * y2 + ((x1 * y1) / pow2 56)) / pow2 56)) / pow2 56)) (pow2 56);
  lemma_mul_5''' x1 x2 x3 x4 x5 y1 y2 y3 y4 y5;
  lemma_mod_264_ (x1 * y1) (x2 * y1 + x1 * y2) (x3 * y1 + x2 * y2 + x1 * y3) (x4 * y1 + x3 * y2 + x2 * y3 + x1 * y4) (x5 * y1 + x4 * y2 + x3 * y3 + x2 * y4 + x1 * y5)
#pop-options

private
val lemma_optimized_barrett_reduce:
  a:nat{a < pow2 512} ->
  Lemma (a - (((a / pow2 248) * (pow2 512 / S.q)) / pow2 264) * S.q < 2 * S.q
    /\ a - (((a / pow2 248) * (pow2 512 / S.q)) / pow2 264) * S.q >= 0)
#push-options "--z3rlimit 50"
let lemma_optimized_barrett_reduce a =
  assert_norm (pow2 248 = 0x100000000000000000000000000000000000000000000000000000000000000);
  assert_norm (pow2 264 = 0x1000000000000000000000000000000000000000000000000000000000000000000);
  assert_norm (S.q == 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed);
  assert_norm (0x100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 == pow2 512)
#pop-options

private
val lemma_optimized_barrett_reduce2:
  a:nat{a < pow2 512} ->
  Lemma (a - ((a * (pow2 512 / S.q)) / pow2 512) * S.q < pow2 264 /\
         a - ((a * (pow2 512 / S.q)) / pow2 512) * S.q >= 0)

#push-options "--z3rlimit 50"
val lemma_optimized_barrett_reduce2:
  a:nat{a < pow2 512} ->
  Lemma (a - ((a * (pow2 512 / S.q)) / pow2 512) * S.q < pow2 264 /\
         a - ((a * (pow2 512 / S.q)) / pow2 512) * S.q >= 0)
a: Prims.nat{a < Prims.pow2 512}
  -> FStar.Pervasives.Lemma
    (ensures
      a - (a * (Prims.pow2 512 / Spec.Ed25519.q) / Prims.pow2 512) * Spec.Ed25519.q < Prims.pow2 264 /\
      a - (a * (Prims.pow2 512 / Spec.Ed25519.q) / Prims.pow2 512) * Spec.Ed25519.q >= 0)