UltrafastSecp256k1/cpu/tests/test_point_edge_cases.cpp
Vano Chkheidze ab70b82be7
Code-scanning remediation pass (#102)
* docs(arm64): Add comprehensive ARM64 audit, benchmark, and gap analysis

- ARM64_AUDIT_BENCHMARK.md: Platform certification with full audit results
  * Apple Silicon M1/M2/M3 native dudect verification (48/49 modules)
  * Android ARM64 (Cortex-A55) real hardware benchmarks
  * Linux ARM64 cross-compile validation
  * Performance comparison: ARM64 vs x86-64 vs RISC-V
  * CI workflows: ct-arm64.yml, release.yml Android NDK

- ARM64_GAPS_ANALYSIS.md: Testing and optimization opportunities
  * Testing gaps: Native Linux ARM64 CI, Android device farm
  * Optimization roadmap: NEON batch ops (2-3x speedup priority)
  * Stack round-trip elimination, Metal GPU analysis
  * Decision matrix with effort/impact/recommendations

- README.md: Add ARM64 docs to Documentation section

Addresses issue #87 ARM audit request

* perf(x86-64): comprehensive primitive optimization sweep

Field arithmetic:
- FE52 normalize: rewrite normalizes_to_zero_var with libsecp fast-path
  (check t0/t4 only, skip full carry propagation in 99.99% of calls)
- FE52 negate: eliminate copy-then-negate pattern, compute directly
- FE52 normalize: fold-first approach (2 carry chains instead of 3)
- Force-inline fe52_normalize_inline with SECP256K1_FE52_FORCE_INLINE
- inverse_safegcd: use var-time zero check instead of CT normalizes_to_zero

Scalar arithmetic:
- scalar_mul: complement-based reduction (Barrett ~36 muls -> ~14 muls)
  35.1ns -> 18.9ns (46% faster)
- Force-inline 4 SafeGCD helpers (divsteps_62_var, update_de_62,
  update_fg_62_var, normalize_62) - scalar_inv 10% faster

Field SafeGCD:
- Force-inline 5 helpers (safegcd_divsteps_62_var, safegcd_update_fg,
  safegcd_update_de, safegcd_reduce_len, safegcd_normalize)

Point arithmetic:
- Reduce negate magnitudes: negate(23)->negate(8) for X coords,
  negate(10)->negate(4) for Y coords in all 6 add_mixed variants
- Enables fast-path normalizes_to_zero_var (lower magnitude = fewer ops)

Schnorr:
- Cached verify path with pre-lifted pubkey (skip sqrt/lift_x)
- Direct FE52 r-value parsing (no FieldElement intermediate)

Benchmark:
- Fix point_add/dbl fairness: remove loop-carried dependency
  (was penalizing Ultra vs libsecp's independent iteration pattern)
- Fix normalize benchmark: use magnitude-2 input for both sides
- Add FE52 hot-path micro-diagnostics
- Add libsecp scalar_negate, from_bytes benchmarks
- Add comprehensive verify cost decomposition

All 21/21 selftest modules pass, 89/89 ECC properties verified.

* Fix code-scanning findings in CT and lint clusters

---------

Co-authored-by: shrec <shrec@users.noreply.github.com>
2026-03-07 06:19:36 +04:00

346 lines
12 KiB
C++

// ============================================================================
// Test: Point output function edge cases (infinity + Z=0 defensive guards)
// ============================================================================
// Exercises all Point output functions with:
// - Point::infinity() (the canonical infinity representation)
// - P + (-P) = infinity (computed infinity through group law)
// - Normal point G (baseline sanity check)
//
// Ensures to_compressed(), to_uncompressed(), x(), y(), has_even_y(),
// and x_bytes_and_parity() all behave correctly on edge-case inputs
// WITHOUT crashing or invoking undefined behavior.
// ============================================================================
#include "secp256k1/point.hpp"
#include "secp256k1/scalar.hpp"
#include "secp256k1/field.hpp"
#include <cstdio>
#include <cstdint>
#include <array>
using namespace secp256k1::fast;
static int tests_run = 0;
static int tests_passed = 0;
#define CHECK(cond, msg) do { \
++tests_run; \
if (cond) { ++tests_passed; printf(" [PASS] %s\n", msg); } \
else { printf(" [FAIL] %s\n", msg); } \
} while(0)
// Check that all 33 bytes are zero (infinity encoding)
static bool is_zero_33(const std::array<uint8_t, 33>& a) {
for (auto b : a) {
if (b != 0) {
return false;
}
}
return true;
}
// Check that all 65 bytes are zero (infinity encoding)
static bool is_zero_65(const std::array<uint8_t, 65>& a) {
for (auto b : a) {
if (b != 0) {
return false;
}
}
return true;
}
// All 32 bytes zero
static bool is_zero_32(const std::array<uint8_t, 32>& a) {
for (auto b : a) {
if (b != 0) {
return false;
}
}
return true;
}
// -- infinity tests ----------------------------------------------------------
static void test_infinity_outputs() {
printf("\n=== Infinity point output functions ===\n");
const Point inf = Point::infinity();
// to_compressed: should return all zeros
const auto comp = inf.to_compressed();
CHECK(is_zero_33(comp), "infinity to_compressed -> all zeros");
// to_uncompressed: should return all zeros
const auto uncomp = inf.to_uncompressed();
CHECK(is_zero_65(uncomp), "infinity to_uncompressed -> all zeros");
// x(): should be zero field element
const FieldElement xv = inf.x();
CHECK(xv == FieldElement::zero(), "infinity x() -> zero");
// y(): should be zero field element
const FieldElement yv = inf.y();
CHECK(yv == FieldElement::zero(), "infinity y() -> zero");
// has_even_y: infinity -> false
CHECK(inf.has_even_y() == false, "infinity has_even_y -> false");
// x_bytes_and_parity: infinity -> (zeros, false)
auto [xb, parity] = inf.x_bytes_and_parity();
CHECK(is_zero_32(xb), "infinity x_bytes_and_parity x -> zeros");
CHECK(parity == false, "infinity x_bytes_and_parity parity -> false");
// is_infinity flag
CHECK(inf.is_infinity(), "infinity is_infinity -> true");
}
// -- computed infinity via P + (-P) ------------------------------------------
static void test_computed_infinity() {
printf("\n=== Computed infinity (P + (-P)) output functions ===\n");
const Point G = Point::generator();
const Point negG = G.negate();
const Point sum = G.add(negG); // should be infinity
CHECK(sum.is_infinity(), "G + (-G) is infinity");
const auto comp = sum.to_compressed();
CHECK(is_zero_33(comp), "G+(-G) to_compressed -> all zeros");
const auto uncomp = sum.to_uncompressed();
CHECK(is_zero_65(uncomp), "G+(-G) to_uncompressed -> all zeros");
const FieldElement xv = sum.x();
CHECK(xv == FieldElement::zero(), "G+(-G) x() -> zero");
const FieldElement yv = sum.y();
CHECK(yv == FieldElement::zero(), "G+(-G) y() -> zero");
CHECK(sum.has_even_y() == false, "G+(-G) has_even_y -> false");
auto [xb, parity] = sum.x_bytes_and_parity();
CHECK(is_zero_32(xb), "G+(-G) x_bytes_and_parity x -> zeros");
CHECK(parity == false, "G+(-G) x_bytes_and_parity parity -> false");
}
// -- normal point G (sanity baseline) ----------------------------------------
static void test_generator_outputs() {
printf("\n=== Generator point output functions ===\n");
const Point G = Point::generator();
CHECK(!G.is_infinity(), "G is not infinity");
auto comp = G.to_compressed();
// G compressed starts with 0x02 (even y)
CHECK(comp[0] == 0x02 || comp[0] == 0x03, "G compressed prefix valid");
CHECK(!is_zero_33(comp), "G compressed is nonzero");
auto uncomp = G.to_uncompressed();
CHECK(uncomp[0] == 0x04, "G uncompressed prefix 0x04");
CHECK(!is_zero_65(uncomp), "G uncompressed is nonzero");
const FieldElement xv = G.x();
CHECK(!(xv == FieldElement::zero()), "G x() is nonzero");
const FieldElement yv = G.y();
CHECK(!(yv == FieldElement::zero()), "G y() is nonzero");
// G has even y (known property of secp256k1 generator)
CHECK(G.has_even_y() == true, "G has_even_y -> true");
auto [xb, parity] = G.x_bytes_and_parity();
CHECK(!is_zero_32(xb), "G x_bytes nonzero");
// parity=false means even y for G
CHECK(parity == false, "G x_bytes_and_parity parity -> false (even y)");
}
// -- scalar mul edge cases ---------------------------------------------------
static void test_scalar_mul_edge_cases() {
printf("\n=== Scalar multiplication edge cases ===\n");
const Point G = Point::generator();
// 0 * G = infinity
const Scalar zero_s = Scalar::from_uint64(0);
const Point p0 = G.scalar_mul(zero_s);
CHECK(p0.is_infinity(), "0*G is infinity");
auto comp0 = p0.to_compressed();
CHECK(is_zero_33(comp0), "0*G compressed -> zeros");
// 1 * G = G
const Scalar one_s = Scalar::from_uint64(1);
const Point p1 = G.scalar_mul(one_s);
CHECK(!p1.is_infinity(), "1*G is not infinity");
CHECK(p1.to_compressed() == G.to_compressed(), "1*G == G");
// n * G = infinity (group order)
// n = FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141
const Scalar n = Scalar::from_limbs({
0xBFD25E8CD0364141ULL,
0xBAAEDCE6AF48A03BULL,
0xFFFFFFFFFFFFFFFEULL,
0xFFFFFFFFFFFFFFFFULL
});
const Point pn = G.scalar_mul(n);
CHECK(pn.is_infinity(), "n*G is infinity");
auto compn = pn.to_compressed();
CHECK(is_zero_33(compn), "n*G compressed -> zeros");
CHECK(pn.has_even_y() == false, "n*G has_even_y -> false");
}
// -- roundtrip: compressed encode consistency ----------------------------
static void test_roundtrip() {
printf("\n=== Roundtrip compressed encoding ===\n");
const Point G = Point::generator();
// G compressed twice should give same bytes
auto comp1 = G.to_compressed();
auto comp2 = G.to_compressed();
CHECK(comp1 == comp2, "G double-compress consistency");
// 2*G = G + G
const Point G2 = G.add(G);
auto comp2a = G2.to_compressed();
auto comp2b = G2.to_compressed();
CHECK(comp2a == comp2b, "2*G double-compress consistency");
// Uncompressed consistency
auto uncomp1 = G.to_uncompressed();
auto uncomp2 = G.to_uncompressed();
CHECK(uncomp1 == uncomp2, "G double-uncompress consistency");
// x() and y() consistency
const FieldElement x1 = G.x();
const FieldElement x2 = G.x();
CHECK(x1 == x2, "G x() consistency");
const FieldElement y1 = G.y();
const FieldElement y2 = G.y();
CHECK(y1 == y2, "G y() consistency");
// has_even_y consistency
const bool e1 = G.has_even_y();
const bool e2 = G.has_even_y();
CHECK(e1 == e2, "G has_even_y consistency");
// x_bytes_and_parity consistency
auto [xb1, p1] = G.x_bytes_and_parity();
auto [xb2, p2] = G.x_bytes_and_parity();
CHECK(xb1 == xb2, "G x_bytes consistency");
CHECK(p1 == p2, "G parity consistency");
}
// -- x_only_bytes tests ------------------------------------------------------
static void test_x_only_bytes() {
printf("\n=== x_only_bytes tests ===\n");
const Point G = Point::generator();
// x_only_bytes should match x_bytes_and_parity x component
auto xonly = G.x_only_bytes();
auto [xbp, parity] = G.x_bytes_and_parity();
(void)parity;
CHECK(xonly == xbp, "G x_only_bytes matches x_bytes_and_parity");
// x_only_bytes should match to_compressed bytes 1..32
auto comp = G.to_compressed();
std::array<uint8_t, 32> comp_x;
std::copy(comp.begin() + 1, comp.end(), comp_x.begin());
CHECK(xonly == comp_x, "G x_only_bytes matches to_compressed[1..32]");
// infinity -> zeros
auto inf_xonly = Point::infinity().x_only_bytes();
CHECK(is_zero_32(inf_xonly), "infinity x_only_bytes -> zeros");
// 2G consistency
const Point G2 = G.add(G);
auto xonly2a = G2.x_only_bytes();
auto xonly2b = G2.x_only_bytes();
CHECK(xonly2a == xonly2b, "2G x_only_bytes consistency");
}
// -- batch serialization tests -----------------------------------------------
static void test_batch_serialization() {
printf("\n=== Batch serialization tests ===\n");
const Point G = Point::generator();
// Create 8 distinct points: G, 2G, 3G, ..., 8G
constexpr int N = 8;
Point points[N];
points[0] = G;
for (int i = 1; i < N; ++i) {
points[i] = points[i-1].add(G);
}
// batch_to_compressed: should match individual to_compressed
std::array<uint8_t, 33> batch_comp[N];
Point::batch_to_compressed(points, N, batch_comp);
bool all_match = true;
for (int i = 0; i < N; ++i) {
if (batch_comp[i] != points[i].to_compressed()) {
all_match = false;
printf(" batch_to_compressed mismatch at index %d\n", i);
}
}
CHECK(all_match, "batch_to_compressed matches individual");
// batch_x_only_bytes: should match individual x_only_bytes
std::array<uint8_t, 32> batch_xonly[N];
Point::batch_x_only_bytes(points, N, batch_xonly);
bool xonly_match = true;
for (int i = 0; i < N; ++i) {
if (batch_xonly[i] != points[i].x_only_bytes()) {
xonly_match = false;
printf(" batch_x_only_bytes mismatch at index %d\n", i);
}
}
CHECK(xonly_match, "batch_x_only_bytes matches individual");
// batch_normalize: check x,y match individual x(),y()
FieldElement batch_x[N], batch_y[N];
Point::batch_normalize(points, N, batch_x, batch_y);
bool norm_match = true;
for (int i = 0; i < N; ++i) {
if (!(batch_x[i] == points[i].x()) || !(batch_y[i] == points[i].y())) {
norm_match = false;
printf(" batch_normalize mismatch at index %d\n", i);
}
}
CHECK(norm_match, "batch_normalize matches individual x(),y()");
// Edge: batch with infinity points mixed in
Point mixed[4] = {G, Point::infinity(), G.add(G), Point::infinity()};
std::array<uint8_t, 33> mixed_comp[4];
Point::batch_to_compressed(mixed, 4, mixed_comp);
CHECK(mixed_comp[0] == G.to_compressed(), "batch mixed[0]=G correct");
CHECK(is_zero_33(mixed_comp[1]), "batch mixed[1]=inf -> zeros");
CHECK(mixed_comp[2] == G.add(G).to_compressed(), "batch mixed[2]=2G correct");
CHECK(is_zero_33(mixed_comp[3]), "batch mixed[3]=inf -> zeros");
// Edge: empty batch (should not crash)
Point::batch_to_compressed(nullptr, 0, nullptr);
Point::batch_x_only_bytes(nullptr, 0, nullptr);
CHECK(true, "empty batch calls do not crash");
}
// ============================================================================
int main() {
printf("Point edge-case tests\n");
printf("=====================\n");
test_infinity_outputs();
test_computed_infinity();
test_generator_outputs();
test_scalar_mul_edge_cases();
test_roundtrip();
test_x_only_bytes();
test_batch_serialization();
printf("\n-----\nResults: %d / %d passed\n", tests_passed, tests_run);
return (tests_passed == tests_run) ? 0 : 1;
}