UltrafastSecp256k1/cpu/tests/test_edge_cases.cpp
shrec ca14a9e47e
fix(ci): resolve macOS Metal shader errors + code scanning alerts
- Metal: add constant address space overload for zk_tagged_hash_midstate()
  (constant ZKTagMidstate refs cannot bind to thread address space params)
- Metal: add device address space overload for scalar_mul()
  (device AffinePoint arrays cannot bind to thread address space params)
- Fix -Wsign-conversion in host_helpers.h (int -> size_t loop vars, explicit casts)
- Fix -Wsign-conversion in audit/test_vectors.hpp (int -> size_t loop vars)
- Fix -Wunused-const-variable for BARRETT_MU in scalar.cpp (only used in non-int128 path)
- Fix CodeQL cpp/unused-local-variable alerts in test_edge_cases.cpp:
  add (void) casts for unused structured binding variables (child, hchild, bad_master, bad_master2)
2026-03-13 13:12:20 +00:00

477 lines
17 KiB
C++

// ============================================================================
// Test: Edge cases & coverage gaps
// ============================================================================
// Exercises untested branches and rare code paths:
// 1. Scalar zero rejection for ECDSA/Schnorr signing
// 2. Point at infinity arithmetic (O+O, dbl(O), O+P)
// 3. BIP-32 IL >= curve order rejection
// 4. Precompute cache corruption recovery
// 5. Scalar boundary values (k=n-1 => -G, k=1, k=2)
// 6. parse_bytes_strict boundary rejection (scalar == n, scalar == n+1)
// ============================================================================
#include "secp256k1/point.hpp"
#include "secp256k1/scalar.hpp"
#include "secp256k1/field.hpp"
#include "secp256k1/ecdsa.hpp"
#include "secp256k1/schnorr.hpp"
#include "secp256k1/bip32.hpp"
#include "secp256k1/precompute.hpp"
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <array>
#include <fstream>
using namespace secp256k1::fast;
using secp256k1::ecdsa_sign;
using secp256k1::ecdsa_verify;
using secp256k1::ECDSASignature;
using secp256k1::schnorr_sign;
using secp256k1::schnorr_verify;
using secp256k1::bip32_master_key;
static int g_tests_run = 0;
static int g_tests_passed = 0;
#define CHECK(cond, msg) do { \
++g_tests_run; \
if (cond) { ++g_tests_passed; std::printf(" [PASS] %s\n", msg); } \
else { std::printf(" [FAIL] %s\n", msg); } \
} while(0)
// -- secp256k1 curve order n (big-endian bytes) ------------------------------
static constexpr std::array<uint8_t, 32> ORDER_N = {
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFE,
0xBA,0xAE,0xDC,0xE6,0xAF,0x48,0xA0,0x3B,
0xBF,0xD2,0x5E,0x8C,0xD0,0x36,0x41,0x41
};
// n-1 (big-endian bytes)
static constexpr std::array<uint8_t, 32> ORDER_N_MINUS_1 = {
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFE,
0xBA,0xAE,0xDC,0xE6,0xAF,0x48,0xA0,0x3B,
0xBF,0xD2,0x5E,0x8C,0xD0,0x36,0x41,0x40
};
// n+1 (big-endian bytes)
static constexpr std::array<uint8_t, 32> ORDER_N_PLUS_1 = {
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFE,
0xBA,0xAE,0xDC,0xE6,0xAF,0x48,0xA0,0x3B,
0xBF,0xD2,0x5E,0x8C,0xD0,0x36,0x41,0x42
};
// ============================================================================
// 1. Scalar zero rejection paths
// ============================================================================
static void test_scalar_zero_rejection() {
std::printf("\n=== Scalar zero rejection ===\n");
const auto zero = Scalar::zero();
CHECK(zero.is_zero(), "Scalar::zero() is zero");
const auto from0 = Scalar::from_uint64(0);
CHECK(from0.is_zero(), "Scalar::from_uint64(0) is zero");
// parse_bytes_strict_nonzero must reject zero
const std::array<uint8_t, 32> zero_bytes{};
Scalar out{};
bool ok = Scalar::parse_bytes_strict_nonzero(zero_bytes, out);
CHECK(!ok, "parse_bytes_strict_nonzero rejects zero");
// parse_bytes_strict accepts zero (it only rejects >= n)
ok = Scalar::parse_bytes_strict(zero_bytes, out);
CHECK(ok, "parse_bytes_strict accepts zero (valid < n)");
CHECK(out.is_zero(), "parse_bytes_strict(0) returns zero scalar");
}
// ============================================================================
// 2. parse_bytes_strict boundary values
// ============================================================================
static void test_scalar_parse_boundaries() {
std::printf("\n=== Scalar parse_bytes_strict boundaries ===\n");
Scalar out{};
// n-1: must be accepted (valid scalar)
bool ok = Scalar::parse_bytes_strict(ORDER_N_MINUS_1, out);
CHECK(ok, "parse_bytes_strict accepts n-1");
CHECK(!out.is_zero(), "n-1 is nonzero");
// n: must be rejected
ok = Scalar::parse_bytes_strict(ORDER_N, out);
CHECK(!ok, "parse_bytes_strict rejects n (== order)");
// n+1: must be rejected
ok = Scalar::parse_bytes_strict(ORDER_N_PLUS_1, out);
CHECK(!ok, "parse_bytes_strict rejects n+1 (> order)");
// all 0xFF: must be rejected
std::array<uint8_t, 32> all_ff{};
std::memset(all_ff.data(), 0xFF, 32);
ok = Scalar::parse_bytes_strict(all_ff, out);
CHECK(!ok, "parse_bytes_strict rejects 0xFF..FF");
// parse_bytes_strict_nonzero: n-1 must be accepted
ok = Scalar::parse_bytes_strict_nonzero(ORDER_N_MINUS_1, out);
CHECK(ok, "parse_bytes_strict_nonzero accepts n-1");
// parse_bytes_strict_nonzero: n must be rejected
ok = Scalar::parse_bytes_strict_nonzero(ORDER_N, out);
CHECK(!ok, "parse_bytes_strict_nonzero rejects n");
}
// ============================================================================
// 3. Point at infinity arithmetic
// ============================================================================
static void test_infinity_arithmetic() {
std::printf("\n=== Infinity arithmetic ===\n");
const Point O = Point::infinity();
const Point G = Point::generator();
// O + O = O
const Point OO = O.add(O);
CHECK(OO.is_infinity(), "O + O = O");
// dbl(O) = O
const Point dblO = O.dbl();
CHECK(dblO.is_infinity(), "dbl(O) = O");
// O + G = G
const Point OG = O.add(G);
CHECK(!OG.is_infinity(), "O + G is not infinity");
CHECK(OG.to_compressed() == G.to_compressed(), "O + G = G");
// G + O = G
const Point GO = G.add(O);
CHECK(!GO.is_infinity(), "G + O is not infinity");
CHECK(GO.to_compressed() == G.to_compressed(), "G + O = G");
// (n-1)*G + G = O (another way to get infinity)
const Scalar nm1 = Scalar::from_bytes(ORDER_N_MINUS_1);
const Point negG = G.scalar_mul(nm1);
CHECK(!negG.is_infinity(), "(n-1)*G is not infinity");
const Point should_be_O = negG.add(G);
CHECK(should_be_O.is_infinity(), "(n-1)*G + G = O");
// Verify (n-1)*G = -G (negation)
const Point minusG = G.negate();
CHECK(negG.to_compressed() == minusG.to_compressed(), "(n-1)*G == -G");
}
// ============================================================================
// 4. ECDSA signing with zero/boundary keys
// ============================================================================
static void test_ecdsa_zero_key() {
std::printf("\n=== ECDSA zero/boundary key tests ===\n");
std::array<uint8_t, 32> msg{};
msg[0] = 0x42; // non-zero message hash
// Sign with valid key, verify it works
const Scalar valid_key = Scalar::from_uint64(1);
const auto sig = ecdsa_sign(msg, valid_key);
const Point pub = Point::generator().scalar_mul(valid_key);
const bool valid = ecdsa_verify(msg, pub, sig);
CHECK(valid, "ECDSA sign+verify with k=1");
// Sign with n-1 key, should work
const Scalar nm1_key = Scalar::from_bytes(ORDER_N_MINUS_1);
const auto sig_nm1 = ecdsa_sign(msg, nm1_key);
const Point pub_nm1 = Point::generator().scalar_mul(nm1_key);
const bool valid_nm1 = ecdsa_verify(msg, pub_nm1, sig_nm1);
CHECK(valid_nm1, "ECDSA sign+verify with k=n-1");
// Verify with wrong key should fail
const Scalar wrong_key = Scalar::from_uint64(2);
const Point wrong_pub = Point::generator().scalar_mul(wrong_key);
const bool wrong = ecdsa_verify(msg, wrong_pub, sig);
CHECK(!wrong, "ECDSA verify with wrong key fails");
}
// ============================================================================
// 5. Schnorr signing boundary tests
// ============================================================================
static void test_schnorr_boundary() {
std::printf("\n=== Schnorr boundary key tests ===\n");
std::array<uint8_t, 32> msg{};
msg[0] = 0xAB;
std::array<uint8_t, 32> aux{};
aux[0] = 0xCD;
// Sign with k=1
const Scalar k1 = Scalar::from_uint64(1);
const auto sig1 = schnorr_sign(k1, msg, aux);
auto pub1_x = Point::generator().scalar_mul(k1).x_only_bytes();
const bool v1 = schnorr_verify(pub1_x, msg, sig1);
CHECK(v1, "Schnorr sign+verify with k=1");
// Sign with k=n-1
const Scalar knm1 = Scalar::from_bytes(ORDER_N_MINUS_1);
const auto sig_nm1 = schnorr_sign(knm1, msg, aux);
auto pub_nm1_x = Point::generator().scalar_mul(knm1).x_only_bytes();
const bool v_nm1 = schnorr_verify(pub_nm1_x, msg, sig_nm1);
CHECK(v_nm1, "Schnorr sign+verify with k=n-1");
}
// ============================================================================
// 6. BIP-32 IL >= n rejection
// ============================================================================
static void test_bip32_il_geq_n() {
std::printf("\n=== BIP-32 IL >= n rejection ===\n");
// Create a valid master key from a known seed
const uint8_t seed[16] = {
0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,
0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F
};
auto [master, ok] = bip32_master_key(seed, 16);
CHECK(ok, "BIP-32 master key from 16-byte seed");
// Derive a valid child (index 0)
auto [child0, ok0] = master.derive_child(0);
CHECK(ok0, "BIP-32 child derivation index=0 succeeds");
CHECK(child0.depth == 1, "BIP-32 child depth = 1");
// Derive multiple children to exercise the loop
bool all_ok = true;
for (uint32_t i = 0; i < 10; ++i) {
auto [child, cok] = master.derive_child(i);
(void)child;
if (!cok) { all_ok = false; break; }
}
CHECK(all_ok, "BIP-32 derive 10 children all succeed");
// Hardened derivation
auto [hchild, hok] = master.derive_hardened(0);
(void)hchild;
CHECK(hok, "BIP-32 hardened child index=0 succeeds");
// Seed too short (< 16 bytes)
auto [bad_master, bad_ok] = bip32_master_key(seed, 15);
(void)bad_master;
CHECK(!bad_ok, "BIP-32 rejects seed < 16 bytes");
// Seed too long (> 64 bytes)
uint8_t long_seed[65] = {};
auto [bad_master2, bad_ok2] = bip32_master_key(long_seed, 65);
(void)bad_master2;
CHECK(!bad_ok2, "BIP-32 rejects seed > 64 bytes");
}
// ============================================================================
// 7. Precompute cache corruption recovery
// ============================================================================
static void test_precompute_cache_corrupt() {
std::printf("\n=== Precompute cache corruption recovery ===\n");
// Loading from nonexistent file should return false
bool ok = load_precompute_cache("/tmp/nonexistent_secp256k1_cache_xyz.bin");
CHECK(!ok, "load_precompute_cache rejects nonexistent file");
// Create a truncated/corrupt cache file
{
std::ofstream f("/tmp/secp256k1_test_corrupt_cache.bin", std::ios::binary);
const char garbage[] = "not a valid cache header";
f.write(garbage, sizeof(garbage));
}
ok = load_precompute_cache("/tmp/secp256k1_test_corrupt_cache.bin");
CHECK(!ok, "load_precompute_cache rejects corrupt file");
// Create a file with valid magic but truncated data
{
std::ofstream f("/tmp/secp256k1_test_trunc_cache.bin", std::ios::binary);
// Write 8 bytes (likely wrong magic + version)
uint64_t fake_header = 0;
f.write(reinterpret_cast<const char*>(&fake_header), 8);
}
ok = load_precompute_cache("/tmp/secp256k1_test_trunc_cache.bin");
CHECK(!ok, "load_precompute_cache rejects truncated file");
// Cleanup temp files
(void)std::remove("/tmp/secp256k1_test_corrupt_cache.bin");
(void)std::remove("/tmp/secp256k1_test_trunc_cache.bin");
}
// ============================================================================
// 8. Scalar arithmetic edge cases
// ============================================================================
static void test_scalar_arithmetic_edges() {
std::printf("\n=== Scalar arithmetic edges ===\n");
const Scalar zero = Scalar::zero();
const Scalar one = Scalar::from_uint64(1);
const Scalar two = Scalar::from_uint64(2);
const Scalar nm1 = Scalar::from_bytes(ORDER_N_MINUS_1);
// 0 + 1 = 1
const Scalar sum01 = zero + one;
CHECK(sum01.to_bytes() == one.to_bytes(), "0 + 1 = 1");
// n-1 + 1 = 0 (mod n)
const Scalar wrap = nm1 + one;
CHECK(wrap.is_zero(), "(n-1) + 1 = 0 mod n");
// n-1 + 2 = 1 (mod n)
const Scalar wrap2 = nm1 + two;
CHECK(wrap2.to_bytes() == one.to_bytes(), "(n-1) + 2 = 1 mod n");
// 1 * 0 = 0
const Scalar prod0 = one * zero;
CHECK(prod0.is_zero(), "1 * 0 = 0");
// 1 * 1 = 1
const Scalar prod1 = one * one;
CHECK(prod1.to_bytes() == one.to_bytes(), "1 * 1 = 1");
// negate(0) = 0
const Scalar neg0 = zero.negate();
CHECK(neg0.is_zero(), "negate(0) = 0");
// negate(1) = n-1
const Scalar neg1 = one.negate();
CHECK(neg1.to_bytes() == nm1.to_bytes(), "negate(1) = n-1");
// negate(n-1) = 1
const Scalar neg_nm1 = nm1.negate();
CHECK(neg_nm1.to_bytes() == one.to_bytes(), "negate(n-1) = 1");
}
// ============================================================================
// 9. Field element edge cases
// ============================================================================
static void test_field_edge_cases() {
std::printf("\n=== Field element edge cases ===\n");
const auto zero = FieldElement::zero();
const auto one = FieldElement::one();
// 0 * 0 = 0
const auto prod00 = zero * zero;
CHECK(prod00 == zero, "FE: 0 * 0 = 0");
// 1 * 1 = 1
const auto prod11 = one * one;
CHECK(prod11 == one, "FE: 1 * 1 = 1");
// 0 * 1 = 0
const auto prod01 = zero * one;
CHECK(prod01 == zero, "FE: 0 * 1 = 0");
// 0 + 0 = 0
const auto sum00 = zero + zero;
CHECK(sum00 == zero, "FE: 0 + 0 = 0");
// 1 + 0 = 1
const auto sum10 = one + zero;
CHECK(sum10 == one, "FE: 1 + 0 = 1");
// a - a = 0 (intentionally same operand on both sides)
const auto other_one = FieldElement::from_uint64(1);
const auto sub_aa = one - other_one;
CHECK(sub_aa == zero, "FE: 1 - 1 = 0");
// negate(0) = 0
const auto neg0 = zero.negate();
CHECK(neg0 == zero, "FE: negate(0) = 0");
// square(0) = 0
const auto sq0 = zero.square();
CHECK(sq0 == zero, "FE: square(0) = 0");
// square(1) = 1
const auto sq1 = one.square();
CHECK(sq1 == one, "FE: square(1) = 1");
}
// ============================================================================
// 10. ECDSASignature parse_compact_strict boundaries
// ============================================================================
static void test_ecdsa_sig_parse_boundaries() {
std::printf("\n=== ECDSA signature parse boundaries ===\n");
// Zero signature must be rejected (r=0)
const std::array<uint8_t, 64> zero_sig{};
ECDSASignature out{};
bool ok = ECDSASignature::parse_compact_strict(zero_sig, out);
CHECK(!ok, "parse_compact_strict rejects zero sig (r=0,s=0)");
// r=1, s=0 must be rejected
std::array<uint8_t, 64> r1s0{};
r1s0[31] = 0x01;
ok = ECDSASignature::parse_compact_strict(r1s0, out);
CHECK(!ok, "parse_compact_strict rejects r=1,s=0");
// r=0, s=1 must be rejected
std::array<uint8_t, 64> r0s1{};
r0s1[63] = 0x01;
ok = ECDSASignature::parse_compact_strict(r0s1, out);
CHECK(!ok, "parse_compact_strict rejects r=0,s=1");
// r=1, s=1 must be accepted
std::array<uint8_t, 64> r1s1{};
r1s1[31] = 0x01;
r1s1[63] = 0x01;
ok = ECDSASignature::parse_compact_strict(r1s1, out);
CHECK(ok, "parse_compact_strict accepts r=1,s=1");
// r=n, s=1 must be rejected
std::array<uint8_t, 64> rns1{};
std::memcpy(rns1.data(), ORDER_N.data(), 32);
rns1[63] = 0x01;
ok = ECDSASignature::parse_compact_strict(rns1, out);
CHECK(!ok, "parse_compact_strict rejects r=n");
// r=1, s=n must be rejected
std::array<uint8_t, 64> r1sn{};
r1sn[31] = 0x01;
std::memcpy(r1sn.data() + 32, ORDER_N.data(), 32);
ok = ECDSASignature::parse_compact_strict(r1sn, out);
CHECK(!ok, "parse_compact_strict rejects s=n");
// r=n-1, s=n-1 must be accepted
std::array<uint8_t, 64> rnm1snm1{};
std::memcpy(rnm1snm1.data(), ORDER_N_MINUS_1.data(), 32);
std::memcpy(rnm1snm1.data() + 32, ORDER_N_MINUS_1.data(), 32);
ok = ECDSASignature::parse_compact_strict(rnm1snm1, out);
CHECK(ok, "parse_compact_strict accepts r=n-1,s=n-1");
}
// ============================================================================
// Entry point (matches test runner pattern)
// ============================================================================
int test_edge_cases_run() {
g_tests_run = 0;
g_tests_passed = 0;
test_scalar_zero_rejection();
test_scalar_parse_boundaries();
test_infinity_arithmetic();
test_ecdsa_zero_key();
test_schnorr_boundary();
test_bip32_il_geq_n();
test_precompute_cache_corrupt();
test_scalar_arithmetic_edges();
test_field_edge_cases();
test_ecdsa_sig_parse_boundaries();
std::printf("\n--- Edge case summary: %d/%d passed ---\n",
g_tests_passed, g_tests_run);
return (g_tests_passed == g_tests_run) ? 0 : 1;
}
#ifdef STANDALONE_TEST
int main() {
return test_edge_cases_run();
}
#endif