From 37ad2b3bb340a3ea912790e2c7a00dd8ff1bd008 Mon Sep 17 00:00:00 2001 From: victor felder Date: Sun, 26 Apr 2026 01:03:51 +0200 Subject: [PATCH 01/15] fix(crypto/math): handle negative input in mod_inverse --- lib/crypto/math.ex | 9 ++++++--- test/crypto/math_test.exs | 27 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 test/crypto/math_test.exs diff --git a/lib/crypto/math.ex b/lib/crypto/math.ex index e63f8c3..9219359 100644 --- a/lib/crypto/math.ex +++ b/lib/crypto/math.ex @@ -67,9 +67,12 @@ defmodule Tezex.Crypto.Math do Returns {:ok, inverse} or {:error, :not_invertible}. """ @spec mod_inverse(integer(), integer()) :: {:ok, integer()} | {:error, :not_invertible} - def mod_inverse(a, m) do - case extended_gcd(a, m) do - {1, x, _} -> {:ok, rem(x + m, m)} + def mod_inverse(a, m) when m > 0 do + # Normalize a to [0, m) so extended_gcd returns a positive gcd. + a_pos = Integer.mod(a, m) + + case extended_gcd(a_pos, m) do + {1, x, _} -> {:ok, Integer.mod(x, m)} _ -> {:error, :not_invertible} end end diff --git a/test/crypto/math_test.exs b/test/crypto/math_test.exs new file mode 100644 index 0000000..51745d4 --- /dev/null +++ b/test/crypto/math_test.exs @@ -0,0 +1,27 @@ +defmodule Tezex.Crypto.MathTest do + use ExUnit.Case, async: true + + alias Tezex.Crypto.Math + + describe "mod_inverse/2" do + test "returns the modular inverse for positive coprime input" do + # 5 * 3 = 15 = 1 (mod 7) + assert {:ok, 3} = Math.mod_inverse(5, 7) + end + + test "returns the modular inverse for negative coprime input" do + # -5 ≡ 2 (mod 7), and 2 * 4 = 8 = 1 (mod 7) + assert {:ok, 4} = Math.mod_inverse(-5, 7) + end + + test "returns :error for zero" do + assert {:error, :not_invertible} = Math.mod_inverse(0, 7) + end + + test "returns :error for non-coprime input" do + assert {:error, :not_invertible} = Math.mod_inverse(7, 7) + assert {:error, :not_invertible} = Math.mod_inverse(14, 7) + assert {:error, :not_invertible} = Math.mod_inverse(6, 9) + end + end +end From 0cf84e18a3be0f404277cc0466554208e8dface1 Mon Sep 17 00:00:00 2001 From: victor felder Date: Tue, 19 May 2026 15:59:54 +0200 Subject: [PATCH 02/15] fix(crypto/private_key): preserve real error in from_encoded_key --- lib/crypto/private_key.ex | 58 +++++++++++++++----------------- test/crypto/private_key_test.exs | 17 ++++++++++ 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/lib/crypto/private_key.ex b/lib/crypto/private_key.ex index a0cd6d5..df49bf0 100644 --- a/lib/crypto/private_key.ex +++ b/lib/crypto/private_key.ex @@ -222,45 +222,41 @@ defmodule Tezex.Crypto.PrivateKey do raise "invalid_key_length" end - # Decode from base58 and strip prefix - decoded_key = + # Decode from base58. decode58! raises FunctionClauseError on malformed + # base58; we translate only that to "invalid_base58". Other RuntimeErrors + # raised below ("invalid_length", "invalid_checksum", + # "unsupported_key_format") must propagate so callers see the real cause. + decoded_with_prefix_and_checksum = try do - # Use decode58! which includes checksum, then validate and remove it - decoded_with_prefix_and_checksum = Base58Check.decode58!(encoded_key_bytes) - - # Find the prefix and expected data length from the encoding table - prefix_info = find_encoding_info(encoded_key) - prefix_len = byte_size(prefix_info.d_prefix) - expected_data_len = prefix_info.d_len + Base58Check.decode58!(encoded_key_bytes) + rescue + FunctionClauseError -> raise "invalid_base58" + end - # Validate total length (prefix + data + 4-byte checksum) - expected_total_len = prefix_len + expected_data_len + 4 + prefix_info = find_encoding_info(encoded_key) + prefix_len = byte_size(prefix_info.d_prefix) + expected_data_len = prefix_info.d_len + expected_total_len = prefix_len + expected_data_len + 4 - if byte_size(decoded_with_prefix_and_checksum) != expected_total_len do - raise "invalid_length" - end + if byte_size(decoded_with_prefix_and_checksum) != expected_total_len do + raise "invalid_length" + end - # Extract prefix + data (without checksum) - prefix_and_data = - binary_part(decoded_with_prefix_and_checksum, 0, prefix_len + expected_data_len) + prefix_and_data = + binary_part(decoded_with_prefix_and_checksum, 0, prefix_len + expected_data_len) - checksum = - binary_part(decoded_with_prefix_and_checksum, prefix_len + expected_data_len, 4) + checksum = + binary_part(decoded_with_prefix_and_checksum, prefix_len + expected_data_len, 4) - # Validate checksum - computed_checksum = - :crypto.hash(:sha256, :crypto.hash(:sha256, prefix_and_data)) - |> binary_part(0, 4) + computed_checksum = + :crypto.hash(:sha256, :crypto.hash(:sha256, prefix_and_data)) + |> binary_part(0, 4) - if checksum != computed_checksum do - raise "invalid_checksum" - end + if checksum != computed_checksum do + raise "invalid_checksum" + end - # Strip the prefix to get just the key data - binary_part(prefix_and_data, prefix_len, expected_data_len) - rescue - _ -> raise "invalid_base58" - end + decoded_key = binary_part(prefix_and_data, prefix_len, expected_data_len) # Extract secret key bytes secret_key_bytes = diff --git a/test/crypto/private_key_test.exs b/test/crypto/private_key_test.exs index 0a45515..99f58ec 100644 --- a/test/crypto/private_key_test.exs +++ b/test/crypto/private_key_test.exs @@ -13,4 +13,21 @@ defmodule Tezex.Crypto.PrivateKeyTest do assert private_key1.secret == private_key2.secret assert private_key1.curve == private_key2.curve end + + describe "from_encoded_key/2 error reporting" do + test "returns :invalid_checksum for keys with corrupted checksum, not :invalid_base58" do + pk = PrivateKey.generate(nil, :secp256k1) + valid_spsk = Tezex.Crypto.Base58Check.encode(pk.secret, <<17, 162, 224, 201>>) + + assert {:ok, _} = PrivateKey.from_encoded_key(valid_spsk) + + # Flip a base58 char in the checksum suffix to corrupt the checksum + # without breaking base58 decodability. + last = String.last(valid_spsk) + replacement = if last == "1", do: "2", else: "1" + corrupted = String.slice(valid_spsk, 0..-2//1) <> replacement + + assert {:error, :invalid_checksum} = PrivateKey.from_encoded_key(corrupted) + end + end end From d2093b39da9543068de0f5d895611c961f9236e4 Mon Sep 17 00:00:00 2001 From: victor felder Date: Tue, 19 May 2026 16:00:25 +0200 Subject: [PATCH 03/15] style(crypto/private_key): drop unless and noop binary round-trip --- lib/crypto/private_key.ex | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/crypto/private_key.ex b/lib/crypto/private_key.ex index df49bf0..95c7ece 100644 --- a/lib/crypto/private_key.ex +++ b/lib/crypto/private_key.ex @@ -128,7 +128,7 @@ defmodule Tezex.Crypto.PrivateKey do - edesk... (Ed25519 encrypted seed) - spsk... (Secp256k1 secret key) - spesk... (Secp256k1 encrypted secret key) - - p2sk... (P256 secret key) + - p2sk... (P256 secret key) - p2esk... (P256 encrypted secret key) - BLsk... (BLS12-381 secret key) - BLesk... (BLS12-381 encrypted secret key) @@ -171,10 +171,8 @@ defmodule Tezex.Crypto.PrivateKey do """ @spec from_encoded_key!(String.t(), String.t() | nil) :: t() def from_encoded_key!(encoded_key, passphrase \\ nil) when is_binary(encoded_key) do - encoded_key_bytes = String.to_charlist(encoded_key) |> :erlang.list_to_binary() - # Parse key format - curve_prefix = binary_part(encoded_key_bytes, 0, 2) + curve_prefix = binary_part(encoded_key, 0, 2) curve = case curve_prefix do @@ -187,7 +185,7 @@ defmodule Tezex.Crypto.PrivateKey do # Check if encrypted encrypted? = - case binary_part(encoded_key_bytes, 2, 1) do + case binary_part(encoded_key, 2, 1) do "e" -> true _ -> false end @@ -195,12 +193,12 @@ defmodule Tezex.Crypto.PrivateKey do # Check if this is a secret key key_type = if encrypted? do - binary_part(encoded_key_bytes, 3, 2) + binary_part(encoded_key, 3, 2) else - binary_part(encoded_key_bytes, 2, 2) + binary_part(encoded_key, 2, 2) end - unless key_type == "sk" do + if key_type != "sk" do raise "not_secret_key" end @@ -218,7 +216,7 @@ defmodule Tezex.Crypto.PrivateKey do end end - unless byte_size(encoded_key_bytes) in [expected_length, 98] do + if byte_size(encoded_key) not in [expected_length, 98] do raise "invalid_key_length" end @@ -228,7 +226,7 @@ defmodule Tezex.Crypto.PrivateKey do # "unsupported_key_format") must propagate so callers see the real cause. decoded_with_prefix_and_checksum = try do - Base58Check.decode58!(encoded_key_bytes) + Base58Check.decode58!(encoded_key) rescue FunctionClauseError -> raise "invalid_base58" end @@ -261,7 +259,7 @@ defmodule Tezex.Crypto.PrivateKey do # Extract secret key bytes secret_key_bytes = if encrypted? do - unless passphrase do + if is_nil(passphrase) do raise "passphrase_required" end From 11514169a3cb86025e8ee03f4aa3663f97f17755 Mon Sep 17 00:00:00 2001 From: victor felder Date: Sun, 26 Apr 2026 09:52:06 +0200 Subject: [PATCH 04/15] fix(crypto/ecdsa): k upper-bound check The guard `not (k <= 1 or k >= ns1)` compared an integer k to a binary ns1 (:binary.encode_unsigned(N - 1)). So `k >= ns1` was always false. Since HMAC-DRBG output is truncated to N's bit length, it wasn't an issue in practice. --- lib/crypto/ecdsa.ex | 84 ++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/lib/crypto/ecdsa.ex b/lib/crypto/ecdsa.ex index c3bbc01..43cadbc 100644 --- a/lib/crypto/ecdsa.ex +++ b/lib/crypto/ecdsa.ex @@ -145,40 +145,44 @@ defmodule Tezex.Crypto.ECDSA do %{hashfunc: hashfunc} = Enum.into(options, %{hashfunc: fn msg -> :crypto.hash(:sha256, msg) end}) - number_message = - hashfunc.(message) - |> Utils.number_from_string() - curve_data = public_key.curve - - inv = Math.inv(signature.s, curve_data."N") - - v = - Math.add( - Math.multiply( - curve_data."G", - Utils.mod(number_message * inv, curve_data."N"), - curve_data."N", - curve_data."A", - curve_data."P" - ), - Math.multiply( - public_key.point, - Utils.mod(signature.r * inv, curve_data."N"), - curve_data."N", - curve_data."A", - curve_data."P" - ), - curve_data."A", - curve_data."P" - ) + n = curve_data."N" cond do - signature.r < 1 or signature.r >= curve_data."N" -> false - signature.s < 1 or signature.s >= curve_data."N" -> false - Point.is_at_infinity?(v) -> false - Utils.mod(v.x, curve_data."N") != signature.r -> false - true -> true + signature.r < 1 or signature.r >= n -> + false + + signature.s < 1 or signature.s >= n -> + false + + true -> + number_message = + hashfunc.(message) + |> Utils.number_from_string() + + inv = Math.inv(signature.s, n) + + v = + Math.add( + Math.multiply( + curve_data."G", + Utils.mod(number_message * inv, n), + n, + curve_data."A", + curve_data."P" + ), + Math.multiply( + public_key.point, + Utils.mod(signature.r * inv, n), + n, + curve_data."A", + curve_data."P" + ), + curve_data."A", + curve_data."P" + ) + + not Point.is_at_infinity?(v) and Utils.mod(v.x, n) == signature.r end end @@ -203,25 +207,25 @@ defmodule Tezex.Crypto.ECDSA do defp generate_signature(drbg, secret, message, curve_data) do {k, drbg} = HMACDRBG.generate(drbg, 32) - nh = curve_data."N" >>> 1 - ns1 = :binary.encode_unsigned(curve_data."N" - 1) + n = curve_data."N" + nh = n >>> 1 k = k |> :binary.decode_unsigned() - |> Utils.truncate_to_n(curve_data."N", true) + |> Utils.truncate_to_n(n, true) - with true <- not (k <= 1 or k >= ns1), - kp = Math.multiply(curve_data."G", k, curve_data."N", curve_data."A", curve_data."P"), + with true <- k > 1 and k < n - 1, + kp = Math.multiply(curve_data."G", k, n, curve_data."A", curve_data."P"), false <- Point.is_at_infinity?(kp), - r = rem(kp.x, curve_data."N"), + r = rem(kp.x, n), true <- r != 0, - s = Math.inv(k, curve_data."N") * (r * :binary.decode_unsigned(secret) + message), - s = rem(s, curve_data."N"), + s = Math.inv(k, n) * (r * :binary.decode_unsigned(secret) + message), + s = rem(s, n), true <- s != 0 do s = if s > nh do - curve_data."N" - s + n - s else s end From b5d1efe2fadd05baba933e4532d21349c41ecba2 Mon Sep 17 00:00:00 2001 From: victor felder Date: Tue, 19 May 2026 16:01:17 +0200 Subject: [PATCH 05/15] fix(crypto/bls): handle negative input in from_integer --- lib/crypto/bls/fq.ex | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/lib/crypto/bls/fq.ex b/lib/crypto/bls/fq.ex index e494815..1b06fc8 100644 --- a/lib/crypto/bls/fq.ex +++ b/lib/crypto/bls/fq.ex @@ -28,15 +28,7 @@ defmodule Tezex.Crypto.BLS.Fq do """ @spec from_integer(integer()) :: t() def from_integer(n) when is_integer(n) do - # Handle negative integers by converting to positive modular equivalent - reduced = - if n >= 0 do - rem(n, @modulus) - else - # For negative n, compute n mod p as (p - ((-n) mod p)) mod p - @modulus - rem(-n, @modulus) - end - + reduced = Integer.mod(n, @modulus) <> end From db3c95bb169c16bc0a15752c4f18bc1850f1cec9 Mon Sep 17 00:00:00 2001 From: victor felder Date: Tue, 19 May 2026 16:02:05 +0200 Subject: [PATCH 06/15] fix(crypto/bls): improve sqrt zero case --- lib/crypto/bls/fq.ex | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/crypto/bls/fq.ex b/lib/crypto/bls/fq.ex index 1b06fc8..dd9c9a4 100644 --- a/lib/crypto/bls/fq.ex +++ b/lib/crypto/bls/fq.ex @@ -182,20 +182,19 @@ defmodule Tezex.Crypto.BLS.Fq do @doc """ Computes the square root of a field element if it exists. """ + @sqrt_exp div(@modulus + 1, 4) + @spec sqrt(t()) :: {:ok, t()} | {:error, :no_sqrt} + def sqrt(@zero), do: {:ok, @zero} + def sqrt(a) when byte_size(a) == 48 do - # Use Tonelli-Shanks algorithm for square root - # Since q ≡ 3 (mod 4), we can use a^((q+1)/4) - with false <- is_zero?(a) and :is_zero, - 3 <- rem(@modulus, 4), - exp = div(@modulus + 1, 4), - candidate = pow(a, exp), - # Verify it's actually a square root - true <- eq?(square(candidate), a) do + # q ≡ 3 (mod 4), so √a = a^((q+1)/4) when a is a quadratic residue. + candidate = pow(a, @sqrt_exp) + + if eq?(square(candidate), a) do {:ok, candidate} else - :is_zero -> {:ok, @zero} - _ -> {:error, :no_sqrt} + {:error, :no_sqrt} end end From a8fbbf2f011c719a618aa6c0c0f11b15c0ff9011 Mon Sep 17 00:00:00 2001 From: victor felder Date: Tue, 19 May 2026 16:03:49 +0200 Subject: [PATCH 07/15] fix(crypto/bls): use from_integer reduction in from_seed --- lib/crypto/bls.ex | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/crypto/bls.ex b/lib/crypto/bls.ex index 06a6741..7042cc2 100644 --- a/lib/crypto/bls.ex +++ b/lib/crypto/bls.ex @@ -66,12 +66,13 @@ defmodule Tezex.Crypto.BLS do 32 """ @spec from_seed(binary()) :: {:ok, t()} | {:error, :invalid_seed} - def from_seed(seed) when byte_size(seed) == @secret_key_size do - with {:ok, fr_element} <- Fr.from_bytes(seed), - false <- Fr.is_zero?(fr_element) do - {:ok, %__MODULE__{secret_key: fr_element}} + def from_seed(<>) do + fr_element = Fr.from_integer(seed_int) + + if Fr.is_zero?(fr_element) do + {:error, :invalid_seed} else - _ -> {:error, :invalid_seed} + {:ok, %__MODULE__{secret_key: fr_element}} end end From 42aee0f8dba40900ad532cfeb591ce5d686eea49 Mon Sep 17 00:00:00 2001 From: victor felder Date: Tue, 19 May 2026 16:04:35 +0200 Subject: [PATCH 08/15] fix(crypto/bls): validate scalars in from_bytes --- lib/crypto/bls/fr.ex | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/crypto/bls/fr.ex b/lib/crypto/bls/fr.ex index acb8ff2..c602fca 100644 --- a/lib/crypto/bls/fr.ex +++ b/lib/crypto/bls/fr.ex @@ -24,19 +24,14 @@ defmodule Tezex.Crypto.BLS.Fr do @doc """ Creates a field element from a binary (32 bytes, big-endian). + Rejects values outside `[0, @order)`. """ - @spec from_bytes(binary()) :: {:ok, t()} | {:error, :invalid_size} - def from_bytes(bytes) when byte_size(bytes) == 32 do - <> = bytes - - if value < @order do - {:ok, bytes} - else - # Reduce if needed - {:ok, from_integer(value)} - end + @spec from_bytes(binary()) :: {:ok, t()} | {:error, :invalid_size | :out_of_range} + def from_bytes(<> = bytes) when value < @order do + {:ok, bytes} end + def from_bytes(bytes) when byte_size(bytes) == 32, do: {:error, :out_of_range} def from_bytes(_), do: {:error, :invalid_size} @doc """ From b8ce2f2f4876c4181db03665dbc01d8cd4a64e1b Mon Sep 17 00:00:00 2001 From: victor felder Date: Tue, 19 May 2026 16:05:16 +0200 Subject: [PATCH 09/15] perf(crypto/bls): drop redundant eq?/2 check in add --- lib/crypto/bls/g2.ex | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/crypto/bls/g2.ex b/lib/crypto/bls/g2.ex index 6a2aca5..f828f95 100644 --- a/lib/crypto/bls/g2.ex +++ b/lib/crypto/bls/g2.ex @@ -266,14 +266,13 @@ defmodule Tezex.Crypto.BLS.G2 do cond do is_zero?(p1) -> p2 is_zero?(p2) -> p1 - eq?(p1, p2) -> double(p1) - true -> add_different(p1, p2) + true -> add_nonzero(p1, p2) end end - # Helper for adding two different points - # Optimized addition algorithm - defp add_different(p1, p2) do + # Add two non-zero points. Handles the same-point and inverse cases inline + # so we don't pay for an upfront eq?/2 (which redoes the same V1/V2/U1/U2 muls). + defp add_nonzero(p1, p2) do %{x: x1, y: y1, z: z1} = p1 %{x: x2, y: y2, z: z2} = p2 From 58a403c48d4b9b152f9e578021fe08c96948c42c Mon Sep 17 00:00:00 2001 From: victor felder Date: Tue, 19 May 2026 16:06:08 +0200 Subject: [PATCH 10/15] docs(crypto/bls): G1.new and G2 moduledoc use projective coordinates --- lib/crypto/bls/g1.ex | 2 +- lib/crypto/bls/g2.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/crypto/bls/g1.ex b/lib/crypto/bls/g1.ex index 1f79f42..f74ebdc 100644 --- a/lib/crypto/bls/g1.ex +++ b/lib/crypto/bls/g1.ex @@ -36,7 +36,7 @@ defmodule Tezex.Crypto.BLS.G1 do @i_8 Fq.from_integer(8) @doc """ - Creates a G1 point from Jacobian coordinates. + Creates a G1 point from projective coordinates (X, Y, Z). """ @spec new(Fq.t(), Fq.t(), Fq.t()) :: t() def new(x, y, z) do diff --git a/lib/crypto/bls/g2.ex b/lib/crypto/bls/g2.ex index f828f95..674d14d 100644 --- a/lib/crypto/bls/g2.ex +++ b/lib/crypto/bls/g2.ex @@ -5,8 +5,8 @@ defmodule Tezex.Crypto.BLS.G2 do This is the elliptic curve E'(Fq2): y² = x³ + 4(1 + u) over the quadratic extension Fq2. G2 is used for signatures in BLS signatures. - Points are represented in Jacobian coordinates (X, Y, Z) where: - - Affine coordinates: (X/Z², Y/Z³) over Fq2 + Points are represented in projective coordinates (X, Y, Z) where: + - Affine coordinates: (X/Z, Y/Z) over Fq2 - Point at infinity: (1, 1, 0) """ From 0c5120a3823fc6e629a5ce6634f2f1c1516e25a7 Mon Sep 17 00:00:00 2001 From: victor felder Date: Tue, 19 May 2026 16:07:47 +0200 Subject: [PATCH 11/15] refactor(crypto/bls): Fq12.inv and FqP.inv return tuples --- lib/crypto/bls/fq12.ex | 9 +++++---- lib/crypto/bls/fqp.ex | 10 +++++----- lib/crypto/bls/pairing.ex | 31 ++++++++++++------------------- test/crypto/bls/fq12_test.exs | 2 +- 4 files changed, 23 insertions(+), 29 deletions(-) diff --git a/lib/crypto/bls/fq12.ex b/lib/crypto/bls/fq12.ex index fdb063c..454ed73 100644 --- a/lib/crypto/bls/fq12.ex +++ b/lib/crypto/bls/fq12.ex @@ -86,19 +86,20 @@ defmodule Tezex.Crypto.BLS.Fq12 do @doc """ Computes the modular inverse of an Fq12 element. - Uses the FqP inverse implementation. + Returns `{:ok, inverse}` or `{:error, :not_invertible}` if the element is zero. """ - @spec inv(t()) :: t() + @spec inv(t()) :: {:ok, t()} | {:error, :not_invertible} def inv(a) do FqP.inv(a) end @doc """ - Divides two Fq12 elements (a / b = a * b^(-1)). + Divides two Fq12 elements (a / b = a * b^(-1)). Raises if `b` is zero. """ @spec field_div(t(), t()) :: t() def field_div(a, b) do - mul(a, inv(b)) + {:ok, b_inv} = inv(b) + mul(a, b_inv) end @doc """ diff --git a/lib/crypto/bls/fqp.ex b/lib/crypto/bls/fqp.ex index e57653a..10e6aa8 100644 --- a/lib/crypto/bls/fqp.ex +++ b/lib/crypto/bls/fqp.ex @@ -176,15 +176,15 @@ defmodule Tezex.Crypto.BLS.FqP do @doc """ Computes the modular inverse using the extended Euclidean algorithm. - Polynomial inversion using extended Euclidean algorithm. + Returns `{:ok, inverse}` or `{:error, :not_invertible}` if the element is zero. """ - @spec inv(t()) :: t() + @spec inv(t()) :: {:ok, t()} | {:error, :not_invertible} def inv(%{coeffs: coeffs, modulus_coeffs: modulus_coeffs} = elem) do if is_zero?(elem) do - raise ArithmeticError, "Cannot invert zero element" + {:error, :not_invertible} + else + {:ok, polynomial_inv(coeffs, modulus_coeffs)} end - - polynomial_inv(coeffs, modulus_coeffs) end # Helper functions diff --git a/lib/crypto/bls/pairing.ex b/lib/crypto/bls/pairing.ex index 256cc77..06cb7ed 100644 --- a/lib/crypto/bls/pairing.ex +++ b/lib/crypto/bls/pairing.ex @@ -281,7 +281,7 @@ defmodule Tezex.Crypto.BLS.Pairing do @doc """ Inverts a GT element. """ - @spec gt_inv(gt()) :: gt() + @spec gt_inv(gt()) :: {:ok, gt()} | {:error, :not_invertible} def gt_inv(gt_element) do Fq12.inv(gt_element) end @@ -321,24 +321,17 @@ defmodule Tezex.Crypto.BLS.Pairing do if not valid_points do false else - try do - # Compute the two pairings for BLS verification - # e1 = e(signature, G1_generator) - e1 = pairing(g1_generator, signature_point, false) - - # e2 = e(H(msg), pubkey) - e2 = pairing(pubkey_point, h_msg_point, false) - - # Final exponentiate the product: (e1 * e2^(-1)) - # This is equivalent to checking e1 == e2 - e2_inv = Fq12.inv(e2) - product = Fq12.mul(e1, e2_inv) - final_result = final_exponentiation(product) - - # Check if the result is 1 (identity in GT) - Fq12.is_one?(final_result) - rescue - _ -> false + e1 = pairing(g1_generator, signature_point, false) + e2 = pairing(pubkey_point, h_msg_point, false) + + # Check e1 == e2 by final-exponentiating e1 * e2^(-1) and testing for identity. + case Fq12.inv(e2) do + {:ok, e2_inv} -> + product = Fq12.mul(e1, e2_inv) + Fq12.is_one?(final_exponentiation(product)) + + {:error, _} -> + false end end end diff --git a/test/crypto/bls/fq12_test.exs b/test/crypto/bls/fq12_test.exs index 662d2fd..36cb485 100644 --- a/test/crypto/bls/fq12_test.exs +++ b/test/crypto/bls/fq12_test.exs @@ -460,7 +460,7 @@ defmodule Tezex.Crypto.BLS.Fq12Test do five = Fq12.new([Fq.from_integer(5) | List.duplicate(Fq.zero(), 11)], @fq12_modulus_coeffs) # Compute inverse - five_inv = Fq12.inv(five) + {:ok, five_inv} = Fq12.inv(five) # Multiply should give 1 result = Fq12.mul(five, five_inv) From e6d95b25c93ae280f2c2cf2670b136b82383bbfd Mon Sep 17 00:00:00 2001 From: victor felder Date: Tue, 19 May 2026 16:09:30 +0200 Subject: [PATCH 12/15] refactor(crypto/bls): pre-compute Miller loop bits, extract miller_step --- lib/crypto/bls/constants.ex | 21 ++++----- lib/crypto/bls/pairing.ex | 89 +++++++++++++------------------------ 2 files changed, 42 insertions(+), 68 deletions(-) diff --git a/lib/crypto/bls/constants.ex b/lib/crypto/bls/constants.ex index f80afe9..a507df7 100644 --- a/lib/crypto/bls/constants.ex +++ b/lib/crypto/bls/constants.ex @@ -10,17 +10,18 @@ defmodule Tezex.Crypto.BLS.Constants do # Curve order (number of points on the curve) @curve_order 52_435_875_175_126_190_479_447_740_508_185_965_837_690_552_500_527_637_822_603_658_699_938_581_184_513 - # Pseudo-binary encoding of the ATE loop count for efficient Miller loop - # This is the binary representation (LSB first) for the Miller algorithm - # Binary: 1101001000000001000000000000000000000000000000010000000000000000 - # Reversed for Miller loop (LSB first): - @pseudo_binary_encoding for char <- - String.graphemes( - "0000000000000000100000000000000000000000000000001000000001001011" - ), - do: String.to_integer(char) + # Pseudo-binary encoding of the ATE loop count for the BLS12-381 Miller loop. + # Source string is LSB-first (64 bits); the Miller loop iterates bits 62..0, + # so we precompute that iteration order once at compile time. + @miller_loop_bits "0000000000000000100000000000000000000000000000001000000001001011" + |> String.graphemes() + |> Enum.map(&String.to_integer/1) + |> Enum.slice(0..62) + |> Enum.reverse() + + 63 = length(@miller_loop_bits) def field_modulus, do: @field_modulus def curve_order, do: @curve_order - def pseudo_binary_encoding, do: @pseudo_binary_encoding + def miller_loop_bits, do: @miller_loop_bits end diff --git a/lib/crypto/bls/pairing.ex b/lib/crypto/bls/pairing.ex index 06cb7ed..4cfb5e5 100644 --- a/lib/crypto/bls/pairing.ex +++ b/lib/crypto/bls/pairing.ex @@ -51,7 +51,7 @@ defmodule Tezex.Crypto.BLS.Pairing do end # Compute Miller loop - result = miller_loop(g2_point, g1_point, final_exponentiate) + result = miller_loop(g2_point, g1_point) if final_exponentiate do final_exponentiation(result) @@ -67,66 +67,39 @@ defmodule Tezex.Crypto.BLS.Pairing do Core algorithm for optimal ATE pairing computation. """ - @spec miller_loop(G2.t(), G1.t(), boolean()) :: gt() - def miller_loop(q_point, p_point, _final_exponentiate) do - # Cast P to Fq12 for line function evaluations + @spec miller_loop(G2.t(), G1.t()) :: gt() + def miller_loop(q_point, p_point) do cast_p = cast_point_to_fq12(p_point) - - # Initialize Miller loop variables - # twist_R = twist_Q = twist(Q) twist_q = apply_twist(q_point) - r_point = q_point - f_num = Fq12.one() - f_den = Fq12.one() - - # Main Miller loop - # for v in pseudo_binary_encoding[62::-1]: - Constants.pseudo_binary_encoding() - # Take first 63 elements (0..62) - |> Enum.take(63) - # Reverse to match [62::-1] - |> Enum.reverse() - |> Enum.reduce({r_point, f_num, f_den}, fn bit, {r, f_n, f_d} -> - # Apply twist to current R for line function - twist_r = apply_twist(r) - - # _n, _d = linefunc(twist_R, twist_R, cast_P) - {line_num, line_den} = line_function_fq12(twist_r, twist_r, cast_p) - - # f_num = f_num * f_num * _n - # f_den = f_den * f_den * _d - new_f_n = Fq12.mul(Fq12.mul(f_n, f_n), line_num) - new_f_d = Fq12.mul(Fq12.mul(f_d, f_d), line_den) - - # R = double(R) - doubled_r = G2.double(r) - - # twist_R = twist(R) - doubled_twist_r = apply_twist(doubled_r) - - # Add step if bit is 1 - if bit == 1 do - # _n, _d = linefunc(twist_R, twist_Q, cast_P) - # Note: using doubled_twist_r which is twist(doubled_R) - {add_line_num, add_line_den} = line_function_fq12(doubled_twist_r, twist_q, cast_p) - # f_num = f_num * _n - # f_den = f_den * _d - final_f_n = Fq12.mul(new_f_n, add_line_num) - final_f_d = Fq12.mul(new_f_d, add_line_den) + initial = {q_point, Fq12.one(), Fq12.one()} - # R = add(R, Q) - added_r = G2.add(doubled_r, q_point) + {_r, f_n, f_d} = + Enum.reduce(Constants.miller_loop_bits(), initial, fn bit, {r, f_n, f_d} -> + miller_step(bit, r, f_n, f_d, q_point, twist_q, cast_p) + end) - {added_r, final_f_n, final_f_d} - else - {doubled_r, new_f_n, new_f_d} - end - end) - |> then(fn {_final_r, final_f_n, final_f_d} -> - # f = f_num / f_den - Fq12.field_div(final_f_n, final_f_d) - end) + Fq12.field_div(f_n, f_d) + end + + defp miller_step(bit, r, f_n, f_d, q_point, twist_q, cast_p) do + twist_r = apply_twist(r) + {line_num, line_den} = line_function_fq12(twist_r, twist_r, cast_p) + + f_n = Fq12.mul(Fq12.mul(f_n, f_n), line_num) + f_d = Fq12.mul(Fq12.mul(f_d, f_d), line_den) + + doubled_r = G2.double(r) + + case bit do + 1 -> + doubled_twist_r = apply_twist(doubled_r) + {add_line_num, add_line_den} = line_function_fq12(doubled_twist_r, twist_q, cast_p) + {G2.add(doubled_r, q_point), Fq12.mul(f_n, add_line_num), Fq12.mul(f_d, add_line_den)} + + _ -> + {doubled_r, f_n, f_d} + end end @doc """ @@ -220,8 +193,8 @@ defmodule Tezex.Crypto.BLS.Pairing do end defp embed_fq12_from_integer(n) do - fq_element = Fq.from_integer(n) - embed_fq_to_fq12(fq_element) + Fq.from_integer(n) + |> embed_fq_to_fq12() end @doc """ From 6491ad59ea32aae8d5ff8cd5e1fc3ba42f187c28 Mon Sep 17 00:00:00 2001 From: victor felder Date: Tue, 19 May 2026 16:11:00 +0200 Subject: [PATCH 13/15] style(crypto/bls): drop is_ prefix from predicates --- lib/crypto/bls.ex | 16 +++++------ lib/crypto/bls/fq.ex | 12 ++++---- lib/crypto/bls/fq12.ex | 10 +++---- lib/crypto/bls/fq2.ex | 16 +++++------ lib/crypto/bls/fqp.ex | 12 ++++---- lib/crypto/bls/fr.ex | 4 +-- lib/crypto/bls/g1.ex | 30 ++++++++++---------- lib/crypto/bls/g2.ex | 42 ++++++++++++++-------------- lib/crypto/bls/pairing.ex | 48 +++++++++++++++----------------- test/crypto/bls/fq12_test.exs | 34 +++++++++++----------- test/crypto/bls/g1_test.exs | 10 +++---- test/crypto/bls/g2_test.exs | 14 +++++----- test/crypto/bls/pairing_test.exs | 4 +-- 13 files changed, 125 insertions(+), 127 deletions(-) diff --git a/lib/crypto/bls.ex b/lib/crypto/bls.ex index 7042cc2..f30c72b 100644 --- a/lib/crypto/bls.ex +++ b/lib/crypto/bls.ex @@ -69,7 +69,7 @@ defmodule Tezex.Crypto.BLS do def from_seed(<>) do fr_element = Fr.from_integer(seed_int) - if Fr.is_zero?(fr_element) do + if Fr.zero?(fr_element) do {:error, :invalid_seed} else {:ok, %__MODULE__{secret_key: fr_element}} @@ -122,7 +122,7 @@ defmodule Tezex.Crypto.BLS do def from_secret_exponent(secret) when is_integer(secret) and secret > 0 do fr_element = Fr.from_integer(secret) - if Fr.is_zero?(fr_element) do + if Fr.zero?(fr_element) do {:error, :invalid_secret} else {:ok, %__MODULE__{secret_key: fr_element}} @@ -263,7 +263,7 @@ defmodule Tezex.Crypto.BLS do else # Non-infinity point: try to decompress and validate case G2.from_compressed_bytes(signature) do - {:ok, point} -> G2.is_on_curve?(point) and not G2.is_infinity?(point) + {:ok, point} -> G2.is_on_curve?(point) and not G2.infinity?(point) {:error, _} -> false end end @@ -295,7 +295,7 @@ defmodule Tezex.Crypto.BLS do else # Try to decompress and validate the point case G1.from_compressed_bytes(pubkey) do - {:ok, point} -> G1.is_on_curve?(point) and not G1.is_infinity?(point) + {:ok, point} -> G1.is_on_curve?(point) and not G1.infinity?(point) {:error, _} -> false end end @@ -312,8 +312,8 @@ defmodule Tezex.Crypto.BLS do with {:ok, pubkey_point} <- G1.from_compressed_bytes(public_key), {:ok, signature_point} <- G2.from_compressed_bytes(signature), - false <- G1.is_infinity?(pubkey_point), - false <- G2.is_infinity?(signature_point) do + false <- G1.infinity?(pubkey_point), + false <- G2.infinity?(signature_point) do # Hash the message to G2 message_point = G2.hash_to_curve(message, ciphersuite) @@ -458,7 +458,7 @@ defmodule Tezex.Crypto.BLS do secret_key_int = derive_key_with_hkdf(ikm, salt, key_info, 0) fr_element = Fr.from_integer(secret_key_int) - if Fr.is_zero?(fr_element) do + if Fr.zero?(fr_element) do derive_key_retry(ikm, salt, key_info, 1) else {:ok, fr_element} @@ -471,7 +471,7 @@ defmodule Tezex.Crypto.BLS do secret_key_int = derive_key_with_hkdf(ikm, salt, key_info, iteration) fr_element = Fr.from_integer(secret_key_int) - if Fr.is_zero?(fr_element) do + if Fr.zero?(fr_element) do derive_key_retry(ikm, base_salt, key_info, iteration + 1) else {:ok, fr_element} diff --git a/lib/crypto/bls/fq.ex b/lib/crypto/bls/fq.ex index dd9c9a4..73a8cac 100644 --- a/lib/crypto/bls/fq.ex +++ b/lib/crypto/bls/fq.ex @@ -78,16 +78,16 @@ defmodule Tezex.Crypto.BLS.Fq do @doc """ Checks if a field element is zero. """ - @spec is_zero?(t()) :: boolean() - def is_zero?(fq) do + @spec zero?(t()) :: boolean() + def zero?(fq) do fq == @zero end @doc """ Checks if a field element is one. """ - @spec is_one?(t()) :: boolean() - def is_one?(fq) do + @spec one?(t()) :: boolean() + def one?(fq) do fq == @one end @@ -129,7 +129,7 @@ defmodule Tezex.Crypto.BLS.Fq do """ @spec neg(t()) :: t() def neg(a) when byte_size(a) == 48 do - if is_zero?(a) do + if zero?(a) do @zero else a_int = to_integer(a) @@ -152,7 +152,7 @@ defmodule Tezex.Crypto.BLS.Fq do """ @spec inv(t()) :: {:ok, t()} | {:error, :not_invertible} def inv(a) when byte_size(a) == 48 do - with false <- is_zero?(a), + with false <- zero?(a), a_int = to_integer(a), {:ok, inv_int} <- Math.mod_inverse(a_int, @modulus) do {:ok, from_integer(inv_int)} diff --git a/lib/crypto/bls/fq12.ex b/lib/crypto/bls/fq12.ex index 454ed73..2af309e 100644 --- a/lib/crypto/bls/fq12.ex +++ b/lib/crypto/bls/fq12.ex @@ -113,16 +113,16 @@ defmodule Tezex.Crypto.BLS.Fq12 do @doc """ Checks if an Fq12 element is zero. """ - @spec is_zero?(t()) :: boolean() - def is_zero?(a) do - FqP.is_zero?(a) + @spec zero?(t()) :: boolean() + def zero?(a) do + FqP.zero?(a) end @doc """ Checks if an Fq12 element is one. """ - @spec is_one?(t()) :: boolean() - def is_one?(a) do + @spec one?(t()) :: boolean() + def one?(a) do FqP.eq?(a, one()) end diff --git a/lib/crypto/bls/fq2.ex b/lib/crypto/bls/fq2.ex index e0f0453..5a98f5a 100644 --- a/lib/crypto/bls/fq2.ex +++ b/lib/crypto/bls/fq2.ex @@ -42,17 +42,17 @@ defmodule Tezex.Crypto.BLS.Fq2 do @doc """ Checks if an Fq2 element is zero. """ - @spec is_zero?(t()) :: boolean() - def is_zero?({a, b}) do - Fq.is_zero?(a) and Fq.is_zero?(b) + @spec zero?(t()) :: boolean() + def zero?({a, b}) do + Fq.zero?(a) and Fq.zero?(b) end @doc """ Checks if an Fq2 element is one. """ - @spec is_one?(t()) :: boolean() - def is_one?({a, b}) do - Fq.is_one?(a) and Fq.is_zero?(b) + @spec one?(t()) :: boolean() + def one?({a, b}) do + Fq.one?(a) and Fq.zero?(b) end @doc """ @@ -141,7 +141,7 @@ defmodule Tezex.Crypto.BLS.Fq2 do """ @spec inv(t()) :: {:ok, t()} | {:error, :not_invertible} def inv(element) do - if is_zero?(element) do + if zero?(element) do {:error, :not_invertible} else case Fq.inv(norm(element)) do @@ -215,7 +215,7 @@ defmodule Tezex.Crypto.BLS.Fq2 do @spec sqrt(t()) :: {:ok, t()} | {:error, :no_sqrt} def sqrt(element) do cond do - is_zero?(element) -> + zero?(element) -> {:ok, zero()} eq?(element, one()) -> diff --git a/lib/crypto/bls/fqp.ex b/lib/crypto/bls/fqp.ex index 10e6aa8..968d100 100644 --- a/lib/crypto/bls/fqp.ex +++ b/lib/crypto/bls/fqp.ex @@ -96,7 +96,7 @@ defmodule Tezex.Crypto.BLS.FqP do top_pos = degree + exp top = Map.get(acc_map, top_pos, @zero) - if Fq.is_zero?(top) do + if Fq.zero?(top) do acc_map else # Remove the high-degree term @@ -157,9 +157,9 @@ defmodule Tezex.Crypto.BLS.FqP do @doc """ Checks if the element is zero. """ - @spec is_zero?(t()) :: boolean() - def is_zero?(%{coeffs: coeffs}) do - Enum.all?(coeffs, &Fq.is_zero?/1) + @spec zero?(t()) :: boolean() + def zero?(%{coeffs: coeffs}) do + Enum.all?(coeffs, &Fq.zero?/1) end @doc """ @@ -180,7 +180,7 @@ defmodule Tezex.Crypto.BLS.FqP do """ @spec inv(t()) :: {:ok, t()} | {:error, :not_invertible} def inv(%{coeffs: coeffs, modulus_coeffs: modulus_coeffs} = elem) do - if is_zero?(elem) do + if zero?(elem) do {:error, :not_invertible} else {:ok, polynomial_inv(coeffs, modulus_coeffs)} @@ -276,7 +276,7 @@ defmodule Tezex.Crypto.BLS.FqP do defp deg_helper(_p, 0), do: 0 defp deg_helper(p, d) do - if Fq.is_zero?(Enum.at(p, d)) and d > 0 do + if Fq.zero?(Enum.at(p, d)) and d > 0 do deg_helper(p, d - 1) else d diff --git a/lib/crypto/bls/fr.ex b/lib/crypto/bls/fr.ex index c602fca..c44610d 100644 --- a/lib/crypto/bls/fr.ex +++ b/lib/crypto/bls/fr.ex @@ -70,8 +70,8 @@ defmodule Tezex.Crypto.BLS.Fr do @doc """ Checks if a field element is zero. """ - @spec is_zero?(t()) :: boolean() - def is_zero?(fr) do + @spec zero?(t()) :: boolean() + def zero?(fr) do fr == zero() end end diff --git a/lib/crypto/bls/g1.ex b/lib/crypto/bls/g1.ex index f74ebdc..08121ae 100644 --- a/lib/crypto/bls/g1.ex +++ b/lib/crypto/bls/g1.ex @@ -65,16 +65,16 @@ defmodule Tezex.Crypto.BLS.G1 do @doc """ Checks if a point is the point at infinity. """ - @spec is_zero?(t()) :: boolean() - def is_zero?(%{z: z}) do - Fq.is_zero?(z) + @spec zero?(t()) :: boolean() + def zero?(%{z: z}) do + Fq.zero?(z) end @doc """ - Alias for is_zero?/1 for compatibility. + Alias for zero?/1 for compatibility. """ - @spec is_infinity?(t()) :: boolean() - def is_infinity?(point), do: is_zero?(point) + @spec infinity?(t()) :: boolean() + def infinity?(point), do: zero?(point) @doc """ Checks if a point is on the curve using Jacobian coordinates. @@ -82,7 +82,7 @@ defmodule Tezex.Crypto.BLS.G1 do """ @spec is_on_curve?(t()) :: boolean() def is_on_curve?(point) do - if is_zero?(point) do + if zero?(point) do true else %{x: x, y: y, z: z} = point @@ -106,7 +106,7 @@ defmodule Tezex.Crypto.BLS.G1 do """ @spec double(t()) :: t() def double(point) do - if is_zero?(point) do + if zero?(point) do zero() else %{x: x, y: y, z: z} = point @@ -137,8 +137,8 @@ defmodule Tezex.Crypto.BLS.G1 do @spec add(t(), t()) :: t() def add(p1, p2) do cond do - is_zero?(p1) -> p2 - is_zero?(p2) -> p1 + zero?(p1) -> p2 + zero?(p2) -> p1 true -> add_projective_algorithm(p1, p2) end end @@ -188,7 +188,7 @@ defmodule Tezex.Crypto.BLS.G1 do """ @spec negate(t()) :: t() def negate(point) do - if is_zero?(point) do + if zero?(point) do zero() else new(point.x, Fq.neg(point.y), point.z) @@ -231,8 +231,8 @@ defmodule Tezex.Crypto.BLS.G1 do @spec eq?(t(), t()) :: boolean() def eq?(p1, p2) do cond do - is_zero?(p1) and is_zero?(p2) -> true - is_zero?(p1) or is_zero?(p2) -> false + zero?(p1) and zero?(p2) -> true + zero?(p1) or zero?(p2) -> false true -> eq_different?(p1, p2) end end @@ -261,7 +261,7 @@ defmodule Tezex.Crypto.BLS.G1 do """ @spec to_affine(t()) :: {:ok, {Fq.t(), Fq.t()}} | {:error, :point_at_infinity} def to_affine(point) do - with false <- is_zero?(point), + with false <- zero?(point), %{x: x, y: y, z: z} = point, {:ok, z_inv} <- Fq.inv(z) do affine_x = Fq.mul(x, z_inv) @@ -292,7 +292,7 @@ defmodule Tezex.Crypto.BLS.G1 do """ @spec to_compressed_bytes(t()) :: binary() def to_compressed_bytes(point) do - with false <- is_zero?(point), + with false <- zero?(point), {:ok, {x, y}} <- to_affine(point) do x_int = Fq.to_integer(x) y_int = Fq.to_integer(y) diff --git a/lib/crypto/bls/g2.ex b/lib/crypto/bls/g2.ex index 674d14d..44bea1c 100644 --- a/lib/crypto/bls/g2.ex +++ b/lib/crypto/bls/g2.ex @@ -176,23 +176,23 @@ defmodule Tezex.Crypto.BLS.G2 do @doc """ Checks if a point is the point at infinity. """ - @spec is_zero?(t()) :: boolean() - def is_zero?(%{z: z}) do - Fq2.is_zero?(z) + @spec zero?(t()) :: boolean() + def zero?(%{z: z}) do + Fq2.zero?(z) end @doc """ - Alias for is_zero?/1 for compatibility. + Alias for zero?/1 for compatibility. """ - @spec is_infinity?(t()) :: boolean() - def is_infinity?(point), do: is_zero?(point) + @spec infinity?(t()) :: boolean() + def infinity?(point), do: zero?(point) @doc """ Checks if a point is on the curve. """ @spec is_on_curve?(t()) :: boolean() def is_on_curve?(point) do - if is_zero?(point) do + if zero?(point) do true else %{x: x, y: y, z: z} = point @@ -219,7 +219,7 @@ defmodule Tezex.Crypto.BLS.G2 do """ @spec double(t()) :: t() def double(point) do - if is_zero?(point) do + if zero?(point) do zero() else %{x: x, y: y, z: z} = point @@ -264,8 +264,8 @@ defmodule Tezex.Crypto.BLS.G2 do @spec add(t(), t()) :: t() def add(p1, p2) do cond do - is_zero?(p1) -> p2 - is_zero?(p2) -> p1 + zero?(p1) -> p2 + zero?(p2) -> p1 true -> add_nonzero(p1, p2) end end @@ -345,7 +345,7 @@ defmodule Tezex.Crypto.BLS.G2 do """ @spec negate(t()) :: t() def negate(point) do - if is_zero?(point) do + if zero?(point) do zero() else new(point.x, Fq2.neg(point.y), point.z) @@ -367,7 +367,7 @@ defmodule Tezex.Crypto.BLS.G2 do """ @spec mul(t(), Fr.t()) :: t() def mul(point, scalar) when byte_size(scalar) == 32 do - if is_zero?(point) do + if zero?(point) do zero() else # Convert scalar to integer for bit operations @@ -381,7 +381,7 @@ defmodule Tezex.Crypto.BLS.G2 do """ @spec mul_integer(t(), non_neg_integer()) :: t() def mul_integer(point, scalar_int) when is_integer(scalar_int) do - if is_zero?(point) do + if zero?(point) do zero() else scalar_multiplication(point, scalar_int) @@ -412,8 +412,8 @@ defmodule Tezex.Crypto.BLS.G2 do @spec eq?(t(), t()) :: boolean() def eq?(p1, p2) do cond do - is_zero?(p1) and is_zero?(p2) -> true - is_zero?(p1) or is_zero?(p2) -> false + zero?(p1) and zero?(p2) -> true + zero?(p1) or zero?(p2) -> false true -> eq_different?(p1, p2) end end @@ -443,7 +443,7 @@ defmodule Tezex.Crypto.BLS.G2 do """ @spec to_affine(t()) :: {:ok, {Fq2.t(), Fq2.t()}} | {:error, :point_at_infinity} def to_affine(point) do - if is_zero?(point) do + if zero?(point) do {:error, :point_at_infinity} else %{x: x, y: y, z: z} = point @@ -483,7 +483,7 @@ defmodule Tezex.Crypto.BLS.G2 do """ @spec to_compressed_bytes(t()) :: binary() def to_compressed_bytes(point) do - if is_zero?(point) do + if zero?(point) do @infinity else case to_affine(point) do @@ -652,7 +652,7 @@ defmodule Tezex.Crypto.BLS.G2 do # Exceptional case: if denominator is zero denominator = - if Fq2.is_zero?(denominator) do + if Fq2.zero?(denominator) do Fq2.mul(@iso_3_z, @iso_3_a) else denominator @@ -734,7 +734,7 @@ defmodule Tezex.Crypto.BLS.G2 do sqrt_candidate = Fq2.mul(root, gamma) temp = Fq2.sub(Fq2.mul(Fq2.square(sqrt_candidate), v), u) - if Fq2.is_zero?(temp) do + if Fq2.zero?(temp) do {:halt, {true, sqrt_candidate}} else {:cont, {false, gamma}} @@ -747,7 +747,7 @@ defmodule Tezex.Crypto.BLS.G2 do Enum.any?(@etas, fn eta -> eta_sqrt_candidate = Fq2.mul(eta, sqrt_candidate) temp = Fq2.sub(Fq2.mul(Fq2.square(eta_sqrt_candidate), v), u) - Fq2.is_zero?(temp) + Fq2.zero?(temp) end) end @@ -756,7 +756,7 @@ defmodule Tezex.Crypto.BLS.G2 do eta_sqrt_candidate = Fq2.mul(eta, sqrt_candidate) temp = Fq2.sub(Fq2.mul(Fq2.square(eta_sqrt_candidate), v), u) - if Fq2.is_zero?(temp) do + if Fq2.zero?(temp) do {:halt, eta_sqrt_candidate} else {:cont, nil} diff --git a/lib/crypto/bls/pairing.ex b/lib/crypto/bls/pairing.ex index 4cfb5e5..f2c28c9 100644 --- a/lib/crypto/bls/pairing.ex +++ b/lib/crypto/bls/pairing.ex @@ -38,26 +38,24 @@ defmodule Tezex.Crypto.BLS.Pairing do @spec pairing(G1.t(), G2.t(), boolean()) :: gt() def pairing(g1_point, g2_point, final_exponentiate) do # Handle special cases - if G1.is_zero?(g1_point) or G2.is_zero?(g2_point) do - Fq12.one() - else - # Verify points are on the correct curves - unless G1.is_on_curve?(g1_point) do + cond do + G1.zero?(g1_point) or G2.zero?(g2_point) -> + Fq12.one() + + not G1.is_on_curve?(g1_point) -> raise ArgumentError, "G1 point is not on curve" - end - unless G2.is_on_curve?(g2_point) do + not G2.is_on_curve?(g2_point) -> raise ArgumentError, "G2 point is not on curve" - end - # Compute Miller loop - result = miller_loop(g2_point, g1_point) + true -> + result = miller_loop(g2_point, g1_point) - if final_exponentiate do - final_exponentiation(result) - else - result - end + if final_exponentiate do + final_exponentiation(result) + else + result + end end end @@ -154,7 +152,7 @@ defmodule Tezex.Crypto.BLS.Pairing do m_den = Fq12.sub(Fq12.mul(x2, z1), Fq12.mul(x1, z2)) cond do - not Fq12.is_zero?(m_den) -> + not Fq12.zero?(m_den) -> # Regular case: different points # Line equation: m * (xt - x1) - (yt - y1) = 0 # Evaluated at T: m * (xt/zt - x1/z1) - (yt/zt - y1/z1) @@ -167,7 +165,7 @@ defmodule Tezex.Crypto.BLS.Pairing do denominator = Fq12.mul(Fq12.mul(m_den, zt), z1) {line_val, denominator} - Fq12.is_zero?(m_num) -> + Fq12.zero?(m_num) -> # Doubling case: same point or point and its negative # Tangent slope: m = 3x1^2 / (2y1*z1) three = embed_fq12_from_integer(3) @@ -238,9 +236,9 @@ defmodule Tezex.Crypto.BLS.Pairing do @doc """ Checks if a GT element is the identity. """ - @spec is_identity?(gt()) :: boolean() - def is_identity?(gt_element) do - Fq12.is_one?(gt_element) + @spec identity?(gt()) :: boolean() + def identity?(gt_element) do + Fq12.one?(gt_element) end @doc """ @@ -282,10 +280,10 @@ defmodule Tezex.Crypto.BLS.Pairing do def pairing_check(pubkey_point, h_msg_point, g1_generator, signature_point) do # First, ensure all points are valid (not zero and on curve) valid_points = - not G1.is_zero?(pubkey_point) and - not G2.is_zero?(h_msg_point) and - not G2.is_zero?(signature_point) and - not G1.is_zero?(g1_generator) and + not G1.zero?(pubkey_point) and + not G2.zero?(h_msg_point) and + not G2.zero?(signature_point) and + not G1.zero?(g1_generator) and G1.is_on_curve?(pubkey_point) and G2.is_on_curve?(h_msg_point) and G2.is_on_curve?(signature_point) and @@ -301,7 +299,7 @@ defmodule Tezex.Crypto.BLS.Pairing do case Fq12.inv(e2) do {:ok, e2_inv} -> product = Fq12.mul(e1, e2_inv) - Fq12.is_one?(final_exponentiation(product)) + Fq12.one?(final_exponentiation(product)) {:error, _} -> false diff --git a/test/crypto/bls/fq12_test.exs b/test/crypto/bls/fq12_test.exs index 36cb485..14da189 100644 --- a/test/crypto/bls/fq12_test.exs +++ b/test/crypto/bls/fq12_test.exs @@ -36,7 +36,7 @@ defmodule Tezex.Crypto.BLS.Fq12Test do for _ <- 1..10 do x = Fq.random() - unless Fq.is_zero?(x) do + unless Fq.zero?(x) do # Test that x^a * x^b = x^(a+b) a = :rand.uniform(20) b = :rand.uniform(20) @@ -64,7 +64,7 @@ defmodule Tezex.Crypto.BLS.Fq12Test do for _ <- 1..20 do x = Fq.random() - unless Fq.is_zero?(x) do + unless Fq.zero?(x) do case Fq.inv(x) do {:ok, x_inv} -> # Test that x^1 * x^(-1) = 1 @@ -94,7 +94,7 @@ defmodule Tezex.Crypto.BLS.Fq12Test do a = random_fq2() b = random_fq2() - unless Fq2.is_zero?(a) or Fq2.is_zero?(b) do + unless Fq2.zero?(a) or Fq2.zero?(b) do # Test that (a * b) * inv(b) = a (when possible) product = Fq2.mul(a, b) @@ -144,7 +144,7 @@ defmodule Tezex.Crypto.BLS.Fq12Test do # norm returns Fq element, product is Fq2, so we compare the real part {real_part, imag_part} = product assert Fq.eq?(norm_a, real_part) - assert Fq2.is_zero?({Fq.zero(), imag_part}) or Fq.is_zero?(imag_part) + assert Fq2.zero?({Fq.zero(), imag_part}) or Fq.zero?(imag_part) # Test conjugate of conjugate: conj(conj(a)) = a conj_conj_a = Fq2.conjugate(conj_a) @@ -158,25 +158,25 @@ defmodule Tezex.Crypto.BLS.Fq12Test do for _ <- 1..100 do random_fq = Fq.random() # Random values should almost never be one - refute Fq.is_one?(random_fq) + refute Fq.one?(random_fq) end end test "is_one with zero" do - refute Fq.is_one?(Fq.zero()) - refute Fq2.is_one?(Fq2.zero()) + refute Fq.one?(Fq.zero()) + refute Fq2.one?(Fq2.zero()) end test "is_one with actual one" do - assert Fq.is_one?(Fq.one()) - assert Fq2.is_one?(Fq2.one()) + assert Fq.one?(Fq.one()) + assert Fq2.one?(Fq2.one()) end test "Fq2 is_one with random value" do for _ <- 1..100 do random_fq2 = random_fq2() # Random Fq2 values should almost never be one - refute Fq2.is_one?(random_fq2) + refute Fq2.one?(random_fq2) end end end @@ -186,25 +186,25 @@ defmodule Tezex.Crypto.BLS.Fq12Test do for _ <- 1..100 do random_fq = Fq.random() # Random values should almost never be zero - refute Fq.is_zero?(random_fq) + refute Fq.zero?(random_fq) end end test "is_zero with actual zero" do - assert Fq.is_zero?(Fq.zero()) - assert Fq2.is_zero?(Fq2.zero()) + assert Fq.zero?(Fq.zero()) + assert Fq2.zero?(Fq2.zero()) end test "is_zero with one" do - refute Fq.is_zero?(Fq.one()) - refute Fq2.is_zero?(Fq2.one()) + refute Fq.zero?(Fq.one()) + refute Fq2.zero?(Fq2.one()) end test "Fq2 is_zero with random value" do for _ <- 1..100 do random_fq2 = random_fq2() # Random Fq2 values should almost never be zero - refute Fq2.is_zero?(random_fq2) + refute Fq2.zero?(random_fq2) end end end @@ -261,7 +261,7 @@ defmodule Tezex.Crypto.BLS.Fq12Test do for _ <- 1..50 do a = Fq.random() - unless Fq.is_zero?(a) do + unless Fq.zero?(a) do case Fq.inv(a) do {:ok, a_inv} -> # a * a^(-1) = 1 diff --git a/test/crypto/bls/g1_test.exs b/test/crypto/bls/g1_test.exs index 9486c10..4dc5bd3 100644 --- a/test/crypto/bls/g1_test.exs +++ b/test/crypto/bls/g1_test.exs @@ -34,7 +34,7 @@ defmodule Tezex.Crypto.BLS.G1Test do test "is not zero" do generator = G1.generator() - refute G1.is_infinity?(generator) + refute G1.infinity?(generator) end end @@ -54,7 +54,7 @@ defmodule Tezex.Crypto.BLS.G1Test do result = G1.mul(generator, zero) - assert G1.is_infinity?(result) + assert G1.infinity?(result) end test "2 * generator equals generator + generator" do @@ -96,7 +96,7 @@ defmodule Tezex.Crypto.BLS.G1Test do assert compressed == <<0xC0>> <> <<0::376>> assert {:ok, decompressed} = G1.from_compressed_bytes(compressed) - assert G1.is_infinity?(decompressed) + assert G1.infinity?(decompressed) end end @@ -228,7 +228,7 @@ defmodule Tezex.Crypto.BLS.G1Test do curve_order = Constants.curve_order() curve_order_fr = Fr.from_integer(curve_order) mul_g1_order = G1.mul(g1, curve_order_fr) - assert G1.is_infinity?(mul_g1_order) + assert G1.infinity?(mul_g1_order) end end @@ -256,7 +256,7 @@ defmodule Tezex.Crypto.BLS.G1Test do # assert is_inf(neg(Z1)) neg_z1 = G1.negate(z1) - assert G1.is_infinity?(neg_z1) + assert G1.infinity?(neg_z1) end end diff --git a/test/crypto/bls/g2_test.exs b/test/crypto/bls/g2_test.exs index 4516e06..9e8f26a 100644 --- a/test/crypto/bls/g2_test.exs +++ b/test/crypto/bls/g2_test.exs @@ -14,7 +14,7 @@ defmodule Tezex.Crypto.BLS.G2Test do test "is not zero" do generator = G2.generator() - refute G2.is_infinity?(generator) + refute G2.infinity?(generator) end end @@ -34,7 +34,7 @@ defmodule Tezex.Crypto.BLS.G2Test do result = G2.mul(generator, zero) - assert G2.is_infinity?(result) + assert G2.infinity?(result) end end @@ -62,7 +62,7 @@ defmodule Tezex.Crypto.BLS.G2Test do assert compressed == <<0xC0, 0::376, 0::384>> assert {:ok, decompressed} = G2.from_compressed_bytes(compressed) - assert G2.is_infinity?(decompressed) + assert G2.infinity?(decompressed) end end @@ -106,7 +106,7 @@ defmodule Tezex.Crypto.BLS.G2Test do point = G2.hash_to_curve(test_message, ciphersuite) assert G2.is_on_curve?(point) - refute G2.is_infinity?(point) + refute G2.infinity?(point) end end @@ -343,14 +343,14 @@ defmodule Tezex.Crypto.BLS.G2Test do curve_order = Constants.curve_order() curve_order_fr = Fr.from_integer(curve_order) mul_g2_order = G2.mul(g2, curve_order_fr) - assert G2.is_infinity?(mul_g2_order) + assert G2.infinity?(mul_g2_order) # assert not is_inf(multiply(G2, 2 * field_modulus - curve_order)) field_modulus = Constants.field_modulus() special_scalar = 2 * field_modulus - curve_order special_scalar_fr = Fr.from_integer(special_scalar) mul_g2_special = G2.mul(g2, special_scalar_fr) - refute G2.is_infinity?(mul_g2_special) + refute G2.infinity?(mul_g2_special) # assert is_on_curve(multiply(G2, 9), b2) assert G2.is_on_curve?(mul_g2_9) @@ -381,7 +381,7 @@ defmodule Tezex.Crypto.BLS.G2Test do # assert is_inf(neg(Z2)) neg_z2 = G2.negate(z2) - assert G2.is_infinity?(neg_z2) + assert G2.infinity?(neg_z2) end end end diff --git a/test/crypto/bls/pairing_test.exs b/test/crypto/bls/pairing_test.exs index f17abbe..d087a32 100644 --- a/test/crypto/bls/pairing_test.exs +++ b/test/crypto/bls/pairing_test.exs @@ -24,7 +24,7 @@ defmodule Tezex.Crypto.BLS.PairingTest do # assert p1 * pn1 == 1 product = Fq12.mul(p1, pn1) - assert Fq12.is_one?(product) + assert Fq12.one?(product) end test "pairing output order" do @@ -36,7 +36,7 @@ defmodule Tezex.Crypto.BLS.PairingTest do # assert p1**curve_order == 1 p1_pow_order = Fq12.pow(p1, @curve_order) - assert Fq12.is_one?(p1_pow_order) + assert Fq12.one?(p1_pow_order) end test "pairing bilinearity on G1" do From e1b0a66eba3d97f436e1ded5a5842ce7a2138316 Mon Sep 17 00:00:00 2001 From: victor felder Date: Tue, 19 May 2026 12:24:15 +0200 Subject: [PATCH 14/15] refactor(forge_operation): extract required keys --- lib/forge_operation.ex | 88 ++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 55 deletions(-) diff --git a/lib/forge_operation.ex b/lib/forge_operation.ex index ac5b382..efee084 100644 --- a/lib/forge_operation.ex +++ b/lib/forge_operation.ex @@ -76,9 +76,10 @@ defmodule Tezex.ForgeOperation do end end + @keys ~w(branch contents) @spec operation_group(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def operation_group(operation_group) do - with :ok <- validate_required_keys(operation_group, ~w(branch contents)), + with :ok <- validate_required_keys(operation_group, @keys), operations = Enum.map(operation_group["contents"], &operation/1), nil <- Enum.find(operations, &(elem(&1, 0) == :error)) do operations = Enum.map(operations, fn {:ok, operation} -> operation end) @@ -95,9 +96,10 @@ defmodule Tezex.ForgeOperation do end end + @keys ~w(kind pkh secret) @spec activate_account(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def activate_account(content) do - with :ok <- validate_required_keys(content, ~w(kind pkh secret)) do + with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(content["kind"]), @@ -110,13 +112,10 @@ defmodule Tezex.ForgeOperation do end end + @keys ~w(kind source fee counter gas_limit storage_limit public_key) @spec reveal(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def reveal(content) do - with :ok <- - validate_required_keys( - content, - ~w(kind source fee counter gas_limit storage_limit public_key) - ) do + with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(content["kind"]), @@ -140,19 +139,16 @@ defmodule Tezex.ForgeOperation do end end + @keys ~w(kind source fee counter gas_limit storage_limit amount destination) + @params ~w(parameters parameters.entrypoint parameters.value) @spec transaction(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def transaction(content) do - with :ok <- - validate_required_keys( - content, - ~w(kind source fee counter gas_limit storage_limit amount destination) - ), + with :ok <- validate_required_keys(content, @keys), :ok <- - (has_parameters(content) && - validate_required_keys( - content, - ~w(parameters parameters.entrypoint parameters.value) - )) || :ok do + if(has_parameters(content), + do: validate_required_keys(content, @params), + else: :ok + ) do content = [ forge_tag(content["kind"]), @@ -181,13 +177,10 @@ defmodule Tezex.ForgeOperation do end end + @keys ~w(kind source fee counter gas_limit storage_limit balance) @spec origination(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def origination(content) do - with :ok <- - validate_required_keys( - content, - ~w(kind source fee counter gas_limit storage_limit balance) - ) do + with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(content["kind"]), @@ -212,13 +205,10 @@ defmodule Tezex.ForgeOperation do end end + @keys ~w(kind source fee counter gas_limit storage_limit) @spec delegation(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def delegation(content) do - with :ok <- - validate_required_keys( - content, - ~w(kind source fee counter gas_limit storage_limit) - ) do + with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(content["kind"]), @@ -241,9 +231,10 @@ defmodule Tezex.ForgeOperation do end end + @keys ~w(kind level) @spec endorsement(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def endorsement(content) do - with :ok <- validate_required_keys(content, ~w(kind level)) do + with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(content["kind"]), @@ -255,13 +246,10 @@ defmodule Tezex.ForgeOperation do end end + @keys ~w(branch operations operations.kind operations.level signature) @spec inline_endorsement(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def inline_endorsement(content) do - with :ok <- - validate_required_keys( - content, - ~w(branch operations operations.kind operations.level signature) - ) do + with :ok <- validate_required_keys(content, @keys) do content = [ Forge.forge_base58(content["branch"]), @@ -275,9 +263,10 @@ defmodule Tezex.ForgeOperation do end end + @keys ~w(kind endorsement slot) @spec endorsement_with_slot(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def endorsement_with_slot(content) do - with :ok <- validate_required_keys(content, ~w(kind endorsement slot)), + with :ok <- validate_required_keys(content, @keys), {:ok, endorsement} <- inline_endorsement(content["endorsement"]) do content = [ @@ -291,9 +280,10 @@ defmodule Tezex.ForgeOperation do end end + @keys ~w(kind arbitrary) @spec failing_noop(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def failing_noop(content) do - with :ok <- validate_required_keys(content, ~w(kind arbitrary)) do + with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(content["kind"]), @@ -305,13 +295,10 @@ defmodule Tezex.ForgeOperation do end end + @keys ~w(kind source fee counter gas_limit storage_limit value) @spec register_global_constant(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def register_global_constant(content) do - with :ok <- - validate_required_keys( - content, - ~w(kind source fee counter gas_limit storage_limit value) - ) do + with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(content["kind"]), @@ -328,13 +315,10 @@ defmodule Tezex.ForgeOperation do end end + @keys ~w(kind source fee counter gas_limit storage_limit ticket_contents ticket_ty ticket_ticketer ticket_amount destination entrypoint) @spec transfer_ticket(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def transfer_ticket(content) do - with :ok <- - validate_required_keys( - content, - ~w(kind source fee counter gas_limit storage_limit ticket_contents ticket_ty ticket_ticketer ticket_amount destination entrypoint) - ) do + with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(content["kind"]), @@ -356,13 +340,10 @@ defmodule Tezex.ForgeOperation do end end + @keys ~w(kind source fee counter gas_limit storage_limit message) @spec smart_rollup_add_messages(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def smart_rollup_add_messages(content) do - with :ok <- - validate_required_keys( - content, - ~w(kind source fee counter gas_limit storage_limit message) - ) do + with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(content["kind"]), @@ -384,14 +365,11 @@ defmodule Tezex.ForgeOperation do end end + @keys ~w(kind source fee counter gas_limit storage_limit rollup cemented_commitment output_proof) @spec smart_rollup_execute_outbox_message(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def smart_rollup_execute_outbox_message(content) do - with :ok <- - validate_required_keys( - content, - ~w(kind source fee counter gas_limit storage_limit rollup cemented_commitment output_proof) - ) do + with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(content["kind"]), From cf11ede88a517d8c79fa1218ce282834bd893a71 Mon Sep 17 00:00:00 2001 From: victor felder Date: Tue, 19 May 2026 16:34:51 +0200 Subject: [PATCH 15/15] docs: prepare changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 618544b..a3500b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog for Tezex +## vNext + +- [BREAKING][crypto/bls]: drop `is_` prefix from predicates (`is_zero?` → `zero?`, `is_one?` → `one?`, `is_infinity?` → `infinity?`) across `Fq`, `Fr`, `Fq2`, `Fq12`, `FqP`, `G1`, `G2` +- [BREAKING][crypto/bls]: `Fq12.inv/1` and `FqP.inv/1` now return `{:ok, t} | {:error, :not_invertible}` +- [BREAKING][crypto/bls]: `Fr.from_bytes/1` rejects out-of-range scalars with `:out_of_range` instead of silently reducing +- [zarith]: raise `ArgumentError` on malformed/truncated hex input +- [forge_operation]: fix `endorsement/1`, `endorsement_with_slot/1`, `failing_noop/1` (were passing the kind string to `forge_tag` and raising `ArgumentError`) +- [crypto/private_key]: `from_encoded_key!/2` preserves the underlying error instead of masking everything as `invalid_base58` +- [crypto/math]: `mod_inverse/2` handles negative input +- [crypto/bls]: validate scalars in `Fr.from_bytes/1`, fix `Fq.sqrt/1` zero case, handle negative input in `Fq.from_integer/1`, use `from_integer` reduction in `BLS.from_seed/1` +- [crypto/ecdsa]: fix `k` upper-bound check in signature generation (was comparing integer to binary, always false) +- [crypto/nacl]: fix `crypto_secretbox_open/3` typespec to include `:invalid_nonce_length` and `:invalid_key_length` +- [crypto/bls]: pre-compute Miller loop bits in pairing for faster verification + ## v4.0.0 - [BREAKING]: `Tezex.Rpc.get_counter_for_account/2` now returns a tuple