* 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>
346 lines
12 KiB
C++
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;
|
|
}
|