UltrafastSecp256k1/cpu/tests/test_comprehensive.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

2631 lines
99 KiB
C++

// ============================================================================
// Comprehensive Test Suite -- UltrafastSecp256k1
// ============================================================================
// 500+ categorized tests covering every public API, edge case, and platform.
// Uses TestCategory enum for selective runs. Organic library component --
// no external test frameworks needed.
//
// Build: part of run_selftest (via CMakeLists), or standalone.
// ============================================================================
#include "secp256k1/test_framework.hpp"
#include "secp256k1/fast.hpp"
#include "secp256k1/field.hpp"
#include "secp256k1/scalar.hpp"
#include "secp256k1/point.hpp"
#include "secp256k1/field_branchless.hpp"
#include "secp256k1/field_asm.hpp"
#include "secp256k1/field_optimal.hpp"
#include "secp256k1/glv.hpp"
#include "secp256k1/ct_utils.hpp"
#include "secp256k1/ct/ops.hpp"
#include "secp256k1/ct/field.hpp"
#include "secp256k1/ct/scalar.hpp"
#include "secp256k1/ct/point.hpp"
#include "secp256k1/sha256.hpp"
#include "secp256k1/sha512.hpp"
#include "secp256k1/ecdsa.hpp"
#include "secp256k1/schnorr.hpp"
#include "secp256k1/ecdh.hpp"
#include "secp256k1/recovery.hpp"
#include "secp256k1/pippenger.hpp"
#include "secp256k1/ecmult_gen_comb.hpp"
#include "secp256k1/multiscalar.hpp"
#include "secp256k1/precompute.hpp"
#include "secp256k1/batch_add_affine.hpp"
#include "secp256k1/batch_verify.hpp"
#include <iostream>
#include <iomanip>
#include <sstream>
#include <cstring>
#include <chrono>
#include <vector>
#include <array>
#include <cstdint>
using FE = secp256k1::fast::FieldElement;
using SC = secp256k1::fast::Scalar;
using PT = secp256k1::fast::Point;
using secp256k1::test::TestCategory;
using secp256k1::test::TestCounters;
// -- Global counters ----------------------------------------------------------
static TestCounters g_counters;
#define CHECK(cond, msg) \
do { \
if (cond) { ++g_counters.passed; } \
else { \
++g_counters.failed; \
std::cerr << " FAIL: " << (msg) << '\n'; \
} \
} while (0)
#define SKIP(msg) \
do { ++g_counters.skipped; } while (0)
// -- Utility ------------------------------------------------------------------
static bool pt_eq(const PT& a, const PT& b) {
if (a.is_infinity() && b.is_infinity()) return true;
if (a.is_infinity() || b.is_infinity()) return false;
return a.x().to_bytes() == b.x().to_bytes() &&
a.y().to_bytes() == b.y().to_bytes();
}
// secp256k1 prime p = 2^256 - 0x1000003D1
static FE secp256k1_p_minus_1() {
// p-1 = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2E
return FE::from_hex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2E");
}
// secp256k1 group order n
static SC secp256k1_n() {
return SC::from_hex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141");
}
// ============================================================================
// CATEGORY 1: Field Arithmetic (TestCategory::FieldArith)
// ============================================================================
static void test_field_arith() {
std::cout << " [FieldArith] Field arithmetic tests..." << '\n';
FE const zero = FE::zero();
FE const one = FE::one();
// 1.1: Zero + Zero = Zero
CHECK(FE::zero() + FE::zero() == FE::zero(), "0+0==0");
// 1.2: One + Zero = One
CHECK(one + zero == one, "1+0==1");
// 1.3: Zero + One = One (commutativity)
CHECK(zero + one == one, "0+1==1");
// 1.4: One + One = Two
FE const two = FE::from_uint64(2);
CHECK(one + one == two, "1+1==2");
// 1.5: Subtraction identity
CHECK(one - one == zero, "1-1==0");
// 1.6: Subtraction self
FE const val = FE::from_uint64(42);
CHECK(val - val == zero, "a-a==0");
// 1.7: Multiplication by zero
CHECK(val * zero == zero, "a*0==0");
// 1.8: Multiplication by one
CHECK(val * one == val, "a*1==a");
// 1.9: Multiplication commutativity
FE const a = FE::from_uint64(7);
FE const b = FE::from_uint64(11);
CHECK(a * b == b * a, "a*b==b*a");
// 1.10: Multiplication associativity
FE const c = FE::from_uint64(13);
CHECK((a * b) * c == a * (b * c), "(a*b)*c==a*(b*c)");
// 1.11: Distributive law
CHECK(a * (b + c) == a * b + a * c, "a*(b+c)==a*b+a*c");
// 1.12: Square vs self-multiply
CHECK(a.square() == a * a, "a^2==a*a");
// 1.13: Square of one
CHECK(one.square() == one, "1^2==1");
// 1.14: Square of zero
CHECK(zero.square() == zero, "0^2==0");
// 1.15: Repeated squaring chain
FE const x = FE::from_uint64(3);
FE const x2 = x.square(); // 9
FE const x4 = x2.square(); // 81
FE const expected = FE::from_uint64(81);
CHECK(x4 == expected, "3^4==81");
// 1.16: In-place square
FE y = FE::from_uint64(5);
FE const y_sq = y.square();
y.square_inplace();
CHECK(y == y_sq, "square_inplace consistency");
// 1.17: Additive inverse via subtraction
FE const neg_a = zero - a;
CHECK(a + neg_a == zero, "a+(-a)==0");
// 1.18: Double subtraction identity
CHECK(a - b - a + b == zero, "a-b-a+b==0");
// 1.19: Compound assignment +=
FE acc = FE::from_uint64(10);
FE orig = acc;
acc += FE::from_uint64(5);
CHECK(acc == orig + FE::from_uint64(5), "operator+=");
// 1.20: Compound assignment -=
acc = FE::from_uint64(20);
orig = acc;
acc -= FE::from_uint64(7);
CHECK(acc == orig - FE::from_uint64(7), "operator-=");
// 1.21: Compound assignment *=
acc = FE::from_uint64(6);
orig = acc;
acc *= FE::from_uint64(8);
CHECK(acc == orig * FE::from_uint64(8), "operator*=");
// 1.22: Large value arithmetic
FE const large = FE::from_hex("DEADBEEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF01234567");
CHECK(large + zero == large, "large+0==large");
CHECK(large * one == large, "large*1==large");
CHECK(large - large == zero, "large-large==0");
// 1.23: Iterated addition equals multiplication by small scalar
FE const v = FE::from_uint64(17);
FE const sum3 = v + v + v;
FE const mul3 = v * FE::from_uint64(3);
CHECK(sum3 == mul3, "v+v+v == 3*v");
// 1.24: (a+b)^2 = a^2 + 2ab + b^2
FE const lhs = (a + b).square();
FE const rhs = a.square() + FE::from_uint64(2) * a * b + b.square();
CHECK(lhs == rhs, "(a+b)^2 == a^2 + 2ab + b^2");
// 1.25: (a-b)*(a+b) = a^2 - b^2
FE const diff_prod = (a - b) * (a + b);
FE const sq_diff = a.square() - b.square();
CHECK(diff_prod == sq_diff, "(a-b)(a+b) == a^2-b^2");
}
// ============================================================================
// CATEGORY 2: Field Conversions (TestCategory::FieldConversions)
// ============================================================================
static void test_field_conversions() {
std::cout << " [FieldConversions] Field conversion tests..." << '\n';
// 2.1: from_uint64 -> to_hex -> from_hex roundtrip
for (uint64_t const v : {0ULL, 1ULL, 42ULL, 0xFFFFULL, 0xFFFFFFFFULL, 0xFFFFFFFFFFFFFFFFULL}) {
FE const fe = FE::from_uint64(v);
std::string const hex = fe.to_hex();
FE const back = FE::from_hex(hex);
CHECK(fe == back, "uint64->hex->FE roundtrip for " + std::to_string(v));
}
// 2.2: from_bytes <-> to_bytes roundtrip
FE const val = FE::from_hex("09AF57F4F5C1D64C6BEA6D4193C5D9130421F4F078868E5EC00A56E68001136C");
auto const bytes = val.to_bytes();
FE const recovered = FE::from_bytes(bytes);
CHECK(val == recovered, "bytes roundtrip");
// 2.3: to_bytes_into matches to_bytes
std::array<uint8_t, 32> buf{};
val.to_bytes_into(buf.data());
CHECK(buf == bytes, "to_bytes_into matches to_bytes");
// 2.4: from_limbs roundtrip
auto const limbs = val.limbs();
FE const from_l = FE::from_limbs(limbs);
CHECK(val == from_l, "limbs roundtrip");
// 2.5: Zero conversions
FE const z = FE::zero();
CHECK(z.to_hex().find_first_not_of('0') == std::string::npos, "zero to_hex is all zeros");
auto zb = z.to_bytes();
bool all_zero = true;
for (auto b : zb) if (b != 0) all_zero = false;
CHECK(all_zero, "zero to_bytes is all zeros");
// 2.6: One conversions
FE const o = FE::one();
auto ob = o.to_bytes();
CHECK(ob[31] == 1, "one to_bytes last byte is 1");
bool rest_zero = true;
for (std::size_t i = 0; i < 31; ++i) if (ob[i] != 0) rest_zero = false;
CHECK(rest_zero, "one to_bytes prefix is zeros");
// 2.7: Hex case insensitivity
FE const lower = FE::from_hex("deadbeef0000000000000000000000000000000000000000000000000000abcd");
FE const upper = FE::from_hex("DEADBEEF0000000000000000000000000000000000000000000000000000ABCD");
CHECK(lower == upper, "hex case insensitive");
// 2.8: data() <-> from_data roundtrip
auto d = val.data();
FE const from_d = FE::from_data(d);
CHECK(val == from_d, "data() roundtrip");
// 2.9: MidFieldElement memcpy-based conversion roundtrip
FE const fe4 = FE::from_uint64(0x12345678);
auto const mid = secp256k1::fast::toMid(fe4);
FE const back_fe = mid.ToFieldElement();
CHECK(back_fe == fe4, "MidFieldElement conversion roundtrip");
// 2.10: Large hex value
FE const max_fe = FE::from_hex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2E");
std::string const hex_back = max_fe.to_hex();
FE const max_back = FE::from_hex(hex_back);
CHECK(max_fe == max_back, "p-1 hex roundtrip");
}
// ============================================================================
// CATEGORY 3: Field Edge Cases (TestCategory::FieldEdgeCases)
// ============================================================================
static void test_field_edge_cases() {
std::cout << " [FieldEdgeCases] Field edge case tests..." << '\n';
FE const zero = FE::zero();
FE const one = FE::one();
// 3.1: p-1 is the largest valid field element
FE const p_minus_1 = secp256k1_p_minus_1();
// 3.2: (p-1) + 1 = 0 (mod p)
FE const wrap = p_minus_1 + one;
CHECK(wrap == zero, "(p-1)+1 == 0 mod p");
// 3.3: (p-1)^2 mod p
FE const sq = p_minus_1.square();
// (-1)^2 = 1
CHECK(sq == one, "(p-1)^2 == 1 mod p (since p-1 == -1)");
// 3.4: (p-1) * (p-1) = 1
CHECK(p_minus_1 * p_minus_1 == one, "(p-1)*(p-1) == 1");
// 3.5: Inverse of one
CHECK(one.inverse() == one, "1^-^1 == 1");
// 3.6: Inverse of (p-1) = (p-1) since (-1)^-^1 = -1
CHECK(p_minus_1.inverse() == p_minus_1, "(p-1)^-^1 == p-1");
// 3.7: a * a^-^1 = 1 for random-ish values
uint64_t const test_vals[] = {2, 3, 7, 42, 0xDEADBEEF, 0xFFFFFFFFFFFFULL};
for (auto v : test_vals) {
FE const fe = FE::from_uint64(v);
FE const inv = fe.inverse();
CHECK(fe * inv == one, "a*a^-^1==1 for v=" + std::to_string(v));
}
// 3.8: Double inverse = identity
FE const val = FE::from_uint64(1337);
CHECK(val.inverse().inverse() == val, "(a^-^1)^-^1 == a");
// 3.9: Repeated addition wraps at p
// 2*(p-1) = p-2 = -(2) -> 2*(p-1) + 2 = 0
FE const two = FE::from_uint64(2);
FE const double_pm1 = p_minus_1 + p_minus_1;
CHECK(double_pm1 + two == zero, "2*(p-1)+2 == 0");
// 3.10: Equality reflexivity
FE const q = FE::from_uint64(99);
CHECK(q == q, "FE equality reflexive");
CHECK(!(q != q), "FE inequality reflexive");
// 3.11: Inequality
FE const r = FE::from_uint64(100);
CHECK(q != r, "FE inequality for different values");
// 3.12: from_uint64 edge values
CHECK(FE::from_uint64(0) == zero, "from_uint64(0)==zero()");
CHECK(FE::from_uint64(1) == one, "from_uint64(1)==one()");
// 3.13: Inverse-inplace consistency
FE const a = FE::from_uint64(12345);
FE const a_inv = a.inverse();
FE a_copy = a;
a_copy.inverse_inplace();
CHECK(a_inv == a_copy, "inverse_inplace matches inverse");
// 3.14: Large limb values (LE: limb[0] = least significant)
FE const big = FE::from_limbs({0xFFFFFFFEFFFFFC2EULL, 0xFFFFFFFFFFFFFFFFULL,
0xFFFFFFFFFFFFFFFFULL, 0xFFFFFFFFFFFFFFFFULL});
// This should be p-1 itself (verify via addition wrap)
CHECK(big + FE::one() == FE::zero(), "max limbs + 1 == 0 (i.e., it's p-1)");
// 3.15: Subtraction wrapping
FE const small1 = FE::from_uint64(1);
FE const sub_wrap = zero - small1; // should be p-1
CHECK(sub_wrap == p_minus_1, "0-1 == p-1");
// 3.16: Montgomery R constant roundtrip
const auto& R = secp256k1::fast::montgomery::R();
CHECK(R * R.inverse() == one, "R * R^-^1 == 1");
// 3.17: Montgomery from_mont consistency
FE const v2 = FE::from_uint64(42);
FE const mont = v2 * secp256k1::fast::montgomery::R();
FE const back = FE::from_mont(mont);
CHECK(back == v2, "from_mont(a*R) == a");
}
// ============================================================================
// CATEGORY 4: Field Inverse (TestCategory::FieldInverse)
// ============================================================================
static void test_field_inverse() {
std::cout << " [FieldInverse] Field inverse algorithm tests..." << '\n';
FE const one = FE::one();
// Test values
FE const test_values[] = {
FE::from_uint64(2),
FE::from_uint64(3),
FE::from_uint64(7),
FE::from_uint64(0xDEADBEEF),
FE::from_uint64(0xCAFEBABE12345ULL),
FE::from_hex("09AF57F4F5C1D64C6BEA6D4193C5D9130421F4F078868E5EC00A56E68001136C"),
secp256k1_p_minus_1(),
};
// 4.1-4.7: Standard inverse correctness for each value
for (size_t i = 0; i < sizeof(test_values)/sizeof(test_values[0]); ++i) {
FE const inv = test_values[i].inverse();
CHECK(test_values[i] * inv == one,
"inverse correctness #" + std::to_string(i+1));
}
// 4.8-4.21: Each inverse algorithm matches default
// Only run on x86_64 where all algorithms are available
FE const base = FE::from_uint64(12345);
FE const ref_inv = base.inverse();
using InvFn = FE(*)(const FE&);
struct InvAlgo { const char* name; InvFn fn; };
InvAlgo algos[] = {
{"binary", secp256k1::fast::fe_inverse_binary},
{"window4", secp256k1::fast::fe_inverse_window4},
{"addchain", secp256k1::fast::fe_inverse_addchain},
{"eea", secp256k1::fast::fe_inverse_eea},
{"window_naf_v2", secp256k1::fast::fe_inverse_window_naf_v2},
{"hybrid_eea", secp256k1::fast::fe_inverse_hybrid_eea},
{"bos_coster", secp256k1::fast::fe_inverse_bos_coster},
{"ltr_precomp", secp256k1::fast::fe_inverse_ltr_precomp},
{"booth", secp256k1::fast::fe_inverse_booth},
{"strauss", secp256k1::fast::fe_inverse_strauss},
{"kary16", secp256k1::fast::fe_inverse_kary16},
{"fixed_window5", secp256k1::fast::fe_inverse_fixed_window5},
};
for (auto& algo : algos) {
FE const result = algo.fn(base);
CHECK(result == ref_inv,
std::string("inverse algo '") + algo.name + "' matches default");
}
// 4.22: Batch inverse
const int BATCH = 8;
FE batch[BATCH];
FE expected_inv[BATCH];
for (int i = 0; i < BATCH; ++i) {
batch[i] = FE::from_uint64(static_cast<uint64_t>(i) + 2);
expected_inv[i] = batch[i].inverse();
}
FE batch_copy[BATCH];
std::memcpy(batch_copy, batch, sizeof(batch));
secp256k1::fast::fe_batch_inverse(batch_copy, BATCH);
for (int i = 0; i < BATCH; ++i) {
CHECK(batch_copy[i] == expected_inv[i],
"batch_inverse[" + std::to_string(i) + "] matches single inverse");
}
// 4.23: Batch inverse with scratch
std::vector<FE> scratch;
std::memcpy(batch_copy, batch, sizeof(batch));
secp256k1::fast::fe_batch_inverse(batch_copy, BATCH, scratch);
for (int i = 0; i < BATCH; ++i) {
CHECK(batch_copy[i] == expected_inv[i],
"batch_inverse_scratch[" + std::to_string(i) + "] matches");
}
// 4.24: Batch inverse of 1 element
FE single_batch[1] = {FE::from_uint64(7)};
FE const single_expected = single_batch[0].inverse();
secp256k1::fast::fe_batch_inverse(single_batch, 1);
CHECK(single_batch[0] == single_expected, "batch_inverse(n=1)");
}
// ============================================================================
// CATEGORY 5: Field Branchless (TestCategory::FieldBranchless)
// ============================================================================
static void test_field_branchless() {
std::cout << " [FieldBranchless] Branchless operation tests..." << '\n';
using namespace secp256k1::fast;
FE const a = FE::from_uint64(42);
FE const b = FE::from_uint64(99);
FE const zero = FE::zero();
// 5.1: field_cmov flag=true selects a
FE result;
field_cmov(&result, &a, &b, true);
CHECK(result == a, "cmov(true) selects a");
// 5.2: field_cmov flag=false selects b
field_cmov(&result, &a, &b, false);
CHECK(result == b, "cmov(false) selects b");
// 5.3: field_cmovznz nonzero selects a
field_cmovznz(&result, &a, &b, 1);
CHECK(result == a, "cmovznz(1) selects a");
// 5.4: field_cmovznz zero selects b
field_cmovznz(&result, &a, &b, 0);
CHECK(result == b, "cmovznz(0) selects b");
// 5.5: field_cmovznz large nonzero
field_cmovznz(&result, &a, &b, 0xFFFFFFFFFFFFFFFFULL);
CHECK(result == a, "cmovznz(MAX) selects a");
// 5.6: field_select true
FE sel = field_select(a, b, true);
CHECK(sel == a, "select(true)==a");
// 5.7: field_select false
sel = field_select(a, b, false);
CHECK(sel == b, "select(false)==b");
// 5.8: field_is_zero on zero
CHECK(field_is_zero(zero) == 1, "field_is_zero(0)==1");
// 5.9: field_is_zero on nonzero
CHECK(field_is_zero(a) == 0, "field_is_zero(42)==0");
// 5.10: field_eq same
CHECK(field_eq(a, a) == 1, "field_eq(a,a)==1");
// 5.11: field_eq different
CHECK(field_eq(a, b) == 0, "field_eq(a,b)==0");
// 5.12: field_cneg true negates
FE negated;
field_cneg(&negated, a, true);
CHECK(a + negated == zero, "cneg(true) negates");
// 5.13: field_cneg false preserves
FE preserved;
field_cneg(&preserved, a, false);
CHECK(preserved == a, "cneg(false) preserves");
// 5.14: field_cadd true adds
FE cond_sum;
field_cadd(&cond_sum, a, b, true);
CHECK(cond_sum == a + b, "cadd(true) adds");
// 5.15: field_cadd false preserves
field_cadd(&cond_sum, a, b, false);
CHECK(cond_sum == a, "cadd(false) preserves");
// 5.16: field_csub true subtracts
FE cond_diff;
field_csub(&cond_diff, a, b, true);
CHECK(cond_diff == a - b, "csub(true) subtracts");
// 5.17: field_csub false preserves
field_csub(&cond_diff, a, b, false);
CHECK(cond_diff == a, "csub(false) preserves");
// 5.18: cmov self-assignment (a=a)
FE self = a;
field_cmov(&self, &a, &a, true);
CHECK(self == a, "cmov self-assign");
// 5.19: is_zero on one
CHECK(field_is_zero(FE::one()) == 0, "is_zero(one)==0");
// 5.20: Chained select
FE const c = FE::from_uint64(77);
FE const r1 = field_select(a, b, true);
FE const r2 = field_select(r1, c, false);
CHECK(r2 == c, "chained select");
}
// ============================================================================
// CATEGORY 6: Field Optimal Dispatch (TestCategory::FieldOptimal)
// ============================================================================
static void test_field_optimal() {
std::cout << " [FieldOptimal] Optimal representation dispatch..." << '\n';
using namespace secp256k1::fast;
// 6.1: to_optimal -> from_optimal roundtrip
FE const val = FE::from_uint64(42);
auto opt = to_optimal(val);
FE const back = from_optimal(opt);
CHECK(val == back, "to_optimal->from_optimal roundtrip");
// 6.2: Zero roundtrip
FE const z = FE::zero();
CHECK(from_optimal(to_optimal(z)) == z, "zero optimal roundtrip");
// 6.3: One roundtrip
FE const o = FE::one();
CHECK(from_optimal(to_optimal(o)) == o, "one optimal roundtrip");
// 6.4: Large value roundtrip
FE const large = FE::from_hex("DEADBEEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF01234567");
CHECK(from_optimal(to_optimal(large)) == large, "large optimal roundtrip");
// 6.5: p-1 roundtrip
FE const pm1 = secp256k1_p_minus_1();
CHECK(from_optimal(to_optimal(pm1)) == pm1, "p-1 optimal roundtrip");
// 6.6: Tier name is non-null
// cppcheck-suppress nullPointerRedundantCheck
CHECK(kOptimalTierName != nullptr, "optimal tier name exists");
// cppcheck-suppress nullPointerRedundantCheck
CHECK(std::strlen(kOptimalTierName) > 0, "optimal tier name non-empty");
// 6.7: Multiple distinct values
for (uint64_t v = 1; v <= 64; ++v) {
FE const fe = FE::from_uint64(v);
CHECK(from_optimal(to_optimal(fe)) == fe,
"optimal roundtrip #" + std::to_string(v));
}
}
// ============================================================================
// CATEGORY 7: Field ASM (TestCategory::FieldRepresentations)
// ============================================================================
static void test_field_representations() {
std::cout << " [FieldRepresentations] ASM/platform field ops..." << '\n';
using namespace secp256k1::fast;
FE const a = FE::from_uint64(0xDEADBEEF);
FE const b = FE::from_uint64(0xCAFEBABE);
FE const ref_mul = a * b;
FE const ref_sq = a.square();
FE const ref_add = a + b;
// 7.1-7.5: BMI2 implementations (x86_64)
#if defined(__x86_64__) || defined(_M_X64)
FE const bmi2_mul = field_mul_bmi2(a, b);
CHECK(bmi2_mul == ref_mul, "BMI2 mul matches");
FE const bmi2_sq = field_square_bmi2(a);
CHECK(bmi2_sq == ref_sq, "BMI2 square matches");
FE const kara_sq = field_square_karatsuba(a);
CHECK(kara_sq == ref_sq, "Karatsuba square matches");
FE const bmi2_add = field_add_bmi2(a, b);
CHECK(bmi2_add == ref_add, "BMI2 add matches");
FE const bmi2_neg = field_negate_bmi2(a);
CHECK(bmi2_neg == FE::zero() - a, "BMI2 negate matches");
#else
// Skip on non-x86
for (int i = 0; i < 5; ++i) SKIP("BMI2 not available");
#endif
// 7.6-7.10: ARM64 implementations
#if defined(__aarch64__) || defined(_M_ARM64)
FE arm_mul = field_mul_arm64(a, b);
CHECK(arm_mul == ref_mul, "ARM64 mul matches");
FE arm_sq = field_square_arm64(a);
CHECK(arm_sq == ref_sq, "ARM64 square matches");
FE arm_add = field_add_arm64(a, b);
CHECK(arm_add == ref_add, "ARM64 add matches");
FE arm_sub = field_sub_arm64(a, b);
CHECK(arm_sub == a - b, "ARM64 sub matches");
FE arm_neg = field_negate_arm64(a);
CHECK(arm_neg == FE::zero() - a, "ARM64 negate matches");
#else
for (int i = 0; i < 5; ++i) SKIP("ARM64 not available");
#endif
// 7.11-7.15: RISC-V implementations
#ifdef SECP256K1_HAS_RISCV_ASM
FE rv_mul = field_mul_riscv(a, b);
CHECK(rv_mul == ref_mul, "RISC-V mul matches");
FE rv_sq = field_square_riscv(a);
CHECK(rv_sq == ref_sq, "RISC-V square matches");
FE rv_add = field_add_riscv(a, b);
CHECK(rv_add == ref_add, "RISC-V add matches");
FE rv_sub = field_sub_riscv(a, b);
CHECK(rv_sub == a - b, "RISC-V sub matches");
FE rv_neg = field_negate_riscv(a);
CHECK(rv_neg == FE::zero() - a, "RISC-V negate matches");
#else
for (int i = 0; i < 5; ++i) SKIP("RISC-V ASM not available");
#endif
}
// ============================================================================
// CATEGORY 8: Scalar Arithmetic (TestCategory::ScalarArith)
// ============================================================================
static void test_scalar_arith() {
std::cout << " [ScalarArith] Scalar arithmetic tests..." << '\n';
SC const zero = SC::zero();
SC const one = SC::one();
// 8.1: Zero + Zero = Zero
CHECK(zero + zero == zero, "s: 0+0==0");
// 8.2: One + Zero = One
CHECK(one + zero == one, "s: 1+0==1");
// 8.3: One - One = Zero
CHECK(one - one == zero, "s: 1-1==0");
// 8.4: One * One = One
CHECK(one * one == one, "s: 1*1==1");
// 8.5: Commutativity
SC const a = SC::from_uint64(7);
SC const b = SC::from_uint64(13);
CHECK(a + b == b + a, "s: a+b==b+a");
CHECK(a * b == b * a, "s: a*b==b*a");
// 8.6: Associativity
SC const c = SC::from_uint64(19);
CHECK((a + b) + c == a + (b + c), "s: (a+b)+c==a+(b+c)");
CHECK((a * b) * c == a * (b * c), "s: (a*b)*c==a*(b*c)");
// 8.7: Distributive
CHECK(a * (b + c) == a * b + a * c, "s: a*(b+c)==a*b+a*c");
// 8.8: Multiplication by zero
CHECK(a * zero == zero, "s: a*0==0");
// 8.9: Negate
SC const neg_a = a.negate();
CHECK(a + neg_a == zero, "s: a+(-a)==0");
// 8.10: Double negate
CHECK(a.negate().negate() == a, "s: -(-a)==a");
// 8.11: Negate zero
CHECK(zero.negate() == zero, "s: -0==0");
// 8.12: is_zero
CHECK(zero.is_zero(), "s: zero.is_zero()");
CHECK(!one.is_zero(), "s: !one.is_zero()");
CHECK(!a.is_zero(), "s: !7.is_zero()");
// 8.13: Inverse
SC const inv_a = a.inverse();
CHECK(a * inv_a == one, "s: a*a^-^1==1");
// 8.14: Double inverse
CHECK(a.inverse().inverse() == a, "s: (a^-^1)^-^1==a");
// 8.15: Compound assignment
SC acc = SC::from_uint64(10);
SC orig = acc;
acc += SC::from_uint64(5);
CHECK(acc == orig + SC::from_uint64(5), "s: +=");
acc = SC::from_uint64(20);
orig = acc;
acc -= SC::from_uint64(7);
CHECK(acc == orig - SC::from_uint64(7), "s: -=");
acc = SC::from_uint64(6);
orig = acc;
acc *= SC::from_uint64(8);
CHECK(acc == orig * SC::from_uint64(8), "s: *=");
// 8.16: is_even
CHECK(SC::from_uint64(2).is_even(), "s: 2.is_even()");
CHECK(SC::from_uint64(0).is_even(), "s: 0.is_even()");
CHECK(!SC::from_uint64(1).is_even(), "s: !1.is_even()");
CHECK(!SC::from_uint64(3).is_even(), "s: !3.is_even()");
// 8.17: Exhaustive small-range
unsigned checks = 0;
for (unsigned i = 0; i <= 64; ++i) {
for (unsigned j = 0; j <= 64; ++j) {
SC const si = SC::from_uint64(i);
SC const sj = SC::from_uint64(j);
CHECK(si + sj == SC::from_uint64(i + j),
"s: " + std::to_string(i) + "+" + std::to_string(j));
CHECK(si * sj == SC::from_uint64(static_cast<uint64_t>(i) * j),
"s: " + std::to_string(i) + "*" + std::to_string(j));
++checks;
}
}
std::cout << " " << checks << " small-range pairs verified" << '\n';
}
// ============================================================================
// CATEGORY 9: Scalar Conversions (TestCategory::ScalarConversions)
// ============================================================================
static void test_scalar_conversions() {
std::cout << " [ScalarConversions] Scalar conversion tests..." << '\n';
// 9.1: from_uint64 -> to_hex -> from_hex roundtrip
for (uint64_t const v : {0ULL, 1ULL, 42ULL, 0xFFFFULL, 0xFFFFFFFFULL, 0xFFFFFFFFFFFFFFFFULL}) {
SC const s = SC::from_uint64(v);
std::string const hex = s.to_hex();
SC const back = SC::from_hex(hex);
CHECK(s == back, "s: uint64->hex->SC for " + std::to_string(v));
}
// 9.2: from_bytes <-> to_bytes
SC const val = SC::from_hex("DEADBEEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF01234567");
auto const bytes = val.to_bytes();
SC const recovered = SC::from_bytes(bytes);
CHECK(val == recovered, "s: bytes roundtrip");
// 9.3: from_limbs roundtrip
auto const limbs = val.limbs();
SC const from_l = SC::from_limbs(limbs);
CHECK(val == from_l, "s: limbs roundtrip");
// 9.4: Zero conversions
SC const z = SC::zero();
auto zb = z.to_bytes();
bool all_zero = true;
for (auto b : zb) if (b != 0) all_zero = false;
CHECK(all_zero, "s: zero bytes all zeros");
// 9.5: bit() extraction
SC const s3 = SC::from_uint64(3); // binary: ...011
CHECK(s3.bit(0) == 1, "s: bit(0) of 3");
CHECK(s3.bit(1) == 1, "s: bit(1) of 3");
CHECK(s3.bit(2) == 0, "s: bit(2) of 3");
SC const s5 = SC::from_uint64(5); // binary: ...101
CHECK(s5.bit(0) == 1, "s: bit(0) of 5");
CHECK(s5.bit(1) == 0, "s: bit(1) of 5");
CHECK(s5.bit(2) == 1, "s: bit(2) of 5");
// 9.6: data() <-> from_data
auto d = val.data();
SC const from_d = SC::from_data(d);
CHECK(val == from_d, "s: data() roundtrip");
}
// ============================================================================
// CATEGORY 10: Scalar Edge Cases (TestCategory::ScalarEdgeCases)
// ============================================================================
static void test_scalar_edge_cases() {
std::cout << " [ScalarEdgeCases] Scalar edge case tests..." << '\n';
SC const zero = SC::zero();
SC const one = SC::one();
SC const n = secp256k1_n();
// 10.1: n == 0 (mod n)
// Scalar should wrap: n becomes 0
// Note: from_hex for n may return zero if reduction is applied
SC const n_minus_1 = n - one;
// 10.2: (n-1) + 1 = 0
SC const wrap = n_minus_1 + one;
CHECK(wrap == zero, "s: (n-1)+1 == 0");
// 10.3: (n-1) * (n-1) = 1 (since n-1 == -1, (-1)^2 = 1)
CHECK(n_minus_1 * n_minus_1 == one, "s: (n-1)^2==1");
// 10.4: Inverse of (n-1) = (n-1)
CHECK(n_minus_1.inverse() == n_minus_1, "s: (n-1)^-^1==(n-1)");
// 10.5: Negate of (n-1) = 1
CHECK(n_minus_1.negate() == one, "s: -(n-1)==1");
// 10.6: Negate of 1 = (n-1)
CHECK(one.negate() == n_minus_1, "s: -1==(n-1)");
// 10.7: 2 * (n-1) + 2 = 0
SC const two = SC::from_uint64(2);
SC const double_nm1 = n_minus_1 + n_minus_1;
CHECK(double_nm1 + two == zero, "s: 2*(n-1)+2==0");
// 10.8: n-1 is even (n = ...41 is odd, so n-1 = ...40 is even)
CHECK(n_minus_1.is_even(), "s: n-1 is even");
// 10.9: Various bit positions
for (unsigned bit = 0; bit < 256; ++bit) {
uint8_t const b = one.bit(bit);
if (bit == 0) { CHECK(b == 1, "s: bit(0) of 1");
} else { CHECK(b == 0, "s: bit(" + std::to_string(bit) + ") of 1 is 0");
}
}
}
// ============================================================================
// CATEGORY 11: Scalar NAF/wNAF (TestCategory::ScalarEncoding)
// ============================================================================
static void test_scalar_encoding() {
std::cout << " [ScalarEncoding] NAF/wNAF encoding tests..." << '\n';
// 11.1: NAF of small values
SC const s7 = SC::from_uint64(7); // 111 -> NAF: 100(-1)
auto naf = s7.to_naf();
// Reconstruct from NAF and verify it equals original
SC reconstructed = SC::zero();
SC power = SC::one();
for (size_t i = 0; i < naf.size(); ++i) {
if (naf[i] == 1) { reconstructed = reconstructed + power;
} else if (naf[i] == -1) { reconstructed = reconstructed - power;
}
power = power + power; // power *= 2
}
CHECK(reconstructed == s7, "NAF(7) reconstructs to 7");
// 11.2: NAF property -- no two consecutive nonzero digits
bool naf_valid = true;
for (size_t i = 0; i + 1 < naf.size(); ++i) {
if (naf[i] != 0 && naf[i+1] != 0) { naf_valid = false; break; }
}
CHECK(naf_valid, "NAF(7) has no consecutive nonzero digits");
// 11.3: wNAF reconstruction for various widths
for (unsigned w = 2; w <= 6; ++w) {
SC const s42 = SC::from_uint64(42);
auto wnaf = s42.to_wnaf(w);
SC rec = SC::zero();
SC pw = SC::one();
for (size_t i = 0; i < wnaf.size(); ++i) {
if (wnaf[i] > 0) {
SC const digit = SC::from_uint64(static_cast<uint64_t>(wnaf[i]));
rec = rec + digit * pw;
} else if (wnaf[i] < 0) {
SC const digit = SC::from_uint64(static_cast<uint64_t>(-wnaf[i]));
rec = rec - digit * pw;
}
pw = pw + pw;
}
CHECK(rec == s42, "wNAF(w=" + std::to_string(w) + ",42) reconstructs");
}
// 11.4: NAF of zero
auto naf_zero = SC::zero().to_naf();
bool all_zero = true;
for (auto d : naf_zero) if (d != 0) all_zero = false;
CHECK(all_zero, "NAF(0) is all zeros");
// 11.5: NAF of one
auto naf_one = SC::one().to_naf();
CHECK(naf_one.size() > 0 && naf_one[0] == 1, "NAF(1)[0] == 1");
// 11.6: Large scalar NAF
SC const large = SC::from_hex("DEADBEEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF01234567");
auto naf_large = large.to_naf();
SC rec_large = SC::zero();
SC pw_large = SC::one();
for (size_t i = 0; i < naf_large.size(); ++i) {
if (naf_large[i] == 1) { rec_large = rec_large + pw_large;
} else if (naf_large[i] == -1) { rec_large = rec_large - pw_large;
}
pw_large = pw_large + pw_large;
}
CHECK(rec_large == large, "NAF(large) reconstructs");
// 11.7: wNAF digits are odd
auto wnaf5 = SC::from_uint64(255).to_wnaf(5);
bool all_odd_or_zero = true;
for (auto d : wnaf5) {
if (d != 0 && (d % 2 == 0)) all_odd_or_zero = false;
}
CHECK(all_odd_or_zero, "wNAF digits are odd or zero");
// 11.8: compute_wnaf_into test
SC const s100 = SC::from_uint64(100);
int32_t wnaf_buf[257] = {};
std::size_t wnaf_len = 0;
secp256k1::fast::compute_wnaf_into(s100, 4, wnaf_buf, 257, wnaf_len);
CHECK(wnaf_len > 0, "compute_wnaf_into produces nonzero length");
// Reconstruct
SC rec_wnaf = SC::zero();
SC pw_wnaf = SC::one();
for (std::size_t i = 0; i < wnaf_len; ++i) {
if (wnaf_buf[i] > 0) {
rec_wnaf = rec_wnaf + SC::from_uint64(static_cast<uint64_t>(wnaf_buf[i])) * pw_wnaf;
} else if (wnaf_buf[i] < 0) {
rec_wnaf = rec_wnaf - SC::from_uint64(static_cast<uint64_t>(-wnaf_buf[i])) * pw_wnaf;
}
pw_wnaf = pw_wnaf + pw_wnaf;
}
CHECK(rec_wnaf == s100, "compute_wnaf_into reconstructs 100");
}
// ============================================================================
// CATEGORY 12: Point Basic (TestCategory::PointBasic)
// ============================================================================
static void test_point_basic() {
std::cout << " [PointBasic] Point basic operations..." << '\n';
PT const G = PT::generator();
PT const O = PT::infinity();
// 12.1: Generator is on curve
FE const gx = G.x();
FE const gy = G.y();
CHECK(gy.square() == gx.square() * gx + FE::from_uint64(7), "G on curve");
// 12.2: Generator is not infinity
CHECK(!G.is_infinity(), "G != O");
// 12.3: Infinity is infinity
CHECK(O.is_infinity(), "O.is_infinity()");
// 12.4: P + O = P
CHECK(pt_eq(G.add(O), G), "G+O==G");
// 12.5: O + P = P
CHECK(pt_eq(O.add(G), G), "O+G==G");
// 12.6: O + O = O
CHECK(O.add(O).is_infinity(), "O+O==O");
// 12.7: P + (-P) = O
PT const negG = G.negate();
CHECK(G.add(negG).is_infinity(), "G+(-G)==O");
// 12.8: Commutativity
PT const twoG = G.add(G);
PT const threeG = twoG.add(G);
PT const threeG_alt = G.add(twoG);
CHECK(pt_eq(threeG, threeG_alt), "2G+G == G+2G");
// 12.9: Associativity
PT const fourG = threeG.add(G);
PT const fourG_alt = twoG.add(twoG);
CHECK(pt_eq(fourG, fourG_alt), "(3G+G) == (2G+2G)");
// 12.10: Doubling = addition
CHECK(pt_eq(G.dbl(), G.add(G)), "dbl(G)==G+G");
// 12.11: 2*O = O
CHECK(O.dbl().is_infinity(), "dbl(O)==O");
// 12.12: Negation of infinity
CHECK(O.negate().is_infinity(), "-O==O");
// 12.13: Double negation
PT const P = G.add(G).add(G); // 3G
CHECK(pt_eq(P.negate().negate(), P), "-(-P)==P");
// 12.14: Generator known x-coordinate first 4 bytes
auto comp = G.to_compressed();
CHECK(comp[0] == 0x02 || comp[0] == 0x03, "G compressed prefix");
// 12.15: Negate changes y, keeps x
FE const px = P.x();
FE const py = P.y();
PT const neg_p = P.negate();
CHECK(neg_p.x() == px, "negate preserves x");
CHECK(neg_p.y() != py, "negate changes y");
// 12.16: Add(P, P) == dbl(P)
for (unsigned i = 1; i <= 16; ++i) {
PT const pi = G.scalar_mul(SC::from_uint64(i));
CHECK(pt_eq(pi.add(pi), pi.dbl()), "add(P,P)==dbl(P) for " + std::to_string(i) + "G");
}
// 12.17: from_affine <-> x(),y()
FE const ax = G.x();
FE const ay = G.y();
PT const from_aff = PT::from_affine(ax, ay);
CHECK(pt_eq(from_aff, G), "from_affine(G.x, G.y)==G");
// 12.18: from_hex <-> to_hex
std::string const xh = ax.to_hex();
std::string const yh = ay.to_hex();
PT const from_h = PT::from_hex(xh, yh);
CHECK(pt_eq(from_h, G), "from_hex roundtrip");
// 12.19: Closure -- iterated addition stays on curve
PT acc = G;
for (int i = 0; i < 100; ++i) {
FE const xi = acc.x();
FE const yi = acc.y();
CHECK(yi.square() == xi.square() * xi + FE::from_uint64(7),
"on-curve at iteration " + std::to_string(i));
acc = acc.add(G);
}
}
// ============================================================================
// CATEGORY 13: Point Scalar Mul (TestCategory::PointScalarMul)
// ============================================================================
static void test_point_scalar_mul() {
std::cout << " [PointScalarMul] Scalar multiplication tests..." << '\n';
PT const G = PT::generator();
// 13.1: 0*G = O
CHECK(G.scalar_mul(SC::zero()).is_infinity(), "0*G==O");
// 13.2: 1*G = G
CHECK(pt_eq(G.scalar_mul(SC::one()), G), "1*G==G");
// 13.3: 2*G = G+G
CHECK(pt_eq(G.scalar_mul(SC::from_uint64(2)), G.add(G)), "2*G==G+G");
// 13.4: n*G = O
SC const n = secp256k1_n();
CHECK(G.scalar_mul(n).is_infinity(), "n*G==O");
// 13.5: (n-1)*G = -G
SC const n_minus_1 = n - SC::one();
CHECK(pt_eq(G.scalar_mul(n_minus_1), G.negate()), "(n-1)*G==-G");
// 13.6: Consistency with iterated addition for k=1..256
PT iter = G;
for (unsigned k = 1; k <= 256; ++k) {
PT const mul_result = G.scalar_mul(SC::from_uint64(k));
CHECK(pt_eq(mul_result, iter),
"scalar_mul(" + std::to_string(k) + ") == iterated");
iter = iter.add(G);
}
// 13.7: Scalar associativity -- k*(l*G) = (k*l)*G
const uint64_t pairs[][2] = {
{2, 3}, {3, 5}, {7, 11}, {13, 17}, {100, 200},
{255, 255}, {1000, 999}, {12345, 6789}
};
for (auto& [k, l] : pairs) {
SC const sk = SC::from_uint64(k);
SC const sl = SC::from_uint64(l);
PT const lG = G.scalar_mul(sl);
PT const klG = lG.scalar_mul(sk);
PT const klG_direct = G.scalar_mul(sk * sl);
CHECK(pt_eq(klG, klG_direct),
"k*(l*G)==(k*l)*G for k=" + std::to_string(k) + ",l=" + std::to_string(l));
}
// 13.8: k*G + (-k)*G = O
for (unsigned k = 1; k <= 64; ++k) {
SC const s = SC::from_uint64(k);
PT const kG = G.scalar_mul(s);
PT const nkG = G.scalar_mul(s.negate());
CHECK(kG.add(nkG).is_infinity(),
"k*G+(-k)*G==O for k=" + std::to_string(k));
}
// 13.9: Large scalar
SC const large = SC::from_hex("DEADBEEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF01234567");
PT const large_mul = G.scalar_mul(large);
CHECK(!large_mul.is_infinity(), "large*G is not O");
FE const lx = large_mul.x();
FE const ly = large_mul.y();
CHECK(ly.square() == lx.square() * lx + FE::from_uint64(7), "large*G on curve");
// 13.10: Scalar mul of non-generator points
PT const twoG = G.add(G);
PT const result = twoG.scalar_mul(SC::from_uint64(3));
PT const expected = G.scalar_mul(SC::from_uint64(6));
CHECK(pt_eq(result, expected), "3*(2G)==6G");
// 13.11: Scalar mul of infinity
CHECK(PT::infinity().scalar_mul(SC::from_uint64(42)).is_infinity(), "42*O==O");
}
// ============================================================================
// CATEGORY 14: Point In-Place (TestCategory::PointInplace)
// ============================================================================
static void test_point_inplace() {
std::cout << " [PointInplace] In-place operations..." << '\n';
PT const G = PT::generator();
// 14.1: next_inplace vs add(G)
PT p1 = G, p2 = G;
for (int i = 0; i < 64; ++i) {
PT const immutable_next = p1.add(G);
p2.next_inplace();
CHECK(pt_eq(immutable_next, p2), "next_inplace #" + std::to_string(i));
p1 = immutable_next;
}
// 14.2: prev_inplace (reverse of next)
PT const p3 = G.scalar_mul(SC::from_uint64(100));
PT p4 = p3;
p4.next_inplace();
p4.prev_inplace();
CHECK(pt_eq(p3, p4), "next then prev is identity");
// 14.3: dbl_inplace
PT p5 = G;
PT p6 = G;
for (int i = 0; i < 32; ++i) {
PT const expected_dbl = p5.dbl();
p6 = p5;
p6.dbl_inplace();
CHECK(pt_eq(expected_dbl, p6), "dbl_inplace #" + std::to_string(i));
p5 = expected_dbl;
}
// 14.4: negate_inplace
PT const p7 = G.scalar_mul(SC::from_uint64(42));
PT const neg_immutable = p7.negate();
PT p8 = p7;
p8.negate_inplace();
CHECK(pt_eq(neg_immutable, p8), "negate_inplace");
// 14.5: add_inplace
PT p9 = G;
PT const p10 = G.add(G);
PT const expected_add = p9.add(p10);
p9.add_inplace(p10);
CHECK(pt_eq(expected_add, p9), "add_inplace");
// 14.6: sub_inplace
PT const p11 = G.scalar_mul(SC::from_uint64(50));
PT const p12 = G.scalar_mul(SC::from_uint64(20));
PT const expected_sub = p11.add(p12.negate());
PT p13 = p11;
p13.sub_inplace(p12);
CHECK(pt_eq(expected_sub, p13), "sub_inplace");
// 14.7: Double negate_inplace
PT const p14 = G.scalar_mul(SC::from_uint64(7));
PT p15 = p14;
p15.negate_inplace();
p15.negate_inplace();
CHECK(pt_eq(p14, p15), "double negate_inplace");
// 14.8: add_mixed_inplace (affine RHS where z=1)
FE const ax = G.x();
FE const ay = G.y();
PT const p16 = G.scalar_mul(SC::from_uint64(5)); // 5G
PT p17 = p16;
p17.add_mixed_inplace(ax, ay); // 5G + G = 6G
PT const expected_6g = G.scalar_mul(SC::from_uint64(6));
CHECK(pt_eq(p17, expected_6g), "add_mixed_inplace == 5G+G=6G");
// 14.9: sub_mixed_inplace
PT p18 = G.scalar_mul(SC::from_uint64(10));
p18.sub_mixed_inplace(ax, ay); // 10G - G = 9G
PT const expected_9g = G.scalar_mul(SC::from_uint64(9));
CHECK(pt_eq(p18, expected_9g), "sub_mixed_inplace == 10G-G=9G");
}
// ============================================================================
// CATEGORY 15: Point Precomputed (TestCategory::PointPrecomputed)
// ============================================================================
static void test_point_precomputed() {
std::cout << " [PointPrecomputed] Precomputed scalar mul..." << '\n';
PT const G = PT::generator();
// 15.1: scalar_mul_precomputed_k matches scalar_mul
for (unsigned k = 1; k <= 32; ++k) {
SC const s = SC::from_uint64(k);
PT const fast_result = G.scalar_mul_precomputed_k(s);
PT const ref_result = G.scalar_mul(s);
CHECK(pt_eq(fast_result, ref_result),
"precomputed_k(" + std::to_string(k) + ")");
}
// 15.2: KPlan
SC const k = SC::from_uint64(42);
auto plan = secp256k1::fast::KPlan::from_scalar(k);
PT const plan_result = G.scalar_mul_with_plan(plan);
PT const ref = G.scalar_mul(k);
CHECK(pt_eq(plan_result, ref), "KPlan(42) matches");
// 15.3: KPlan with small-medium scalar
SC const med_k = SC::from_uint64(99999);
auto med_plan = secp256k1::fast::KPlan::from_scalar(med_k);
PT const med_plan_result = G.scalar_mul_with_plan(med_plan);
PT const med_ref = G.scalar_mul(med_k);
CHECK(pt_eq(med_plan_result, med_ref), "KPlan(99999) matches");
// 15.4: scalar_mul_predecomposed
auto decomp = secp256k1::fast::split_scalar_glv(k);
PT const predecomp_result = G.scalar_mul_predecomposed(
decomp.k1, decomp.k2, decomp.neg1, decomp.neg2);
CHECK(pt_eq(predecomp_result, ref), "predecomposed(42) matches");
// 15.5: scalar_mul_precomputed_wnaf
PT const wnaf_result = G.scalar_mul_precomputed_wnaf(
plan.wnaf1, plan.wnaf2, plan.neg1, plan.neg2);
CHECK(pt_eq(wnaf_result, ref), "precomputed_wnaf(42) matches");
// 15.6: Multiple KPlan values
for (uint64_t kv = 1; kv <= 1024; kv += 100) {
SC const sv = SC::from_uint64(kv);
auto pl = secp256k1::fast::KPlan::from_scalar(sv);
PT const res = G.scalar_mul_with_plan(pl);
PT const exp = G.scalar_mul(sv);
CHECK(pt_eq(res, exp), "KPlan(" + std::to_string(kv) + ") matches");
}
// 15.7: Multiple different plans
for (unsigned v = 1; v <= 8; ++v) {
SC const sv = SC::from_uint64(v * 1000 + 7);
auto pl = secp256k1::fast::KPlan::from_scalar(sv);
PT const res = G.scalar_mul_with_plan(pl);
PT const exp = G.scalar_mul(sv);
CHECK(pt_eq(res, exp), "KPlan #" + std::to_string(v));
}
}
// ============================================================================
// CATEGORY 16: Point Serialization (TestCategory::PointSerialization)
// ============================================================================
static void test_point_serialization() {
std::cout << " [PointSerialization] Point serialization tests..." << '\n';
PT const G = PT::generator();
// 16.1: to_compressed returns 33 bytes with 02/03 prefix
auto comp = G.to_compressed();
CHECK(comp.size() == 33, "compressed size == 33");
CHECK(comp[0] == 0x02 || comp[0] == 0x03, "compressed prefix");
// 16.2: to_uncompressed returns 65 bytes with 04 prefix
auto uncomp = G.to_uncompressed();
CHECK(uncomp.size() == 65, "uncompressed size == 65");
CHECK(uncomp[0] == 0x04, "uncompressed prefix");
// 16.3: Compressed x matches uncompressed x
bool x_match = true;
for (std::size_t i = 0; i < 32; ++i) {
if (comp[1 + i] != uncomp[1 + i]) x_match = false;
}
CHECK(x_match, "compressed x == uncompressed x");
// 16.4: Multiple points serialization
PT P = G;
for (std::size_t i = 0; i < 32; ++i) {
auto c = P.to_compressed();
auto u = P.to_uncompressed();
CHECK(c[0] == 0x02 || c[0] == 0x03, "prefix #" + std::to_string(i));
CHECK(u[0] == 0x04, "uncomp prefix #" + std::to_string(i));
P = P.add(G);
}
// 16.5: x_first_half / x_second_half
auto first = G.x_first_half();
auto second = G.x_second_half();
auto full_bytes = G.x().to_bytes();
bool first_match = true, second_match = true;
for (std::size_t i = 0; i < 16; ++i) {
if (first[i] != full_bytes[i]) first_match = false;
if (second[i] != full_bytes[16 + i]) second_match = false;
}
CHECK(first_match, "x_first_half matches");
CHECK(second_match, "x_second_half matches");
// 16.6: Serialization of negated point -- same x
PT const neg = G.negate();
auto comp_neg = neg.to_compressed();
bool same_x = true;
for (std::size_t i = 1; i < 33; ++i) {
if (comp[i] != comp_neg[i]) same_x = false;
}
CHECK(same_x, "negation: same x in compressed");
CHECK(comp[0] != comp_neg[0], "negation: different parity prefix");
// 16.7: from_jacobian_coords
FE const jx = G.X();
FE const jy = G.Y();
FE const jz = G.z();
PT const from_j = PT::from_jacobian_coords(jx, jy, jz, false);
CHECK(pt_eq(from_j, G), "from_jacobian_coords roundtrip");
}
// ============================================================================
// CATEGORY 17: Point Edge Cases (TestCategory::PointEdgeCases)
// ============================================================================
static void test_point_edge_cases() {
std::cout << " [PointEdgeCases] Point edge cases..." << '\n';
PT const G = PT::generator();
PT const O = PT::infinity();
// 17.1: n*G = O
SC const n = secp256k1_n();
CHECK(G.scalar_mul(n).is_infinity(), "n*G==O");
// 17.2: 2*O = O
CHECK(O.dbl().is_infinity(), "2*O==O");
// 17.3: k*O = O for any k
CHECK(O.scalar_mul(SC::from_uint64(42)).is_infinity(), "42*O==O");
CHECK(O.scalar_mul(SC::from_uint64(1)).is_infinity(), "1*O==O");
CHECK(O.scalar_mul(n).is_infinity(), "n*O==O");
// 17.4: P + O = P for various P
for (unsigned k = 1; k <= 16; ++k) {
PT const P = G.scalar_mul(SC::from_uint64(k));
CHECK(pt_eq(P.add(O), P), "P+O==P for " + std::to_string(k) + "G");
CHECK(pt_eq(O.add(P), P), "O+P==P for " + std::to_string(k) + "G");
}
// 17.5: P + (-P) = O for various P
for (unsigned k = 1; k <= 16; ++k) {
PT const P = G.scalar_mul(SC::from_uint64(k));
CHECK(P.add(P.negate()).is_infinity(),
"P+(-P)==O for " + std::to_string(k) + "G");
}
// 17.6: Order of 2G
PT const twoG = G.scalar_mul(SC::from_uint64(2));
// n * (2G) should be O since order of group divides n
CHECK(twoG.scalar_mul(n).is_infinity(), "n*(2G)==O");
// 17.7: Iterated addition = scalar mul for powers of 2
PT P = G;
for (int pow2 = 1; pow2 <= 16; ++pow2) {
P = P.dbl();
uint64_t const expected_k = 1ULL << pow2;
PT const mul_result = G.scalar_mul(SC::from_uint64(expected_k));
CHECK(pt_eq(P, mul_result),
"2^" + std::to_string(pow2) + "*G via doubling");
}
// 17.8: Commutativity exhaustive small range
std::vector<PT> pts(8);
for (std::size_t i = 0; i < 8; ++i) pts[i] = G.scalar_mul(SC::from_uint64(i + 1));
for (std::size_t i = 0; i < 8; ++i) {
for (std::size_t j = i; j < 8; ++j) {
CHECK(pt_eq(pts[i].add(pts[j]), pts[j].add(pts[i])),
"commutative (" + std::to_string(i+1) + "G," + std::to_string(j+1) + "G)");
}
}
// 17.9: Associativity
for (std::size_t i = 0; i < 4; ++i) {
for (std::size_t j = i+1; j < 6; ++j) {
for (std::size_t k = j+1; k < 8; ++k) {
PT const lhs = pts[i].add(pts[j]).add(pts[k]);
PT const rhs = pts[i].add(pts[j].add(pts[k]));
CHECK(pt_eq(lhs, rhs),
"associative (" + std::to_string(i+1) + "," +
std::to_string(j+1) + "," + std::to_string(k+1) + ")");
}
}
}
}
// ============================================================================
// CATEGORY 18: CT Primitives (TestCategory::CTOps)
// ============================================================================
static void test_ct_ops() {
std::cout << " [CTOps] Constant-time primitive tests..." << '\n';
using namespace secp256k1::ct;
// 18.1: value_barrier (should not change value)
uint64_t v = 0xDEADBEEFCAFEBABEULL;
uint64_t const v_orig = v;
value_barrier(v);
CHECK(v == v_orig, "value_barrier preserves value");
// 18.2: is_zero_mask
CHECK(is_zero_mask(0) == 0xFFFFFFFFFFFFFFFFULL, "is_zero_mask(0)==all-ones");
CHECK(is_zero_mask(1) == 0, "is_zero_mask(1)==0");
CHECK(is_zero_mask(0xFFFFFFFFFFFFFFFFULL) == 0, "is_zero_mask(MAX)==0");
// 18.3: is_nonzero_mask
CHECK(is_nonzero_mask(0) == 0, "is_nonzero_mask(0)==0");
CHECK(is_nonzero_mask(1) == 0xFFFFFFFFFFFFFFFFULL, "is_nonzero_mask(1)==all-ones");
// 18.4: eq_mask
CHECK(eq_mask(42, 42) == 0xFFFFFFFFFFFFFFFFULL, "eq_mask(42,42)==all-ones");
CHECK(eq_mask(42, 43) == 0, "eq_mask(42,43)==0");
// 18.5: bool_to_mask
CHECK(bool_to_mask(true) == 0xFFFFFFFFFFFFFFFFULL, "bool_to_mask(true)==all-ones");
CHECK(bool_to_mask(false) == 0, "bool_to_mask(false)==0");
// 18.6: lt_mask
CHECK(lt_mask(5, 10) == 0xFFFFFFFFFFFFFFFFULL, "lt_mask(5,10)==all-ones");
CHECK(lt_mask(10, 5) == 0, "lt_mask(10,5)==0");
CHECK(lt_mask(5, 5) == 0, "lt_mask(5,5)==0");
// 18.7: cmov64
uint64_t dst = 100;
uint64_t const src = 200;
cmov64(&dst, &src, 0xFFFFFFFFFFFFFFFFULL);
CHECK(dst == 200, "cmov64 mask=all-ones copies");
dst = 100;
cmov64(&dst, &src, 0);
CHECK(dst == 100, "cmov64 mask=0 preserves");
// 18.8: cmov256
uint64_t arr_dst[4] = {1, 2, 3, 4};
uint64_t arr_src[4] = {10, 20, 30, 40};
cmov256(arr_dst, arr_src, 0xFFFFFFFFFFFFFFFFULL);
CHECK(arr_dst[0]==10 && arr_dst[1]==20 && arr_dst[2]==30 && arr_dst[3]==40,
"cmov256 mask=all-ones");
uint64_t arr_dst2[4] = {1, 2, 3, 4};
cmov256(arr_dst2, arr_src, 0);
CHECK(arr_dst2[0]==1 && arr_dst2[1]==2 && arr_dst2[2]==3 && arr_dst2[3]==4,
"cmov256 mask=0");
// 18.9: cswap256
uint64_t swap_a[4] = {1, 2, 3, 4};
uint64_t swap_b[4] = {10, 20, 30, 40};
cswap256(swap_a, swap_b, 0xFFFFFFFFFFFFFFFFULL);
CHECK(swap_a[0]==10 && swap_a[1]==20 && swap_a[2]==30 && swap_a[3]==40, "cswap256 swapped a");
CHECK(swap_b[0]==1 && swap_b[1]==2 && swap_b[2]==3 && swap_b[3]==4, "cswap256 swapped b");
uint64_t noswap_a[4] = {1, 2, 3, 4};
uint64_t noswap_b[4] = {10, 20, 30, 40};
cswap256(noswap_a, noswap_b, 0);
CHECK(noswap_a[0]==1 && noswap_b[0]==10, "cswap256 mask=0 no swap");
// 18.10: ct_select
CHECK(ct_select(42, 99, 0xFFFFFFFFFFFFFFFFULL) == 42, "ct_select mask=ones");
CHECK(ct_select(42, 99, 0) == 99, "ct_select mask=0");
// 18.11: ct_equal (byte-level)
uint8_t buf1[4] = {1, 2, 3, 4};
uint8_t buf2[4] = {1, 2, 3, 4};
uint8_t buf3[4] = {1, 2, 3, 5};
CHECK(ct_equal(buf1, buf2, 4), "ct_equal same");
CHECK(!ct_equal(buf1, buf3, 4), "ct_equal different");
// 18.12: ct_is_zero
uint8_t zeros[8] = {};
uint8_t nonzeros[8] = {0, 0, 0, 0, 0, 0, 0, 1};
CHECK(ct_is_zero(zeros, 8), "ct_is_zero for zeros");
CHECK(!ct_is_zero(nonzeros, 8), "ct_is_zero for nonzeros");
// 18.13: ct_memcpy_if
uint8_t dest[4] = {0, 0, 0, 0};
uint8_t source[4] = {5, 6, 7, 8};
ct_memcpy_if(dest, source, 4, true);
CHECK(dest[0]==5 && dest[1]==6 && dest[2]==7 && dest[3]==8, "ct_memcpy_if(true) copies");
uint8_t dest2[4] = {1, 2, 3, 4};
ct_memcpy_if(dest2, source, 4, false);
CHECK(dest2[0]==1 && dest2[1]==2 && dest2[2]==3 && dest2[3]==4, "ct_memcpy_if(false) preserves");
// 18.14: ct_memswap_if
uint8_t sa[4] = {1, 2, 3, 4};
uint8_t sb[4] = {5, 6, 7, 8};
ct_memswap_if(sa, sb, 4, true);
CHECK(sa[0]==5 && sb[0]==1, "ct_memswap_if(true) swaps");
uint8_t sa2[4] = {1, 2, 3, 4};
uint8_t sb2[4] = {5, 6, 7, 8};
ct_memswap_if(sa2, sb2, 4, false);
CHECK(sa2[0]==1 && sb2[0]==5, "ct_memswap_if(false) no swap");
}
// ============================================================================
// CATEGORY 19: CT Field (TestCategory::CTField)
// ============================================================================
static void test_ct_field() {
std::cout << " [CTField] CT field operations..." << '\n';
namespace ctf = secp256k1::ct;
FE const a = FE::from_uint64(42);
FE const b = FE::from_uint64(99);
FE const zero = FE::zero();
FE const one = FE::one();
// 19.1-19.5: CT arithmetic matches fast arithmetic
CHECK(ctf::field_add(a, b) == a + b, "ct field_add");
CHECK(ctf::field_sub(a, b) == a - b, "ct field_sub");
CHECK(ctf::field_mul(a, b) == a * b, "ct field_mul");
CHECK(ctf::field_sqr(a) == a.square(), "ct field_sqr");
CHECK(ctf::field_neg(a) == zero - a, "ct field_neg");
// 19.6: CT inverse
FE const ct_inv = ctf::field_inv(a);
CHECK(a * ct_inv == one, "ct field_inv correct");
// 19.7: CT cmov
FE result = b;
ctf::field_cmov(&result, a, 0xFFFFFFFFFFFFFFFFULL);
CHECK(result == a, "ct field_cmov copies");
result = b;
ctf::field_cmov(&result, a, 0);
CHECK(result == b, "ct field_cmov preserves");
// 19.8: CT cswap
FE fa = a, fb = b;
ctf::field_cswap(&fa, &fb, 0xFFFFFFFFFFFFFFFFULL);
CHECK(fa == b && fb == a, "ct field_cswap swaps");
fa = a; fb = b;
ctf::field_cswap(&fa, &fb, 0);
CHECK(fa == a && fb == b, "ct field_cswap no swap");
// 19.9: CT select
CHECK(ctf::field_select(a, b, 0xFFFFFFFFFFFFFFFFULL) == a, "ct field_select(ones)");
CHECK(ctf::field_select(a, b, 0) == b, "ct field_select(zero)");
// 19.10: CT cneg
CHECK(a + ctf::field_cneg(a, 0xFFFFFFFFFFFFFFFFULL) == zero, "ct field_cneg(ones)");
CHECK(ctf::field_cneg(a, 0) == a, "ct field_cneg(zero)");
// 19.11: CT is_zero
CHECK(ctf::field_is_zero(zero) != 0, "ct field_is_zero(0)");
CHECK(ctf::field_is_zero(a) == 0, "ct field_is_zero(42)");
// 19.12: CT eq
CHECK(ctf::field_eq(a, a) != 0, "ct field_eq same");
CHECK(ctf::field_eq(a, b) == 0, "ct field_eq diff");
// 19.13: CT normalize is idempotent
FE const norm = ctf::field_normalize(a);
FE const norm2 = ctf::field_normalize(norm);
CHECK(norm == norm2, "ct field_normalize idempotent");
// 19.14: CT arithmetic consistency for many values
for (uint64_t v = 1; v <= 64; ++v) {
FE const fv = FE::from_uint64(v);
FE const fv2 = FE::from_uint64(v + 1);
CHECK(ctf::field_add(fv, fv2) == fv + fv2, "ct add #" + std::to_string(v));
CHECK(ctf::field_mul(fv, fv2) == fv * fv2, "ct mul #" + std::to_string(v));
}
}
// ============================================================================
// CATEGORY 20: CT Scalar (TestCategory::CTScalar)
// ============================================================================
static void test_ct_scalar() {
std::cout << " [CTScalar] CT scalar operations..." << '\n';
namespace cts = secp256k1::ct;
SC const a = SC::from_uint64(42);
SC const b = SC::from_uint64(99);
SC const zero = SC::zero();
[[maybe_unused]] SC const one = SC::one();
// 20.1-20.3: Arithmetic
CHECK(cts::scalar_add(a, b) == a + b, "ct scalar_add");
CHECK(cts::scalar_sub(a, b) == a - b, "ct scalar_sub");
CHECK(cts::scalar_neg(a) == a.negate(), "ct scalar_neg");
// 20.4: cmov
SC result = b;
cts::scalar_cmov(&result, a, 0xFFFFFFFFFFFFFFFFULL);
CHECK(result == a, "ct scalar_cmov copies");
result = b;
cts::scalar_cmov(&result, a, 0);
CHECK(result == b, "ct scalar_cmov preserves");
// 20.5: cswap
SC sa = a, sb = b;
cts::scalar_cswap(&sa, &sb, 0xFFFFFFFFFFFFFFFFULL);
CHECK(sa == b && sb == a, "ct scalar_cswap swaps");
sa = a; sb = b;
cts::scalar_cswap(&sa, &sb, 0);
CHECK(sa == a && sb == b, "ct scalar_cswap no swap");
// 20.6: select
CHECK(cts::scalar_select(a, b, 0xFFFFFFFFFFFFFFFFULL) == a, "ct scalar_select(ones)");
CHECK(cts::scalar_select(a, b, 0) == b, "ct scalar_select(zero)");
// 20.7: cneg
CHECK(cts::scalar_cneg(a, 0xFFFFFFFFFFFFFFFFULL) == a.negate(), "ct scalar_cneg(ones)");
CHECK(cts::scalar_cneg(a, 0) == a, "ct scalar_cneg(zero)");
// 20.8: is_zero / eq
CHECK(cts::scalar_is_zero(zero) != 0, "ct scalar_is_zero(0)");
CHECK(cts::scalar_is_zero(a) == 0, "ct scalar_is_zero(42)");
CHECK(cts::scalar_eq(a, a) != 0, "ct scalar_eq same");
CHECK(cts::scalar_eq(a, b) == 0, "ct scalar_eq diff");
// 20.9: bit
SC const s5 = SC::from_uint64(5); // 101
CHECK(cts::scalar_bit(s5, 0) == 1, "ct scalar_bit(5,0)");
CHECK(cts::scalar_bit(s5, 1) == 0, "ct scalar_bit(5,1)");
CHECK(cts::scalar_bit(s5, 2) == 1, "ct scalar_bit(5,2)");
// 20.10: window
SC const s0xFF = SC::from_uint64(0xFF); // 11111111
uint64_t w = cts::scalar_window(s0xFF, 0, 4);
CHECK(w == 0x0F, "ct scalar_window(0xFF,0,4)==0x0F");
w = cts::scalar_window(s0xFF, 4, 4);
CHECK(w == 0x0F, "ct scalar_window(0xFF,4,4)==0x0F");
}
// ============================================================================
// CATEGORY 21: CT Point (TestCategory::CTPoint)
// ============================================================================
static void test_ct_point() {
std::cout << " [CTPoint] CT point operations..." << '\n';
namespace ctp = secp256k1::ct;
PT const G = PT::generator();
[[maybe_unused]] PT const O = PT::infinity();
// 21.1: CT scalar_mul matches fast
for (unsigned k = 1; k <= 64; ++k) {
SC const s = SC::from_uint64(k);
PT const ct_result = ctp::scalar_mul(G, s);
PT const fast_result = G.scalar_mul(s);
CHECK(pt_eq(ct_result, fast_result),
"ct scalar_mul(" + std::to_string(k) + ") matches");
}
// 21.2: CT generator_mul
for (unsigned k = 1; k <= 32; ++k) {
SC const s = SC::from_uint64(k);
PT const ct_gen = ctp::generator_mul(s);
PT const fast_result = G.scalar_mul(s);
CHECK(pt_eq(ct_gen, fast_result),
"ct generator_mul(" + std::to_string(k) + ") matches");
}
// 21.3: CT on-curve check
CHECK(ctp::point_is_on_curve(G) != 0, "ct G is on curve");
for (unsigned k = 2; k <= 16; ++k) {
PT const P = G.scalar_mul(SC::from_uint64(k));
CHECK(ctp::point_is_on_curve(P) != 0,
"ct " + std::to_string(k) + "G on curve");
}
// 21.4: CT point equality
CHECK(ctp::point_eq(G, G) != 0, "ct G==G");
PT const twoG = G.scalar_mul(SC::from_uint64(2));
CHECK(ctp::point_eq(G, twoG) == 0, "ct G!=2G");
// 21.5: Complete addition -- handles all cases
auto jG = ctp::CTJacobianPoint::from_point(G);
auto jO = ctp::CTJacobianPoint::make_infinity();
// P + O = P
auto res = ctp::point_add_complete(jG, jO);
CHECK(pt_eq(res.to_point(), G), "ct complete add: G+O==G");
// O + P = P
res = ctp::point_add_complete(jO, jG);
CHECK(pt_eq(res.to_point(), G), "ct complete add: O+G==G");
// P + P = 2P
res = ctp::point_add_complete(jG, jG);
CHECK(pt_eq(res.to_point(), G.dbl()), "ct complete add: G+G==2G");
// P + (-P) = O
auto jNegG = ctp::point_neg(jG);
res = ctp::point_add_complete(jG, jNegG);
CHECK(res.to_point().is_infinity(), "ct complete add: G+(-G)==O");
// 21.6: point_cmov
auto jp = ctp::CTJacobianPoint::from_point(G);
auto jq = ctp::CTJacobianPoint::from_point(twoG);
ctp::point_cmov(&jp, jq, 0xFFFFFFFFFFFFFFFFULL);
CHECK(pt_eq(jp.to_point(), twoG), "ct point_cmov copies");
// 21.7: point_select
auto sel = ctp::point_select(jG, jq, 0xFFFFFFFFFFFFFFFFULL);
CHECK(pt_eq(sel.to_point(), G), "ct point_select(ones)==G");
sel = ctp::point_select(jG, jq, 0);
CHECK(pt_eq(sel.to_point(), twoG), "ct point_select(zero)==2G");
// 21.8: CT scalar_mul with large scalar
SC const large = SC::from_hex("DEADBEEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF01234567");
PT const ct_large = ctp::scalar_mul(G, large);
PT const fast_large = G.scalar_mul(large);
CHECK(pt_eq(ct_large, fast_large), "ct scalar_mul(large)");
// 21.9: CT scalar_mul identity (0*G = O)
PT const ct_zero = ctp::scalar_mul(G, SC::zero());
CHECK(ct_zero.is_infinity(), "ct 0*G==O");
// 21.10: CT scalar_mul (n*G = O)
PT const ct_order = ctp::scalar_mul(G, secp256k1_n());
CHECK(ct_order.is_infinity(), "ct n*G==O");
}
// ============================================================================
// CATEGORY 22: GLV Endomorphism (TestCategory::GLV)
// ============================================================================
static void test_glv() {
std::cout << " [GLV] GLV endomorphism tests..." << '\n';
using namespace secp256k1::fast;
PT const G = PT::generator();
// 22.1: Endomorphism beta*G.x produces valid point
PT const endo = apply_endomorphism(G);
FE const ex = endo.x();
FE const ey = endo.y();
CHECK(ey.square() == ex.square() * ex + FE::from_uint64(7), "phi(G) on curve");
// 22.2: phi(G).y == G.y (endomorphism preserves y)
CHECK(endo.y() == G.y(), "phi(G).y == G.y");
// 22.3: phi(G).x != G.x
CHECK(endo.x() != G.x(), "phi(G).x != G.x");
// 22.4: verify_endomorphism -- phi(phi(P)) + P should relate to curve
CHECK(verify_endomorphism(G), "verify_endomorphism(G)");
// 22.5: verify_endomorphism for multiple points
for (unsigned k = 2; k <= 16; ++k) {
PT const P = G.scalar_mul(SC::from_uint64(k));
CHECK(verify_endomorphism(P), "verify_endomorphism(" + std::to_string(k) + "G)");
}
// 22.6: GLV decomposition -- k = k1 + k2*lambda (mod n)
for (unsigned k = 1; k <= 64; ++k) {
SC const sk = SC::from_uint64(k);
auto decomp = glv_decompose(sk);
// Verify: k1 + k2*lambda == k (mod n)
SC const lambda = SC::from_bytes(secp256k1::fast::glv_constants::LAMBDA);
SC const k1 = decomp.k1_neg ? decomp.k1.negate() : decomp.k1;
SC const k2 = decomp.k2_neg ? decomp.k2.negate() : decomp.k2;
SC const reconstructed = k1 + k2 * lambda;
CHECK(reconstructed == sk, "GLV decompose/reconstruct k=" + std::to_string(k));
}
// 22.7: GLV decomposition for large scalar
SC const large = SC::from_hex("DEADBEEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF01234567");
auto decomp = glv_decompose(large);
SC const lambda = SC::from_bytes(secp256k1::fast::glv_constants::LAMBDA);
SC const lk1 = decomp.k1_neg ? decomp.k1.negate() : decomp.k1;
SC const lk2 = decomp.k2_neg ? decomp.k2.negate() : decomp.k2;
CHECK(lk1 + lk2 * lambda == large, "GLV decompose large");
// 22.8: lambda*G = phi(G) (eigenvalue property)
SC const lambda_sc = SC::from_bytes(secp256k1::fast::glv_constants::LAMBDA);
PT const lambdaG = G.scalar_mul(lambda_sc);
CHECK(pt_eq(lambdaG, endo), "lambda*G == phi(G)");
// 22.9: Endomorphism of infinity
PT const endo_inf = apply_endomorphism(PT::infinity());
CHECK(endo_inf.is_infinity(), "phi(O)==O");
// 22.10: lambda^2 + lambda + 1 == 0 (mod n)
SC const lambda2 = lambda_sc * lambda_sc;
SC const sum = lambda2 + lambda_sc + SC::one();
CHECK(sum == SC::zero(), "lambda^2+lambda+1 == 0 (mod n)");
// 22.11: beta^3 == 1 (mod p)
FE const beta = FE::from_bytes(secp256k1::fast::glv_constants::BETA);
FE const beta3 = beta * beta * beta;
CHECK(beta3 == FE::one(), "beta^3 == 1 (mod p)");
}
// ============================================================================
// CATEGORY 23: MSM (TestCategory::MSM)
// ============================================================================
static void test_msm() {
std::cout << " [MSM] Multi-scalar multiplication tests..." << '\n';
PT const G = PT::generator();
// 23.1: Empty MSM = O
CHECK(secp256k1::msm(std::vector<SC>{}, std::vector<PT>{}).is_infinity(),
"MSM(empty)==O");
// 23.2: Single element MSM = scalar_mul
SC const s42 = SC::from_uint64(42);
PT const single = secp256k1::msm(std::vector<SC>{s42}, std::vector<PT>{G});
CHECK(pt_eq(single, G.scalar_mul(s42)), "MSM(n=1)==scalar_mul");
// 23.3: Two-element MSM
SC const s_a = SC::from_uint64(7);
SC const s_b = SC::from_uint64(11);
PT const P_a = G;
PT const P_b = G.scalar_mul(SC::from_uint64(3));
PT const msm2 = secp256k1::msm(std::vector<SC>{s_a, s_b}, std::vector<PT>{P_a, P_b});
PT const naive2 = P_a.scalar_mul(s_a).add(P_b.scalar_mul(s_b));
CHECK(pt_eq(msm2, naive2), "MSM(n=2)");
// 23.4: Pippenger for medium n
const std::size_t n = 64;
std::vector<SC> scalars(n);
std::vector<PT> points(n);
PT P = G;
for (std::size_t i = 0; i < n; ++i) {
scalars[i] = SC::from_uint64(static_cast<uint64_t>(i * 31 + 17));
points[i] = P;
P = P.add(G);
}
PT naive = PT::infinity();
for (std::size_t i = 0; i < n; ++i) {
naive = naive.add(points[i].scalar_mul(scalars[i]));
}
PT const pip = secp256k1::pippenger_msm(scalars, points);
CHECK(pt_eq(pip, naive), "Pippenger(n=64)");
PT const unified = secp256k1::msm(scalars, points);
CHECK(pt_eq(unified, naive), "MSM(n=64)");
// 23.5: Shamir trick (2-point MSM)
PT const shamir = secp256k1::shamir_trick(s_a, P_a, s_b, P_b);
CHECK(pt_eq(shamir, naive2), "shamir_trick(2)");
// 23.6: Pippenger with larger n
const std::size_t n2 = 256;
std::vector<SC> scalars2(n2);
std::vector<PT> points2(n2);
P = G;
for (std::size_t i = 0; i < n2; ++i) {
scalars2[i] = SC::from_uint64(static_cast<uint64_t>(i * 13 + 5));
points2[i] = P;
P = P.add(G);
}
PT naive2_big = PT::infinity();
for (std::size_t i = 0; i < n2; ++i) {
naive2_big = naive2_big.add(points2[i].scalar_mul(scalars2[i]));
}
PT const pip2 = secp256k1::pippenger_msm(scalars2, points2);
CHECK(pt_eq(pip2, naive2_big), "Pippenger(n=256)");
// 23.7: Optimal window function
CHECK(secp256k1::pippenger_optimal_window(1) >= 1, "optimal_window(1)>=1");
CHECK(secp256k1::pippenger_optimal_window(256) >= 1, "optimal_window(256)>=1");
// 23.8: MSM with zero scalars
std::vector<SC> const zeros_s = {SC::zero(), SC::zero()};
std::vector<PT> const pts = {G, G.dbl()};
PT const zero_msm = secp256k1::msm(zeros_s, pts);
CHECK(zero_msm.is_infinity(), "MSM with zero scalars == O");
// 23.9: Strauss optimal window
CHECK(secp256k1::strauss_optimal_window(1) >= 1, "strauss_window(1)>=1");
// 23.10: multi_scalar_mul (from multiscalar.hpp)
PT const multi_result = secp256k1::multi_scalar_mul(
std::vector<SC>{s_a, s_b}, std::vector<PT>{P_a, P_b});
CHECK(pt_eq(multi_result, naive2), "multi_scalar_mul(n=2)");
}
// ============================================================================
// CATEGORY 24: Comb Generator (TestCategory::CombGen)
// ============================================================================
static void test_comb_gen() {
std::cout << " [CombGen] Comb generator tests..." << '\n';
PT const G = PT::generator();
// 24.1: Init and ready
secp256k1::fast::CombGenContext ctx;
ctx.init(6);
CHECK(ctx.ready(), "CombGenContext ready after init");
// 24.2: teeth/spacing
CHECK(ctx.teeth() == 6, "teeth==6");
CHECK(ctx.spacing() > 0, "spacing>0");
CHECK(ctx.table_size_bytes() > 0, "table_size>0");
// 24.3: mul matches scalar_mul for small range
for (unsigned k = 1; k <= 128; ++k) {
SC const s = SC::from_uint64(k);
PT const comb_result = ctx.mul(s);
PT const expected = G.scalar_mul(s);
CHECK(pt_eq(comb_result, expected),
"comb_mul(" + std::to_string(k) + ")");
}
// 24.4: CT mul matches
for (unsigned k = 1; k <= 16; ++k) {
SC const s = SC::from_uint64(k);
PT const ct_result = ctx.mul_ct(s);
PT const expected = G.scalar_mul(s);
CHECK(pt_eq(ct_result, expected),
"comb_mul_ct(" + std::to_string(k) + ")");
}
// 24.5: Large scalar
SC const large = SC::from_hex("DEADBEEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF01234567");
CHECK(pt_eq(ctx.mul(large), G.scalar_mul(large)), "comb_mul(large)");
// 24.6: Global singleton
secp256k1::fast::init_comb_gen(6);
CHECK(secp256k1::fast::comb_gen_ready(), "global comb ready");
SC const s42 = SC::from_uint64(42);
CHECK(pt_eq(secp256k1::fast::comb_gen_mul(s42), G.scalar_mul(s42)), "global comb_gen_mul(42)");
// 24.7: CT global
CHECK(pt_eq(secp256k1::fast::comb_gen_mul_ct(s42), G.scalar_mul(s42)), "global comb_gen_mul_ct(42)");
// 24.8: Different teeth values
for (unsigned const teeth : {4u, 5u, 7u, 8u}) {
secp256k1::fast::CombGenContext ctx2;
ctx2.init(teeth);
CHECK(ctx2.ready(), "CombGen teeth=" + std::to_string(teeth) + " ready");
SC const test_s = SC::from_uint64(42);
PT const result = ctx2.mul(test_s);
PT const expected = G.scalar_mul(test_s);
CHECK(pt_eq(result, expected),
"CombGen teeth=" + std::to_string(teeth) + " correct");
}
// 24.9: Zero scalar
SC const s_zero = SC::zero();
// Comb mul of zero should produce infinity
PT const zero_result = ctx.mul(s_zero);
CHECK(zero_result.is_infinity(), "comb_mul(0)==O");
// 24.10: One scalar
SC const s_one = SC::one();
CHECK(pt_eq(ctx.mul(s_one), G), "comb_mul(1)==G");
}
// ============================================================================
// CATEGORY 25: Batch Inverse (TestCategory::BatchInverse)
// ============================================================================
static void test_batch_inverse() {
std::cout << " [BatchInverse] Batch inverse tests..." << '\n';
FE const one = FE::one();
// 25.1: Batch of 1
FE batch1[1] = {FE::from_uint64(7)};
FE const exp1 = batch1[0].inverse();
secp256k1::fast::fe_batch_inverse(batch1, 1);
CHECK(batch1[0] == exp1, "batch_inv(n=1)");
// 25.2: Batch of 2
FE batch2[2] = {FE::from_uint64(3), FE::from_uint64(5)};
FE const exp2[2] = {batch2[0].inverse(), batch2[1].inverse()};
secp256k1::fast::fe_batch_inverse(batch2, 2);
CHECK(batch2[0] == exp2[0] && batch2[1] == exp2[1], "batch_inv(n=2)");
// 25.3: Batch of 16
const int N = 16;
FE batch16[N];
FE expected16[N];
for (unsigned i = 0; i < static_cast<unsigned>(N); ++i) {
batch16[i] = FE::from_uint64(static_cast<uint64_t>(i) + 2);
expected16[i] = batch16[i].inverse();
}
secp256k1::fast::fe_batch_inverse(batch16, N);
bool all_match = true;
for (unsigned i = 0; i < static_cast<unsigned>(N); ++i) {
if (batch16[i] != expected16[i]) all_match = false;
}
CHECK(all_match, "batch_inv(n=16) all match");
// 25.4: Verify a*a^-^1 = 1 for batch results
FE vals[8];
for (unsigned i = 0; i < 8; ++i) vals[i] = FE::from_uint64(static_cast<uint64_t>(i) * 7 + 3);
FE originals[8];
std::memcpy(originals, vals, sizeof(vals));
secp256k1::fast::fe_batch_inverse(vals, 8);
for (unsigned i = 0; i < 8; ++i) {
CHECK(originals[i] * vals[i] == one, "batch a*a^-^1==1 #" + std::to_string(i));
}
// 25.5: Large batch
const int LN = 64;
FE large_batch[LN];
FE large_orig[LN];
for (unsigned i = 0; i < static_cast<unsigned>(LN); ++i) {
large_batch[i] = FE::from_uint64(static_cast<uint64_t>(i) + 2);
large_orig[i] = large_batch[i];
}
secp256k1::fast::fe_batch_inverse(large_batch, LN);
bool large_ok = true;
for (unsigned i = 0; i < static_cast<unsigned>(LN); ++i) {
if (large_orig[i] * large_batch[i] != one) large_ok = false;
}
CHECK(large_ok, "batch_inv(n=64) all correct");
}
// ============================================================================
// CATEGORY 26: ECDSA (TestCategory::ECDSA)
// ============================================================================
static void test_ecdsa() {
std::cout << " [ECDSA] ECDSA sign/verify tests..." << '\n';
PT const G = PT::generator();
// 26.1: Sign and verify basic
SC const privkey = SC::from_uint64(42);
PT const pubkey = G.scalar_mul(privkey);
std::array<uint8_t, 32> msg{};
msg[0] = 0x01;
auto sig = secp256k1::ecdsa_sign(msg, privkey);
CHECK(secp256k1::ecdsa_verify(msg, pubkey, sig), "ECDSA sign+verify");
// 26.2: Wrong message fails
std::array<uint8_t, 32> wrong_msg{};
wrong_msg[0] = 0x02;
CHECK(!secp256k1::ecdsa_verify(wrong_msg, pubkey, sig), "ECDSA wrong msg fails");
// 26.3: Wrong pubkey fails
PT const wrong_pub = G.scalar_mul(SC::from_uint64(43));
CHECK(!secp256k1::ecdsa_verify(msg, wrong_pub, sig), "ECDSA wrong pubkey fails");
// 26.4: Deterministic nonce (RFC 6979)
SC const nonce = secp256k1::rfc6979_nonce(privkey, msg);
CHECK(!nonce.is_zero(), "RFC6979 nonce nonzero");
// Same inputs -> same nonce (deterministic)
SC const nonce2 = secp256k1::rfc6979_nonce(privkey, msg);
CHECK(nonce == nonce2, "RFC6979 deterministic");
// Different key -> different nonce
SC const nonce3 = secp256k1::rfc6979_nonce(SC::from_uint64(43), msg);
CHECK(nonce != nonce3, "RFC6979 different key -> different nonce");
// 26.5: Signature normalization (low-S)
auto norm_sig = sig.normalize();
CHECK(norm_sig.is_low_s(), "normalized is low-S");
CHECK(secp256k1::ecdsa_verify(msg, pubkey, norm_sig), "normalized sig verifies");
// 26.6: Compact serialization roundtrip
auto compact = sig.to_compact();
auto recovered_sig = secp256k1::ECDSASignature::from_compact(compact);
CHECK(recovered_sig.r == sig.r && recovered_sig.s == sig.s, "compact roundtrip");
// 26.7: DER serialization
auto [der, der_len] = sig.to_der();
CHECK(der_len > 0 && der_len <= 72, "DER length valid");
CHECK(der[0] == 0x30, "DER sequence tag");
// 26.8: Multiple keys
for (uint64_t k = 1; k <= 16; ++k) {
SC const key = SC::from_uint64(k * 1000 + 7);
PT const pub = G.scalar_mul(key);
auto s = secp256k1::ecdsa_sign(msg, key);
CHECK(secp256k1::ecdsa_verify(msg, pub, s),
"ECDSA key #" + std::to_string(k));
}
// 26.9: Sign with different messages
for (uint8_t m = 0; m < 16; ++m) {
std::array<uint8_t, 32> msg_i{};
msg_i[0] = m;
auto s = secp256k1::ecdsa_sign(msg_i, privkey);
CHECK(secp256k1::ecdsa_verify(msg_i, pubkey, s),
"ECDSA msg #" + std::to_string(m));
}
}
// ============================================================================
// CATEGORY 27: Schnorr (TestCategory::Schnorr)
// ============================================================================
static void test_schnorr() {
std::cout << " [Schnorr] Schnorr sign/verify tests..." << '\n';
[[maybe_unused]] PT const G = PT::generator();
// 27.1: Sign and verify
SC const privkey = SC::from_uint64(42);
std::array<uint8_t, 32> msg{};
msg[0] = 0x01;
std::array<uint8_t, 32> aux{};
aux[0] = 0xAB;
auto sig = secp256k1::schnorr_sign(privkey, msg, aux);
auto pubkey_x = secp256k1::schnorr_pubkey(privkey);
CHECK(secp256k1::schnorr_verify(pubkey_x, msg, sig), "Schnorr sign+verify");
// 27.2: Wrong message fails
std::array<uint8_t, 32> wrong_msg{};
wrong_msg[0] = 0x02;
CHECK(!secp256k1::schnorr_verify(pubkey_x, wrong_msg, sig), "Schnorr wrong msg fails");
// 27.3: Signature serialization roundtrip
auto sig_bytes = sig.to_bytes();
auto sig_back = secp256k1::SchnorrSignature::from_bytes(sig_bytes);
CHECK(sig_back.s == sig.s, "Schnorr sig bytes roundtrip (s)");
CHECK(sig_back.r == sig.r, "Schnorr sig bytes roundtrip (r)");
// 27.4: Multiple keys
for (uint64_t k = 1; k <= 16; ++k) {
SC const key = SC::from_uint64(k * 1000 + 7);
auto pub_x = secp256k1::schnorr_pubkey(key);
auto s = secp256k1::schnorr_sign(key, msg, aux);
CHECK(secp256k1::schnorr_verify(pub_x, msg, s),
"Schnorr key #" + std::to_string(k));
}
// 27.5: tagged_hash
auto hash1 = secp256k1::tagged_hash("BIP0340/challenge", msg.data(), 32);
auto hash2 = secp256k1::tagged_hash("BIP0340/challenge", msg.data(), 32);
CHECK(hash1 == hash2, "tagged_hash deterministic");
auto hash3 = secp256k1::tagged_hash("DifferentTag", msg.data(), 32);
CHECK(hash1 != hash3, "tagged_hash different tag != result");
// 27.6: Determinism (same inputs -> same sig)
auto sig2 = secp256k1::schnorr_sign(privkey, msg, aux);
CHECK(sig.s == sig2.s && sig.r == sig2.r, "Schnorr deterministic");
// 27.7: Different aux_rand -> different signing nonce
std::array<uint8_t, 32> aux2{};
aux2[0] = 0xCD;
auto sig3 = secp256k1::schnorr_sign(privkey, msg, aux2);
// Sig should be different (different aux_rand)
CHECK(sig3.r != sig.r || sig3.s != sig.s, "different aux -> different sig");
// But still valid
CHECK(secp256k1::schnorr_verify(pubkey_x, msg, sig3), "diff aux sig still verifies");
}
// ============================================================================
// CATEGORY 28: ECDH (TestCategory::ECDH)
// ============================================================================
static void test_ecdh() {
std::cout << " [ECDH] ECDH shared secret tests..." << '\n';
PT const G = PT::generator();
using secp256k1::Scalar;
using secp256k1::Point;
// 28.1: Basic ECDH -- Alice and Bob derive same secret
SC const alice_priv = SC::from_uint64(42);
SC const bob_priv = SC::from_uint64(99);
PT const alice_pub = G.scalar_mul(alice_priv);
PT const bob_pub = G.scalar_mul(bob_priv);
auto secret_alice = secp256k1::ecdh_compute(alice_priv, bob_pub);
auto secret_bob = secp256k1::ecdh_compute(bob_priv, alice_pub);
CHECK(secret_alice == secret_bob, "ECDH shared secret matches");
// 28.2: Different keys -> different secrets
SC const carol_priv = SC::from_uint64(200);
PT const carol_pub = G.scalar_mul(carol_priv);
auto secret_ac = secp256k1::ecdh_compute(alice_priv, carol_pub);
CHECK(secret_alice != secret_ac, "different keys -> different secrets");
// 28.3: xonly variant
auto xonly_alice = secp256k1::ecdh_compute_xonly(alice_priv, bob_pub);
auto xonly_bob = secp256k1::ecdh_compute_xonly(bob_priv, alice_pub);
CHECK(xonly_alice == xonly_bob, "ECDH xonly matches");
// 28.4: Raw variant
auto raw_alice = secp256k1::ecdh_compute_raw(alice_priv, bob_pub);
auto raw_bob = secp256k1::ecdh_compute_raw(bob_priv, alice_pub);
CHECK(raw_alice == raw_bob, "ECDH raw matches");
// 28.5: Multiple key pairs
for (uint64_t ka = 1; ka <= 8; ++ka) {
for (uint64_t kb = ka + 1; kb <= 9; ++kb) {
SC const a = SC::from_uint64(ka * 1000 + 7);
SC const b = SC::from_uint64(kb * 1000 + 13);
PT const pa = G.scalar_mul(a);
PT const pb = G.scalar_mul(b);
auto sa = secp256k1::ecdh_compute(a, pb);
auto sb = secp256k1::ecdh_compute(b, pa);
CHECK(sa == sb, "ECDH pair (" + std::to_string(ka) + "," + std::to_string(kb) + ")");
}
}
}
// ============================================================================
// CATEGORY 29: Key Recovery (TestCategory::Recovery)
// ============================================================================
static void test_recovery() {
std::cout << " [Recovery] Key recovery tests..." << '\n';
PT const G = PT::generator();
// 29.1: Sign recoverable and recover pubkey
SC const privkey = SC::from_uint64(42);
PT const pubkey = G.scalar_mul(privkey);
std::array<uint8_t, 32> msg{};
msg[0] = 0x01;
auto rsig = secp256k1::ecdsa_sign_recoverable(msg, privkey);
CHECK(rsig.recid >= 0 && rsig.recid <= 3, "recid in [0,3]");
// 29.2: Recover public key
auto [recovered, success] = secp256k1::ecdsa_recover(msg, rsig.sig, rsig.recid);
CHECK(success, "recovery succeeded");
CHECK(pt_eq(recovered, pubkey), "recovered pubkey matches");
// 29.3: Wrong recid fails or gives wrong key
int const wrong_recid = (rsig.recid + 1) % 4;
auto [recovered2, success2] = secp256k1::ecdsa_recover(msg, rsig.sig, wrong_recid);
if (success2) {
CHECK(!pt_eq(recovered2, pubkey), "wrong recid -> wrong key");
} else {
CHECK(true, "wrong recid -> recovery failed (expected)");
}
// 29.4: Compact roundtrip
auto compact = secp256k1::recoverable_to_compact(rsig);
auto [back, back_ok] = secp256k1::recoverable_from_compact(compact);
CHECK(back_ok, "recoverable compact roundtrip");
CHECK(back.recid == rsig.recid, "recoverable recid preserved");
CHECK(back.sig.r == rsig.sig.r && back.sig.s == rsig.sig.s, "recoverable sig preserved");
// 29.5: Multiple keys
for (uint64_t k = 1; k <= 8; ++k) {
SC const key = SC::from_uint64(k * 1000 + 7);
PT const pub = G.scalar_mul(key);
auto rs = secp256k1::ecdsa_sign_recoverable(msg, key);
auto [rec, ok] = secp256k1::ecdsa_recover(msg, rs.sig, rs.recid);
CHECK(ok, "recovery key #" + std::to_string(k));
CHECK(pt_eq(rec, pub), "recovered matches key #" + std::to_string(k));
}
}
// ============================================================================
// BONUS: SHA-256 / SHA-512 tests (integrated into protocol categories)
// ============================================================================
static void test_hashing() {
std::cout << " [Hashing] SHA-256/SHA-512 tests..." << '\n';
// SHA-256 NIST vectors
// 30.1: SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
auto empty_hash = secp256k1::SHA256::hash(nullptr, 0);
CHECK(empty_hash[0] == 0xe3 && empty_hash[1] == 0xb0 && empty_hash[2] == 0xc4,
"SHA-256('') first 3 bytes");
// 30.2: SHA-256("abc")
const char* abc = "abc";
auto abc_hash = secp256k1::SHA256::hash(abc, 3);
// Expected: ba7816bf 8f01cfea 414140de 5dae2223 b00361a3 96177a9c b410ff61 f20015ad
CHECK(abc_hash[0] == 0xba && abc_hash[1] == 0x78 && abc_hash[2] == 0x16 && abc_hash[3] == 0xbf,
"SHA-256('abc') first 4 bytes");
// 30.3: SHA-256 incremental matches one-shot
secp256k1::SHA256 sha;
sha.update("ab", 2);
sha.update("c", 1);
auto incremental = sha.finalize();
CHECK(incremental == abc_hash, "SHA-256 incremental matches one-shot");
// 30.4: SHA-256 reset
sha.reset();
sha.update("abc", 3);
auto after_reset = sha.finalize();
CHECK(after_reset == abc_hash, "SHA-256 reset works");
// 30.5: hash256 (double SHA-256)
auto double_hash = secp256k1::SHA256::hash256("abc", 3);
auto manual_double = secp256k1::SHA256::hash(abc_hash.data(), 32);
CHECK(double_hash == manual_double, "hash256 == hash(hash(x))");
// SHA-512
// 30.6: SHA-512("") = cf83e1357eefb8bd...
auto empty512 = secp256k1::SHA512::hash(nullptr, 0);
CHECK(empty512[0] == 0xcf && empty512[1] == 0x83 && empty512[2] == 0xe1,
"SHA-512('') first 3 bytes");
// 30.7: SHA-512("abc")
auto abc512 = secp256k1::SHA512::hash(abc, 3);
// Expected: ddaf35a193617aba cc417349ae204131 12e6fa4e89a97ea2 ...
CHECK(abc512[0] == 0xdd && abc512[1] == 0xaf && abc512[2] == 0x35 && abc512[3] == 0xa1,
"SHA-512('abc') first 4 bytes");
// 30.8: SHA-512 incremental
secp256k1::SHA512 sha512;
sha512.update("ab", 2);
sha512.update("c", 1);
auto inc512 = sha512.finalize();
CHECK(inc512 == abc512, "SHA-512 incremental matches");
// 30.9: SHA-256 longer input
const char* msg448 = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq";
auto hash448 = secp256k1::SHA256::hash(msg448, std::strlen(msg448));
// Expected: 248d6a61 d20638b8 e5c02693 0c3e6039 a33ce459 64ff2167 f6ecedd4 19db06c1
CHECK(hash448[0] == 0x24 && hash448[1] == 0x8d && hash448[2] == 0x6a,
"SHA-256(448-bit msg) first 3 bytes");
// 30.10: Determinism
auto h1 = secp256k1::SHA256::hash(abc, 3);
auto h2 = secp256k1::SHA256::hash(abc, 3);
CHECK(h1 == h2, "SHA-256 deterministic");
}
// ============================================================================
// BONUS: Batch Add Affine tests
// ============================================================================
static void test_batch_add_affine_comprehensive() {
std::cout << " [BatchAddAffine] Batch affine addition tests..." << '\n';
using namespace secp256k1::fast;
PT const G = PT::generator();
[[maybe_unused]] FE const gx = G.x();
[[maybe_unused]] FE const gy = G.y();
// 31.1: precompute_g_multiples
auto g_multiples = precompute_g_multiples(64);
CHECK(g_multiples.size() == 64, "precomputed 64 G multiples");
// 31.2: Verify precomputed multiples are on curve
for (std::size_t i = 0; i < 16 && i < g_multiples.size(); ++i) {
FE const cx = g_multiples[i].x;
FE const cy = g_multiples[i].y;
FE const lhs = cy.square();
FE const rhs = cx.square() * cx + FE::from_uint64(7);
CHECK(lhs == rhs, "g_multiple[" + std::to_string(i) + "] on curve");
}
// 31.3: precompute_point_multiples with custom base
PT const p7 = G.scalar_mul(SC::from_uint64(7));
FE const p7x = p7.x();
FE const p7y = p7.y();
auto p7_multiples = precompute_point_multiples(p7x, p7y, 16);
CHECK(p7_multiples.size() == 16, "precomputed 16 7G multiples");
// 31.5: negate_affine_table flips sign
auto negated = negate_affine_table(g_multiples.data(), 16);
CHECK(negated.size() == 16, "negated table size");
// Negated y should differ from original
CHECK(negated[0].y != g_multiples[0].y, "negated y differs");
CHECK(negated[0].x == g_multiples[0].x, "negated x same");
// 31.6: Negate + original y should sum to zero on curve (y + (-y) = 0 mod p)
FE const y_sum = g_multiples[0].y + negated[0].y;
CHECK(y_sum == FE::zero(), "y + neg_y == 0 (additive inverse)");
}
// ============================================================================
// BONUS: Batch Verify tests
// ============================================================================
static void test_batch_verify() {
std::cout << " [BatchVerify] Batch verification tests..." << '\n';
PT const G = PT::generator();
// 32.1: ECDSA batch verify (all valid)
std::vector<secp256k1::ECDSABatchEntry> ecdsa_entries;
for (uint64_t k = 1; k <= 4; ++k) {
SC const key = SC::from_uint64(k * 1000 + 7);
PT const pub = G.scalar_mul(key);
std::array<uint8_t, 32> msg{};
msg[0] = static_cast<uint8_t>(k);
auto sig = secp256k1::ecdsa_sign(msg, key);
ecdsa_entries.push_back({msg, pub, sig});
}
CHECK(secp256k1::ecdsa_batch_verify(ecdsa_entries), "ECDSA batch verify all valid");
// 32.2: ECDSA batch verify with one invalid
auto bad_entries = ecdsa_entries;
bad_entries[2].msg_hash[0] ^= 0xFF; // corrupt one message
CHECK(!secp256k1::ecdsa_batch_verify(bad_entries), "ECDSA batch verify detects invalid");
// 32.3: Identify invalid entries
auto invalid_indices = secp256k1::ecdsa_batch_identify_invalid(
bad_entries.data(), bad_entries.size());
CHECK(!invalid_indices.empty(), "ecdsa_batch_identify finds invalid");
bool found_idx2 = false;
for (auto idx : invalid_indices) {
if (idx == 2) found_idx2 = true;
}
CHECK(found_idx2, "identified corrupted entry at index 2");
// 32.4: Schnorr batch verify
std::vector<secp256k1::SchnorrBatchEntry> schnorr_entries;
for (uint64_t k = 1; k <= 4; ++k) {
SC const key = SC::from_uint64(k * 1000 + 7);
auto pub_x = secp256k1::schnorr_pubkey(key);
std::array<uint8_t, 32> msg{};
msg[0] = static_cast<uint8_t>(k);
std::array<uint8_t, 32> aux{};
aux[0] = static_cast<uint8_t>(k + 0x10);
auto sig = secp256k1::schnorr_sign(key, msg, aux);
schnorr_entries.push_back({pub_x, msg, sig});
}
CHECK(secp256k1::schnorr_batch_verify(schnorr_entries), "Schnorr batch verify all valid");
// 32.5: Schnorr batch with invalid
auto bad_schnorr = schnorr_entries;
bad_schnorr[1].message[0] ^= 0xFF;
CHECK(!secp256k1::schnorr_batch_verify(bad_schnorr), "Schnorr batch detects invalid");
// 32.6: Empty batch
CHECK(secp256k1::ecdsa_batch_verify(std::vector<secp256k1::ECDSABatchEntry>{}),
"ECDSA empty batch verify");
CHECK(secp256k1::schnorr_batch_verify(std::vector<secp256k1::SchnorrBatchEntry>{}),
"Schnorr empty batch verify");
}
// ============================================================================
// BONUS: Homomorphism & Order tests (expanded)
// ============================================================================
static void test_homomorphism_expanded() {
std::cout << " [Homomorphism] Expanded homomorphism tests..." << '\n';
PT const G = PT::generator();
// 33.1: a*G + b*G = (a+b)*G for larger range
const unsigned N = 128;
unsigned const M = 2 * N + 1;
std::vector<PT> ref(M + 1);
ref[0] = PT::infinity();
ref[1] = G;
for (unsigned k = 2; k <= M; ++k) ref[k] = ref[k-1].add(G);
unsigned const step = N / 32;
for (unsigned a = 1; a <= N; a += step) {
for (unsigned b = 1; b <= N; b += step) {
PT const lhs = ref[a].add(ref[b]);
PT const rhs = ref[a + b];
CHECK(pt_eq(lhs, rhs),
"homo a=" + std::to_string(a) + ",b=" + std::to_string(b));
}
}
// 33.2: Doubling chain -- 2^k * G via repeated doubling
PT dbl_chain = G;
for (int k = 1; k <= 20; ++k) {
PT const dbl_result = dbl_chain.dbl();
PT const add_result = dbl_chain.add(dbl_chain);
CHECK(pt_eq(dbl_result, add_result), "2^" + std::to_string(k) + " dbl==add");
dbl_chain = dbl_result;
}
// 33.3: Subtraction property: (a+b)*G - b*G = a*G
for (unsigned a = 1; a <= 32; ++a) {
for (unsigned b = 1; b <= 32; b += 3) {
PT const abG = ref[a + b];
PT const bG_neg = ref[b].negate();
PT const diff = abG.add(bG_neg);
CHECK(pt_eq(diff, ref[a]),
"(a+b)G - bG = aG, a=" + std::to_string(a) + ",b=" + std::to_string(b));
}
}
}
// ============================================================================
// BONUS: Precompute module tests
// ============================================================================
static void test_precompute() {
std::cout << " [Precompute] Precomputation module tests..." << '\n';
using namespace secp256k1::fast;
PT const G = PT::generator();
// 34.1: split_scalar_glv correctness
for (unsigned k = 1; k <= 32; ++k) {
SC const s = SC::from_uint64(k);
auto decomp = split_scalar_glv(s);
// Reconstructed via endomorphism
SC const lambda = SC::from_bytes(glv_constants::LAMBDA);
SC const k1 = decomp.neg1 ? decomp.k1.negate() : decomp.k1;
SC const k2 = decomp.neg2 ? decomp.k2.negate() : decomp.k2;
CHECK(k1 + k2 * lambda == s,
"split_scalar_glv(" + std::to_string(k) + ")");
}
// 34.2: precompute_scalar_for_arbitrary
SC const key = SC::from_uint64(42);
auto precomp = precompute_scalar_for_arbitrary(key, 4);
CHECK(precomp.is_valid(), "precomputed scalar valid");
// Use it on a point
PT const P = G.scalar_mul(SC::from_uint64(7));
PT const result = scalar_mul_arbitrary_precomputed(P, precomp);
PT const expected = P.scalar_mul(key);
CHECK(pt_eq(result, expected), "precomputed_arbitrary matches");
// 34.3: precompute_scalar_optimized
auto precomp_opt = precompute_scalar_optimized(key, 4);
CHECK(precomp_opt.is_valid(), "optimized precomp valid");
PT const result_opt = scalar_mul_arbitrary_precomputed_optimized(P, precomp_opt);
CHECK(pt_eq(result_opt, expected), "optimized precomputed matches");
// 34.4: scalar_mul_arbitrary (via standard scalar_mul as fallback reference)
PT const arb_result = P.scalar_mul(key);
CHECK(pt_eq(arb_result, expected), "scalar_mul on arbitrary point matches");
// 34.5: compute_wnaf
auto wnaf = compute_wnaf(key, 4);
CHECK(!wnaf.empty(), "compute_wnaf non-empty");
}
// ============================================================================
// Runner -- dispatches by TestCategory
// ============================================================================
using TestFn = void(*)();
struct CategoryEntry {
TestCategory cat;
TestFn fn;
};
static const CategoryEntry CATEGORY_TABLE[] = {
{ TestCategory::FieldArith, test_field_arith },
{ TestCategory::FieldConversions, test_field_conversions },
{ TestCategory::FieldEdgeCases, test_field_edge_cases },
{ TestCategory::FieldInverse, test_field_inverse },
{ TestCategory::FieldBranchless, test_field_branchless },
{ TestCategory::FieldOptimal, test_field_optimal },
{ TestCategory::FieldRepresentations, test_field_representations },
{ TestCategory::ScalarArith, test_scalar_arith },
{ TestCategory::ScalarConversions, test_scalar_conversions },
{ TestCategory::ScalarEdgeCases, test_scalar_edge_cases },
{ TestCategory::ScalarEncoding, test_scalar_encoding },
{ TestCategory::PointBasic, test_point_basic },
{ TestCategory::PointScalarMul, test_point_scalar_mul },
{ TestCategory::PointInplace, test_point_inplace },
{ TestCategory::PointPrecomputed, test_point_precomputed },
{ TestCategory::PointSerialization, test_point_serialization },
{ TestCategory::PointEdgeCases, test_point_edge_cases },
{ TestCategory::CTOps, test_ct_ops },
{ TestCategory::CTField, test_ct_field },
{ TestCategory::CTScalar, test_ct_scalar },
{ TestCategory::CTPoint, test_ct_point },
{ TestCategory::GLV, test_glv },
{ TestCategory::MSM, test_msm },
{ TestCategory::CombGen, test_comb_gen },
{ TestCategory::BatchInverse, test_batch_inverse },
{ TestCategory::ECDSA, test_ecdsa },
{ TestCategory::Schnorr, test_schnorr },
{ TestCategory::ECDH, test_ecdh },
{ TestCategory::Recovery, test_recovery },
};
// Extra tests (not in single-category mapping, run under All)
static const TestFn EXTRA_TESTS[] = {
test_hashing,
test_batch_add_affine_comprehensive,
test_batch_verify,
test_homomorphism_expanded,
test_precompute,
};
#ifdef STANDALONE_TEST
int main() {
#else
int test_comprehensive_run() {
#endif
std::cout << "\n=== Comprehensive Test Suite (" << secp256k1::test::NUM_CATEGORIES
<< " categories) ===" << '\n';
auto t0 = std::chrono::high_resolution_clock::now();
g_counters = {};
// Run all categorized tests
for (auto& entry : CATEGORY_TABLE) {
std::cout << "\n-- " << secp256k1::test::category_name(entry.cat) << " --" << '\n';
entry.fn();
}
// Run extra tests
std::cout << "\n-- Extra/Cross-cutting Tests --" << '\n';
for (auto fn : EXTRA_TESTS) {
fn();
}
auto t1 = std::chrono::high_resolution_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(t1 - t0).count();
std::cout << "\n -- Comprehensive Results: "
<< g_counters.passed << " passed, "
<< g_counters.failed << " failed, "
<< g_counters.skipped << " skipped"
<< " (" << ms << " ms)" << '\n';
if (g_counters.failed > 0) {
std::cerr << "\n *** COMPREHENSIVE TESTS FAILED ***" << '\n';
} else {
std::cout << " All comprehensive tests PASSED" << '\n';
}
#ifdef STANDALONE_TEST
return g_counters.failed > 0 ? 1 : 0;
#else
return static_cast<int>(g_counters.failed);
#endif
}