feat: v2.2.0 — ECDSA (RFC 6979), Schnorr (BIP-340), SHA-256, CI/CD, fuzzing, CT bench
New features: - ECDSA sign/verify with RFC 6979 deterministic nonce (HMAC-SHA256) - Schnorr BIP-340 sign/verify with tagged hashing & x-only pubkeys - Self-contained SHA-256 (header-only, no deps) - Scalar::inverse (Fermat's theorem), negate(), is_even() Build & tooling: - GitHub Actions CI: Linux (gcc-13/clang-17), Windows (MSVC), macOS - .clang-format + .editorconfig for code style - cmake/version.hpp.in auto-generated version header - CMake VERSION 1.0.0 -> 2.2.0 - Fixed install(TARGETS/EXPORT) — moved to cpu/ where target is defined - MSVC compatibility via SECP256K1_NO_INT128 generator expression - Fixed BUILD_TESTING standalone build Testing & benchmarks: - test_ecdsa_schnorr: 22 checks (SHA-256 NIST, inverse, ECDSA, Schnorr) - bench_ct: CT vs fast comparison for all primitives - 3 libFuzzer harnesses (field, scalar, point) - Desktop CLI example (examples/basic_usage) All 5 CTest targets pass (22/22 ECDSA+Schnorr checks).
This commit is contained in:
parent
a9c835ec9f
commit
c347bae204
60
.clang-format
Normal file
60
.clang-format
Normal file
@ -0,0 +1,60 @@
|
||||
---
|
||||
Language: Cpp
|
||||
BasedOnStyle: Google
|
||||
|
||||
# Indentation
|
||||
IndentWidth: 4
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
ContinuationIndentWidth: 4
|
||||
NamespaceIndentation: None
|
||||
IndentCaseLabels: true
|
||||
IndentPPDirectives: AfterHash
|
||||
|
||||
# Line length
|
||||
ColumnLimit: 120
|
||||
|
||||
# Braces
|
||||
BreakBeforeBraces: Attach
|
||||
AllowShortFunctionsOnASingleLine: Inline
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
|
||||
# Alignment
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: true
|
||||
|
||||
# Includes
|
||||
SortIncludes: CaseInsensitive
|
||||
IncludeBlocks: Regroup
|
||||
IncludeCategories:
|
||||
# Project headers
|
||||
- Regex: '^"secp256k1/'
|
||||
Priority: 2
|
||||
# Local headers
|
||||
- Regex: '^"'
|
||||
Priority: 3
|
||||
# System headers
|
||||
- Regex: '^<'
|
||||
Priority: 1
|
||||
|
||||
# Spacing
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpacesInAngles: false
|
||||
SpacesInParentheses: false
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
|
||||
# Other
|
||||
PointerAlignment: Left
|
||||
DerivePointerAlignment: false
|
||||
ReflowComments: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
BinPackParameters: true
|
||||
BinPackArguments: true
|
||||
|
||||
# C++20
|
||||
Standard: c++20
|
||||
...
|
||||
28
.editorconfig
Normal file
28
.editorconfig
Normal file
@ -0,0 +1,28 @@
|
||||
# EditorConfig — https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{hpp,cpp,h,c,cu,cuh}]
|
||||
indent_size = 4
|
||||
|
||||
[CMakeLists.txt]
|
||||
indent_size = 2
|
||||
|
||||
[*.cmake]
|
||||
indent_size = 2
|
||||
|
||||
[*.{json,yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
101
.github/workflows/ci.yml
vendored
Normal file
101
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,101 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
pull_request:
|
||||
branches: [main, dev]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# ── Linux (GCC + Clang) ────────────────────────────────────────────────────
|
||||
linux:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
compiler: [gcc-13, clang-17]
|
||||
build_type: [Release, Debug]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install compiler
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
if [[ "${{ matrix.compiler }}" == gcc-* ]]; then
|
||||
sudo apt-get install -y g++-13
|
||||
else
|
||||
sudo apt-get install -y clang-17 lld-17
|
||||
fi
|
||||
|
||||
- name: Configure
|
||||
run: |
|
||||
if [[ "${{ matrix.compiler }}" == gcc-* ]]; then
|
||||
export CC=gcc-13 CXX=g++-13
|
||||
else
|
||||
export CC=clang-17 CXX=clang++-17
|
||||
fi
|
||||
cmake -S . -B build \
|
||||
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
|
||||
-DSECP256K1_BUILD_TESTS=ON \
|
||||
-DSECP256K1_BUILD_BENCH=ON \
|
||||
-DSECP256K1_BUILD_EXAMPLES=ON
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build -j$(nproc)
|
||||
|
||||
- name: Test
|
||||
run: ctest --test-dir build --output-on-failure -j$(nproc)
|
||||
|
||||
# ── Windows (MSVC + Clang-cl) ──────────────────────────────────────────────
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build_type: [Release]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Configure (MSVC)
|
||||
run: |
|
||||
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 `
|
||||
-DSECP256K1_BUILD_TESTS=ON `
|
||||
-DSECP256K1_BUILD_BENCH=ON `
|
||||
-DSECP256K1_BUILD_EXAMPLES=ON
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build --config ${{ matrix.build_type }} -j
|
||||
|
||||
- name: Test
|
||||
run: ctest --test-dir build -C ${{ matrix.build_type }} --output-on-failure -j
|
||||
|
||||
# ── macOS (AppleClang) ─────────────────────────────────────────────────────
|
||||
macos:
|
||||
runs-on: macos-14
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
build_type: [Release]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Configure
|
||||
run: |
|
||||
cmake -S . -B build \
|
||||
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
|
||||
-DSECP256K1_BUILD_TESTS=ON \
|
||||
-DSECP256K1_BUILD_BENCH=ON \
|
||||
-DSECP256K1_BUILD_EXAMPLES=ON
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build -j$(sysctl -n hw.logicalcpu)
|
||||
|
||||
- name: Test
|
||||
run: ctest --test-dir build --output-on-failure -j$(sysctl -n hw.logicalcpu)
|
||||
@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
|
||||
project(UltrafastSecp256k1
|
||||
VERSION 1.0.0
|
||||
VERSION 2.2.0
|
||||
DESCRIPTION "Ultra high-performance secp256k1 elliptic curve cryptography library"
|
||||
LANGUAGES CXX
|
||||
)
|
||||
@ -15,6 +15,13 @@ set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
# Generate version header from template
|
||||
configure_file(
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/version.hpp.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/include/secp256k1/version.hpp"
|
||||
@ONLY
|
||||
)
|
||||
|
||||
# Build options
|
||||
option(SECP256K1_BUILD_CPU "Build CPU implementation" ON)
|
||||
option(SECP256K1_BUILD_CUDA "Build CUDA GPU support" OFF)
|
||||
@ -81,6 +88,11 @@ endif()
|
||||
|
||||
# Subdirectories
|
||||
if(SECP256K1_BUILD_CPU)
|
||||
# Enable testing so cpu/CMakeLists.txt BUILD_TESTING guard works
|
||||
if(SECP256K1_BUILD_TESTS OR SECP256K1_BUILD_BENCH)
|
||||
set(BUILD_TESTING ON CACHE BOOL "Enable testing" FORCE)
|
||||
enable_testing()
|
||||
endif()
|
||||
add_subdirectory(cpu)
|
||||
endif()
|
||||
|
||||
@ -124,7 +136,18 @@ endif()
|
||||
if(SECP256K1_INSTALL)
|
||||
include(GNUInstallDirs)
|
||||
include(CMakePackageConfigHelpers)
|
||||
|
||||
|
||||
# Install public headers
|
||||
install(DIRECTORY cpu/include/secp256k1
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||
FILES_MATCHING PATTERN "*.hpp"
|
||||
)
|
||||
|
||||
# Install generated version header
|
||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/include/secp256k1/version.hpp"
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/secp256k1
|
||||
)
|
||||
|
||||
# Generate package config
|
||||
write_basic_package_version_file(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/secp256k1-fast-config-version.cmake"
|
||||
|
||||
24
cmake/version.hpp.in
Normal file
24
cmake/version.hpp.in
Normal file
@ -0,0 +1,24 @@
|
||||
#ifndef SECP256K1_VERSION_HPP_IN
|
||||
#define SECP256K1_VERSION_HPP_IN
|
||||
#pragma once
|
||||
|
||||
// Auto-generated by CMake — DO NOT EDIT
|
||||
// Source: cmake/version.hpp.in
|
||||
|
||||
#define SECP256K1_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
|
||||
#define SECP256K1_VERSION_MINOR @PROJECT_VERSION_MINOR@
|
||||
#define SECP256K1_VERSION_PATCH @PROJECT_VERSION_PATCH@
|
||||
#define SECP256K1_VERSION_STRING "@PROJECT_VERSION@"
|
||||
|
||||
namespace secp256k1 {
|
||||
|
||||
struct Version {
|
||||
static constexpr int major = @PROJECT_VERSION_MAJOR@;
|
||||
static constexpr int minor = @PROJECT_VERSION_MINOR@;
|
||||
static constexpr int patch = @PROJECT_VERSION_PATCH@;
|
||||
static constexpr const char* string = "@PROJECT_VERSION@";
|
||||
};
|
||||
|
||||
} // namespace secp256k1
|
||||
|
||||
#endif // SECP256K1_VERSION_HPP_IN
|
||||
@ -14,6 +14,8 @@ set(SECP256K1_SOURCES
|
||||
src/ct_field.cpp # CT field arithmetic (side-channel resistant)
|
||||
src/ct_scalar.cpp # CT scalar arithmetic
|
||||
src/ct_point.cpp # CT point ops (complete addition, CT scalar_mul)
|
||||
src/ecdsa.cpp # ECDSA sign/verify + RFC 6979
|
||||
src/schnorr.cpp # Schnorr BIP-340 sign/verify
|
||||
)
|
||||
|
||||
# Optional inline assembly (Tier 3: Maximum performance, x64/RISC-V)
|
||||
@ -247,18 +249,34 @@ if(NOT TARGET ${SECP256K1_LIB_NAME})
|
||||
PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include>
|
||||
$<BUILD_INTERFACE:${CMAKE_BINARY_DIR}/include>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
PRIVATE
|
||||
${CMAKE_SOURCE_DIR} # For rdtsc.h and intrinsics.h
|
||||
)
|
||||
|
||||
target_compile_features(${SECP256K1_LIB_NAME} PUBLIC cxx_std_20)
|
||||
|
||||
# Install target
|
||||
if(SECP256K1_INSTALL)
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS ${SECP256K1_LIB_NAME}
|
||||
EXPORT secp256k1-fast-targets
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
install(EXPORT secp256k1-fast-targets
|
||||
FILE secp256k1-fast-targets.cmake
|
||||
NAMESPACE secp256k1::
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/secp256k1-fast
|
||||
)
|
||||
endif()
|
||||
target_compile_definitions(${SECP256K1_LIB_NAME}
|
||||
PUBLIC
|
||||
SECP256K1_FAST_NO_SECURITY_CHECKS=1
|
||||
SECP256K1_ULTRA_SPEED=1 # Maximum speed mode
|
||||
$<$<CONFIG:Release>:NDEBUG> # Remove assertions only in Release
|
||||
$<$<CXX_COMPILER_ID:MSVC>:SECP256K1_NO_INT128=1> # MSVC has no __int128
|
||||
)
|
||||
|
||||
# Optional inline assembly support (x64 only)
|
||||
@ -422,6 +440,10 @@ if(BUILD_TESTING)
|
||||
add_executable(bench_atomic_operations bench/bench_atomic_operations.cpp)
|
||||
target_link_libraries(bench_atomic_operations PRIVATE ${SECP256K1_LIB_NAME})
|
||||
|
||||
# CT (Constant-Time) layer benchmark — fast:: vs ct:: overhead comparison
|
||||
add_executable(bench_ct bench/bench_ct.cpp)
|
||||
target_link_libraries(bench_ct PRIVATE ${SECP256K1_LIB_NAME})
|
||||
|
||||
# Comprehensive cross-platform benchmark (all operations for README performance table)
|
||||
add_executable(bench_comprehensive bench/bench_comprehensive_riscv.cpp)
|
||||
target_link_libraries(bench_comprehensive PRIVATE ${SECP256K1_LIB_NAME})
|
||||
@ -469,4 +491,9 @@ if(BUILD_TESTING)
|
||||
target_link_options(test_ct PRIVATE "LINKER:/STACK:8388608")
|
||||
endif()
|
||||
add_test(NAME test_ct COMMAND test_ct)
|
||||
|
||||
# Test: ECDSA + Schnorr (BIP-340) sign/verify
|
||||
add_executable(test_ecdsa_schnorr tests/test_ecdsa_schnorr.cpp)
|
||||
target_link_libraries(test_ecdsa_schnorr PRIVATE ${SECP256K1_LIB_NAME})
|
||||
add_test(NAME test_ecdsa_schnorr COMMAND test_ecdsa_schnorr)
|
||||
endif()
|
||||
|
||||
198
cpu/bench/bench_ct.cpp
Normal file
198
cpu/bench/bench_ct.cpp
Normal file
@ -0,0 +1,198 @@
|
||||
// ============================================================================
|
||||
// Constant-Time (CT) Layer Benchmarks
|
||||
// ============================================================================
|
||||
// Benchmarks for side-channel-resistant operations.
|
||||
// Compare fast:: vs ct:: to quantify the protection cost.
|
||||
// ============================================================================
|
||||
|
||||
#include "secp256k1/field.hpp"
|
||||
#include "secp256k1/scalar.hpp"
|
||||
#include "secp256k1/point.hpp"
|
||||
#include "secp256k1/selftest.hpp"
|
||||
#include "secp256k1/ct/field.hpp"
|
||||
#include "secp256k1/ct/scalar.hpp"
|
||||
#include "secp256k1/ct/point.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
|
||||
using namespace secp256k1::fast;
|
||||
namespace ct = secp256k1::ct;
|
||||
|
||||
// ── timing helper ────────────────────────────────────────────────────────────
|
||||
|
||||
template <typename Func>
|
||||
static double bench_us(Func&& f, int iters) {
|
||||
// warmup
|
||||
for (int i = 0; i < 10; ++i) f();
|
||||
|
||||
auto t0 = std::chrono::high_resolution_clock::now();
|
||||
for (int i = 0; i < iters; ++i) f();
|
||||
auto t1 = std::chrono::high_resolution_clock::now();
|
||||
double us = std::chrono::duration<double, std::micro>(t1 - t0).count();
|
||||
return us / iters;
|
||||
}
|
||||
|
||||
// ── main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
int main() {
|
||||
printf("================================================================\n");
|
||||
printf(" CT Layer Benchmark (fast:: vs ct::)\n");
|
||||
printf("================================================================\n\n");
|
||||
|
||||
// Fixed test data
|
||||
auto G = Point::generator();
|
||||
auto k = Scalar::from_hex(
|
||||
"e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35");
|
||||
auto k2 = Scalar::from_hex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000002");
|
||||
auto P = G.scalar_mul(k);
|
||||
|
||||
auto fe_a = FieldElement::from_hex(
|
||||
"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798");
|
||||
auto fe_b = FieldElement::from_hex(
|
||||
"483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8");
|
||||
|
||||
constexpr int N_SCALAR_MUL = 50;
|
||||
constexpr int N_POINT_OPS = 5000;
|
||||
constexpr int N_FIELD_OPS = 50000;
|
||||
constexpr int N_SCALAR_OPS = 50000;
|
||||
|
||||
// ── Field operations ─────────────────────────────────────────────────────
|
||||
|
||||
printf("--- Field Arithmetic ---\n");
|
||||
|
||||
double fast_field_mul = bench_us([&]() {
|
||||
volatile auto r = fe_a * fe_b;
|
||||
}, N_FIELD_OPS);
|
||||
|
||||
double ct_field_mul = bench_us([&]() {
|
||||
auto r = ct::field_mul(fe_a, fe_b);
|
||||
(void)r;
|
||||
}, N_FIELD_OPS);
|
||||
|
||||
printf(" field_mul fast: %8.3f us ct: %8.3f us ratio: %.2fx\n",
|
||||
fast_field_mul, ct_field_mul, ct_field_mul / fast_field_mul);
|
||||
|
||||
double fast_field_sq = bench_us([&]() {
|
||||
volatile auto r = fe_a.square();
|
||||
}, N_FIELD_OPS);
|
||||
|
||||
double ct_field_sq = bench_us([&]() {
|
||||
auto r = ct::field_sqr(fe_a);
|
||||
(void)r;
|
||||
}, N_FIELD_OPS);
|
||||
|
||||
printf(" field_square fast: %8.3f us ct: %8.3f us ratio: %.2fx\n",
|
||||
fast_field_sq, ct_field_sq, ct_field_sq / fast_field_sq);
|
||||
|
||||
double fast_field_inv = bench_us([&]() {
|
||||
volatile auto r = fe_a.inverse();
|
||||
}, N_FIELD_OPS / 10);
|
||||
|
||||
double ct_field_inv = bench_us([&]() {
|
||||
auto r = ct::field_inv(fe_a);
|
||||
(void)r;
|
||||
}, N_FIELD_OPS / 10);
|
||||
|
||||
printf(" field_inv fast: %8.3f us ct: %8.3f us ratio: %.2fx\n\n",
|
||||
fast_field_inv, ct_field_inv, ct_field_inv / fast_field_inv);
|
||||
|
||||
// ── Scalar operations ────────────────────────────────────────────────────
|
||||
|
||||
printf("--- Scalar Arithmetic ---\n");
|
||||
|
||||
double fast_scalar_add = bench_us([&]() {
|
||||
volatile auto r = k + k2;
|
||||
}, N_SCALAR_OPS);
|
||||
|
||||
double ct_scalar_add = bench_us([&]() {
|
||||
auto r = ct::scalar_add(k, k2);
|
||||
(void)r;
|
||||
}, N_SCALAR_OPS);
|
||||
|
||||
printf(" scalar_add fast: %8.3f us ct: %8.3f us ratio: %.2fx\n",
|
||||
fast_scalar_add, ct_scalar_add, ct_scalar_add / fast_scalar_add);
|
||||
|
||||
double fast_scalar_sub = bench_us([&]() {
|
||||
volatile auto r = k - k2;
|
||||
}, N_SCALAR_OPS);
|
||||
|
||||
double ct_scalar_sub = bench_us([&]() {
|
||||
auto r = ct::scalar_sub(k, k2);
|
||||
(void)r;
|
||||
}, N_SCALAR_OPS);
|
||||
|
||||
printf(" scalar_sub fast: %8.3f us ct: %8.3f us ratio: %.2fx\n\n",
|
||||
fast_scalar_sub, ct_scalar_sub, ct_scalar_sub / fast_scalar_sub);
|
||||
|
||||
// ── Point operations ─────────────────────────────────────────────────────
|
||||
|
||||
printf("--- Point Operations ---\n");
|
||||
|
||||
auto ct_p = ct::CTJacobianPoint::from_point(P);
|
||||
auto ct_g = ct::CTJacobianPoint::from_point(G);
|
||||
|
||||
double fast_point_add = bench_us([&]() {
|
||||
volatile auto r = P.add(G);
|
||||
}, N_POINT_OPS);
|
||||
|
||||
double ct_point_add = bench_us([&]() {
|
||||
auto r = ct::point_add_complete(ct_p, ct_g);
|
||||
(void)r;
|
||||
}, N_POINT_OPS);
|
||||
|
||||
printf(" point_add fast: %8.3f us ct: %8.3f us ratio: %.2fx\n",
|
||||
fast_point_add, ct_point_add, ct_point_add / fast_point_add);
|
||||
|
||||
double fast_point_dbl = bench_us([&]() {
|
||||
volatile auto r = P.dbl();
|
||||
}, N_POINT_OPS);
|
||||
|
||||
double ct_point_dbl = bench_us([&]() {
|
||||
auto r = ct::point_dbl(ct_p);
|
||||
(void)r;
|
||||
}, N_POINT_OPS);
|
||||
|
||||
printf(" point_dbl fast: %8.3f us ct: %8.3f us ratio: %.2fx\n\n",
|
||||
fast_point_dbl, ct_point_dbl, ct_point_dbl / fast_point_dbl);
|
||||
|
||||
// ── Scalar multiplication ────────────────────────────────────────────────
|
||||
|
||||
printf("--- Scalar Multiplication (k * P) ---\n");
|
||||
|
||||
double fast_mul = bench_us([&]() {
|
||||
volatile auto r = P.scalar_mul(k);
|
||||
}, N_SCALAR_MUL);
|
||||
|
||||
double ct_mul = bench_us([&]() {
|
||||
auto r = ct::scalar_mul(P, k);
|
||||
(void)r;
|
||||
}, N_SCALAR_MUL);
|
||||
|
||||
printf(" scalar_mul fast: %8.1f us ct: %8.1f us ratio: %.2fx\n\n",
|
||||
fast_mul, ct_mul, ct_mul / fast_mul);
|
||||
|
||||
// ── Generator multiplication ─────────────────────────────────────────────
|
||||
|
||||
printf("--- Generator Multiplication (k * G) ---\n");
|
||||
|
||||
double fast_gen = bench_us([&]() {
|
||||
volatile auto r = G.scalar_mul(k);
|
||||
}, N_SCALAR_MUL);
|
||||
|
||||
double ct_gen = bench_us([&]() {
|
||||
auto r = ct::generator_mul(k);
|
||||
(void)r;
|
||||
}, N_SCALAR_MUL);
|
||||
|
||||
printf(" generator_mul fast: %7.1f us ct: %8.1f us ratio: %.2fx\n\n",
|
||||
fast_gen, ct_gen, ct_gen / fast_gen);
|
||||
|
||||
printf("================================================================\n");
|
||||
printf(" Lower ratio = smaller CT overhead (1.0x = same speed)\n");
|
||||
printf("================================================================\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
52
cpu/fuzz/fuzz_field.cpp
Normal file
52
cpu/fuzz/fuzz_field.cpp
Normal file
@ -0,0 +1,52 @@
|
||||
// ============================================================================
|
||||
// Fuzz target: Field arithmetic
|
||||
// ============================================================================
|
||||
// Build: clang++ -fsanitize=fuzzer,address -O2 -std=c++20 \
|
||||
// -I cpu/include fuzz_field.cpp cpu/src/field.cpp cpu/src/field_asm.cpp \
|
||||
// -o fuzz_field
|
||||
// Run: ./fuzz_field -max_len=64 -runs=10000000
|
||||
// ============================================================================
|
||||
|
||||
#include "secp256k1/field.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
|
||||
using secp256k1::fast::FieldElement;
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
if (size < 64) return 0; // need two 32-byte field elements
|
||||
|
||||
std::array<uint8_t, 32> buf_a{}, buf_b{};
|
||||
__builtin_memcpy(buf_a.data(), data, 32);
|
||||
__builtin_memcpy(buf_b.data(), data + 32, 32);
|
||||
|
||||
auto a = FieldElement::from_bytes(buf_a);
|
||||
auto b = FieldElement::from_bytes(buf_b);
|
||||
|
||||
// ── Closure: add/sub round-trip ──────────────────────────────────────────
|
||||
auto c = a + b;
|
||||
auto d = c - b;
|
||||
auto a_rt = d.to_bytes();
|
||||
auto a_orig = a.to_bytes();
|
||||
if (a_rt != a_orig) __builtin_trap();
|
||||
|
||||
// ── Closure: mul by 1 = identity ─────────────────────────────────────────
|
||||
auto one = FieldElement::one();
|
||||
auto e = a * one;
|
||||
if (e.to_bytes() != a_orig) __builtin_trap();
|
||||
|
||||
// ── Closure: a * a == a.square() ─────────────────────────────────────────
|
||||
auto f = a * a;
|
||||
auto g = a.square();
|
||||
if (f.to_bytes() != g.to_bytes()) __builtin_trap();
|
||||
|
||||
// ── Closure: a * a^-1 == 1 (if a != 0) ──────────────────────────────────
|
||||
if (a != FieldElement::zero()) {
|
||||
auto inv = a.inverse();
|
||||
auto prod = a * inv;
|
||||
if (prod.to_bytes() != one.to_bytes()) __builtin_trap();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
54
cpu/fuzz/fuzz_point.cpp
Normal file
54
cpu/fuzz/fuzz_point.cpp
Normal file
@ -0,0 +1,54 @@
|
||||
// ============================================================================
|
||||
// Fuzz target: Point arithmetic
|
||||
// ============================================================================
|
||||
// Build: clang++ -fsanitize=fuzzer,address -O2 -std=c++20 \
|
||||
// -I cpu/include fuzz_point.cpp cpu/src/*.cpp -o fuzz_point
|
||||
// Run: ./fuzz_point -max_len=32 -runs=1000000
|
||||
// ============================================================================
|
||||
|
||||
#include "secp256k1/field.hpp"
|
||||
#include "secp256k1/scalar.hpp"
|
||||
#include "secp256k1/point.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
|
||||
using namespace secp256k1::fast;
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
if (size < 32) return 0; // need one 32-byte scalar
|
||||
|
||||
std::array<uint8_t, 32> buf{};
|
||||
__builtin_memcpy(buf.data(), data, 32);
|
||||
|
||||
auto k = Scalar::from_bytes(buf);
|
||||
if (k.is_zero()) return 0;
|
||||
|
||||
auto G = Point::generator();
|
||||
|
||||
// ── k*G must be on-curve ─────────────────────────────────────────────────
|
||||
auto P = G.scalar_mul(k);
|
||||
if (P.is_infinity()) return 0;
|
||||
|
||||
// Verify compressed → uncompressed round-trip consistency
|
||||
auto comp = P.to_compressed();
|
||||
auto uncomp = P.to_uncompressed();
|
||||
// First byte of uncompressed is 0x04
|
||||
if (uncomp[0] != 0x04) __builtin_trap();
|
||||
// Compressed first byte is 0x02 or 0x03
|
||||
if (comp[0] != 0x02 && comp[0] != 0x03) __builtin_trap();
|
||||
|
||||
// ── P + (-P) = infinity ──────────────────────────────────────────────────
|
||||
auto neg_P = P.negate();
|
||||
auto sum = P.add(neg_P);
|
||||
if (!sum.is_infinity()) __builtin_trap();
|
||||
|
||||
// ── 2*P = P + P = P.dbl() ────────────────────────────────────────────────
|
||||
auto dbl = P.dbl();
|
||||
auto add_self = P.add(P);
|
||||
auto dbl_comp = dbl.to_compressed();
|
||||
auto add_comp = add_self.to_compressed();
|
||||
if (dbl_comp != add_comp) __builtin_trap();
|
||||
|
||||
return 0;
|
||||
}
|
||||
58
cpu/fuzz/fuzz_scalar.cpp
Normal file
58
cpu/fuzz/fuzz_scalar.cpp
Normal file
@ -0,0 +1,58 @@
|
||||
// ============================================================================
|
||||
// Fuzz target: Scalar arithmetic
|
||||
// ============================================================================
|
||||
// Build: clang++ -fsanitize=fuzzer,address -O2 -std=c++20 \
|
||||
// -I cpu/include fuzz_scalar.cpp cpu/src/scalar.cpp \
|
||||
// -o fuzz_scalar
|
||||
// Run: ./fuzz_scalar -max_len=64 -runs=10000000
|
||||
// ============================================================================
|
||||
|
||||
#include "secp256k1/scalar.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <array>
|
||||
|
||||
using secp256k1::fast::Scalar;
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
if (size < 64) return 0; // need two 32-byte scalars
|
||||
|
||||
std::array<uint8_t, 32> buf_a{}, buf_b{};
|
||||
__builtin_memcpy(buf_a.data(), data, 32);
|
||||
__builtin_memcpy(buf_b.data(), data + 32, 32);
|
||||
|
||||
auto a = Scalar::from_bytes(buf_a);
|
||||
auto b = Scalar::from_bytes(buf_b);
|
||||
auto zero = Scalar::zero();
|
||||
|
||||
// ── Closure: add/sub round-trip ──────────────────────────────────────────
|
||||
auto c = a + b;
|
||||
auto d = c - b;
|
||||
if (d != a) __builtin_trap();
|
||||
|
||||
// ── Closure: mul by 1 = identity ─────────────────────────────────────────
|
||||
auto one = Scalar::one();
|
||||
auto e = a * one;
|
||||
if (e != a) __builtin_trap();
|
||||
|
||||
// ── Closure: a - a = 0 ───────────────────────────────────────────────────
|
||||
auto f = a - a;
|
||||
if (!f.is_zero()) __builtin_trap();
|
||||
|
||||
// ── Closure: a + 0 = a ───────────────────────────────────────────────────
|
||||
auto g = a + zero;
|
||||
if (g != a) __builtin_trap();
|
||||
|
||||
// ── Closure: (a * b) * 1 = a * b ─────────────────────────────────────────
|
||||
auto h = a * b;
|
||||
auto i = h * one;
|
||||
if (i != h) __builtin_trap();
|
||||
|
||||
// ── Closure: distributive: a*(b+1) = a*b + a ─────────────────────────────
|
||||
auto b_plus_1 = b + one;
|
||||
auto lhs = a * b_plus_1;
|
||||
auto rhs = (a * b) + a;
|
||||
if (lhs != rhs) __builtin_trap();
|
||||
|
||||
return 0;
|
||||
}
|
||||
74
cpu/include/secp256k1/ecdsa.hpp
Normal file
74
cpu/include/secp256k1/ecdsa.hpp
Normal file
@ -0,0 +1,74 @@
|
||||
#ifndef SECP256K1_ECDSA_HPP
|
||||
#define SECP256K1_ECDSA_HPP
|
||||
#pragma once
|
||||
|
||||
// ============================================================================
|
||||
// ECDSA Sign / Verify for secp256k1
|
||||
// ============================================================================
|
||||
// Standard ECDSA (RFC 6979 deterministic nonce recommended for production).
|
||||
// This implementation provides:
|
||||
// - sign(message_hash, private_key) → (r, s)
|
||||
// - verify(message_hash, public_key, r, s) → bool
|
||||
// - Signature normalization (low-S, BIP-62)
|
||||
//
|
||||
// WARNING: The nonce k MUST be cryptographically random or deterministic
|
||||
// (RFC 6979). Reusing k leaks the private key. This library provides a
|
||||
// deterministic nonce function (RFC 6979) for safety.
|
||||
// ============================================================================
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include "secp256k1/scalar.hpp"
|
||||
#include "secp256k1/point.hpp"
|
||||
|
||||
namespace secp256k1 {
|
||||
|
||||
// ── Signature ────────────────────────────────────────────────────────────────
|
||||
|
||||
struct ECDSASignature {
|
||||
fast::Scalar r;
|
||||
fast::Scalar s;
|
||||
|
||||
// DER encoding (variable length, max 72 bytes)
|
||||
// Returns {encoded_bytes, length}
|
||||
std::pair<std::array<std::uint8_t, 72>, std::size_t> to_der() const;
|
||||
|
||||
// Compact 64-byte encoding: r (32 bytes) || s (32 bytes)
|
||||
std::array<std::uint8_t, 64> to_compact() const;
|
||||
|
||||
// Decode from compact 64-byte encoding
|
||||
static ECDSASignature from_compact(const std::array<std::uint8_t, 64>& data);
|
||||
|
||||
// Normalize to low-S form (BIP-62): if s > n/2, replace with n - s
|
||||
ECDSASignature normalize() const;
|
||||
|
||||
// Check if signature has low-S
|
||||
bool is_low_s() const;
|
||||
};
|
||||
|
||||
// ── ECDSA Operations ─────────────────────────────────────────────────────────
|
||||
|
||||
// Sign a 32-byte message hash with a private key.
|
||||
// Uses RFC 6979 deterministic nonce generation.
|
||||
// Returns normalized (low-S) signature.
|
||||
// Returns {zero, zero} signature on failure (zero key, etc.)
|
||||
ECDSASignature ecdsa_sign(const std::array<std::uint8_t, 32>& msg_hash,
|
||||
const fast::Scalar& private_key);
|
||||
|
||||
// Verify an ECDSA signature against a public key and message hash.
|
||||
// Accepts both low-S and high-S signatures.
|
||||
bool ecdsa_verify(const std::array<std::uint8_t, 32>& msg_hash,
|
||||
const fast::Point& public_key,
|
||||
const ECDSASignature& sig);
|
||||
|
||||
// ── RFC 6979 Deterministic Nonce ─────────────────────────────────────────────
|
||||
|
||||
// Generate deterministic nonce k per RFC 6979.
|
||||
// Inputs: private key (32 bytes), message hash (32 bytes).
|
||||
// Output: scalar k suitable for ECDSA signing.
|
||||
fast::Scalar rfc6979_nonce(const fast::Scalar& private_key,
|
||||
const std::array<std::uint8_t, 32>& msg_hash);
|
||||
|
||||
} // namespace secp256k1
|
||||
|
||||
#endif // SECP256K1_ECDSA_HPP
|
||||
@ -41,6 +41,16 @@ public:
|
||||
bool operator==(const Scalar& rhs) const noexcept;
|
||||
bool operator!=(const Scalar& rhs) const noexcept { return !(*this == rhs); }
|
||||
|
||||
// Modular inverse: a^{-1} mod n (Fermat's little theorem: a^{n-2} mod n)
|
||||
// Returns zero if this is zero.
|
||||
Scalar inverse() const;
|
||||
|
||||
// Modular negation: -a mod n (= n - a, or 0 if a == 0)
|
||||
Scalar negate() const;
|
||||
|
||||
// Parity check: returns true if lowest bit is 0
|
||||
bool is_even() const noexcept;
|
||||
|
||||
// Zero-cost conversion to/from shared data type (for cross-backend interop)
|
||||
const ::secp256k1::ScalarData& data() const noexcept {
|
||||
return *reinterpret_cast<const ::secp256k1::ScalarData*>(&limbs_);
|
||||
|
||||
63
cpu/include/secp256k1/schnorr.hpp
Normal file
63
cpu/include/secp256k1/schnorr.hpp
Normal file
@ -0,0 +1,63 @@
|
||||
#ifndef SECP256K1_SCHNORR_HPP
|
||||
#define SECP256K1_SCHNORR_HPP
|
||||
#pragma once
|
||||
|
||||
// ============================================================================
|
||||
// Schnorr Signatures (BIP-340) for secp256k1
|
||||
// ============================================================================
|
||||
// Implements BIP-340 Schnorr signatures:
|
||||
// - X-only public keys (32 bytes)
|
||||
// - 64-byte signatures (R.x || s)
|
||||
// - Tagged hashing per BIP-340 spec
|
||||
//
|
||||
// Reference: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
|
||||
// ============================================================================
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include "secp256k1/scalar.hpp"
|
||||
#include "secp256k1/point.hpp"
|
||||
|
||||
namespace secp256k1 {
|
||||
|
||||
// ── Schnorr Signature ────────────────────────────────────────────────────────
|
||||
|
||||
struct SchnorrSignature {
|
||||
std::array<std::uint8_t, 32> r; // R.x (x-coordinate of nonce point)
|
||||
fast::Scalar s; // scalar s
|
||||
|
||||
// 64-byte compact encoding: r (32) || s (32)
|
||||
std::array<std::uint8_t, 64> to_bytes() const;
|
||||
static SchnorrSignature from_bytes(const std::array<std::uint8_t, 64>& data);
|
||||
};
|
||||
|
||||
// ── BIP-340 Operations ───────────────────────────────────────────────────────
|
||||
|
||||
// Sign a 32-byte message using BIP-340.
|
||||
// private_key: 32-byte secret key (scalar)
|
||||
// msg: 32-byte message (typically a hash)
|
||||
// aux_rand: 32 bytes of auxiliary randomness (can be zeros for deterministic)
|
||||
SchnorrSignature schnorr_sign(const fast::Scalar& private_key,
|
||||
const std::array<std::uint8_t, 32>& msg,
|
||||
const std::array<std::uint8_t, 32>& aux_rand);
|
||||
|
||||
// Verify a BIP-340 Schnorr signature.
|
||||
// pubkey_x: 32-byte x-only public key
|
||||
// msg: 32-byte message
|
||||
// sig: 64-byte signature
|
||||
bool schnorr_verify(const std::array<std::uint8_t, 32>& pubkey_x,
|
||||
const std::array<std::uint8_t, 32>& msg,
|
||||
const SchnorrSignature& sig);
|
||||
|
||||
// ── Tagged Hashing (BIP-340) ─────────────────────────────────────────────────
|
||||
|
||||
// H_tag(msg) = SHA256(SHA256(tag) || SHA256(tag) || msg)
|
||||
std::array<std::uint8_t, 32> tagged_hash(const char* tag,
|
||||
const void* data, std::size_t len);
|
||||
|
||||
// X-only public key from private key (BIP-340: negate if Y is odd)
|
||||
std::array<std::uint8_t, 32> schnorr_pubkey(const fast::Scalar& private_key);
|
||||
|
||||
} // namespace secp256k1
|
||||
|
||||
#endif // SECP256K1_SCHNORR_HPP
|
||||
173
cpu/include/secp256k1/sha256.hpp
Normal file
173
cpu/include/secp256k1/sha256.hpp
Normal file
@ -0,0 +1,173 @@
|
||||
#ifndef SECP256K1_SHA256_HPP
|
||||
#define SECP256K1_SHA256_HPP
|
||||
#pragma once
|
||||
|
||||
// ============================================================================
|
||||
// Minimal SHA-256 implementation for ECDSA / Schnorr
|
||||
// ============================================================================
|
||||
// Self-contained, no dependencies. Used only for message hashing and
|
||||
// tagged hashing (BIP-340). Not performance-critical.
|
||||
// ============================================================================
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
|
||||
namespace secp256k1 {
|
||||
|
||||
class SHA256 {
|
||||
public:
|
||||
using digest_type = std::array<std::uint8_t, 32>;
|
||||
|
||||
SHA256() noexcept { reset(); }
|
||||
|
||||
void reset() noexcept {
|
||||
state_[0] = 0x6a09e667u; state_[1] = 0xbb67ae85u;
|
||||
state_[2] = 0x3c6ef372u; state_[3] = 0xa54ff53au;
|
||||
state_[4] = 0x510e527fu; state_[5] = 0x9b05688cu;
|
||||
state_[6] = 0x1f83d9abu; state_[7] = 0x5be0cd19u;
|
||||
total_ = 0;
|
||||
buf_len_ = 0;
|
||||
}
|
||||
|
||||
void update(const void* data, std::size_t len) noexcept {
|
||||
auto ptr = static_cast<const std::uint8_t*>(data);
|
||||
total_ += len;
|
||||
|
||||
if (buf_len_ > 0) {
|
||||
std::size_t fill = 64 - buf_len_;
|
||||
if (len < fill) {
|
||||
std::memcpy(buf_ + buf_len_, ptr, len);
|
||||
buf_len_ += len;
|
||||
return;
|
||||
}
|
||||
std::memcpy(buf_ + buf_len_, ptr, fill);
|
||||
compress(buf_);
|
||||
ptr += fill;
|
||||
len -= fill;
|
||||
buf_len_ = 0;
|
||||
}
|
||||
|
||||
while (len >= 64) {
|
||||
compress(ptr);
|
||||
ptr += 64;
|
||||
len -= 64;
|
||||
}
|
||||
|
||||
if (len > 0) {
|
||||
std::memcpy(buf_, ptr, len);
|
||||
buf_len_ = len;
|
||||
}
|
||||
}
|
||||
|
||||
digest_type finalize() noexcept {
|
||||
std::uint64_t bits = total_ * 8;
|
||||
|
||||
// Pad
|
||||
std::uint8_t pad = static_cast<std::uint8_t>(0x80);
|
||||
update(&pad, 1);
|
||||
std::uint8_t zero = 0;
|
||||
while (buf_len_ != 56) {
|
||||
update(&zero, 1);
|
||||
}
|
||||
|
||||
// Append length (big-endian)
|
||||
std::uint8_t len_be[8];
|
||||
for (int i = 7; i >= 0; --i) {
|
||||
len_be[i] = static_cast<std::uint8_t>(bits);
|
||||
bits >>= 8;
|
||||
}
|
||||
update(len_be, 8);
|
||||
|
||||
digest_type out{};
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
out[i * 4 + 0] = static_cast<std::uint8_t>(state_[i] >> 24);
|
||||
out[i * 4 + 1] = static_cast<std::uint8_t>(state_[i] >> 16);
|
||||
out[i * 4 + 2] = static_cast<std::uint8_t>(state_[i] >> 8);
|
||||
out[i * 4 + 3] = static_cast<std::uint8_t>(state_[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// One-shot convenience
|
||||
static digest_type hash(const void* data, std::size_t len) noexcept {
|
||||
SHA256 ctx;
|
||||
ctx.update(data, len);
|
||||
return ctx.finalize();
|
||||
}
|
||||
|
||||
// Double-SHA256: SHA256(SHA256(data))
|
||||
static digest_type hash256(const void* data, std::size_t len) noexcept {
|
||||
auto h1 = hash(data, len);
|
||||
return hash(h1.data(), h1.size());
|
||||
}
|
||||
|
||||
private:
|
||||
std::uint32_t state_[8]{};
|
||||
std::uint8_t buf_[64]{};
|
||||
std::size_t buf_len_ = 0;
|
||||
std::uint64_t total_ = 0;
|
||||
|
||||
static constexpr std::uint32_t K[64] = {
|
||||
0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u,
|
||||
0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u,
|
||||
0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u,
|
||||
0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u,
|
||||
0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu,
|
||||
0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau,
|
||||
0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u,
|
||||
0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u,
|
||||
0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u,
|
||||
0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u,
|
||||
0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u,
|
||||
0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u,
|
||||
0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u,
|
||||
0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u,
|
||||
0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u,
|
||||
0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u
|
||||
};
|
||||
|
||||
static constexpr std::uint32_t rotr(std::uint32_t x, int n) noexcept {
|
||||
return (x >> n) | (x << (32 - n));
|
||||
}
|
||||
|
||||
void compress(const std::uint8_t* block) noexcept {
|
||||
std::uint32_t w[64];
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
w[i] = (std::uint32_t(block[i * 4]) << 24) |
|
||||
(std::uint32_t(block[i * 4 + 1]) << 16) |
|
||||
(std::uint32_t(block[i * 4 + 2]) << 8) |
|
||||
(std::uint32_t(block[i * 4 + 3]));
|
||||
}
|
||||
for (int i = 16; i < 64; ++i) {
|
||||
std::uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3);
|
||||
std::uint32_t s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10);
|
||||
w[i] = w[i - 16] + s0 + w[i - 7] + s1;
|
||||
}
|
||||
|
||||
std::uint32_t a = state_[0], b = state_[1], c = state_[2], d = state_[3];
|
||||
std::uint32_t e = state_[4], f = state_[5], g = state_[6], h = state_[7];
|
||||
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
std::uint32_t S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
||||
std::uint32_t ch = (e & f) ^ (~e & g);
|
||||
std::uint32_t temp1 = h + S1 + ch + K[i] + w[i];
|
||||
std::uint32_t S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
||||
std::uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
|
||||
std::uint32_t temp2 = S0 + maj;
|
||||
|
||||
h = g; g = f; f = e;
|
||||
e = d + temp1;
|
||||
d = c; c = b; b = a;
|
||||
a = temp1 + temp2;
|
||||
}
|
||||
|
||||
state_[0] += a; state_[1] += b; state_[2] += c; state_[3] += d;
|
||||
state_[4] += e; state_[5] += f; state_[6] += g; state_[7] += h;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace secp256k1
|
||||
|
||||
#endif // SECP256K1_SHA256_HPP
|
||||
269
cpu/src/ecdsa.cpp
Normal file
269
cpu/src/ecdsa.cpp
Normal file
@ -0,0 +1,269 @@
|
||||
#include "secp256k1/ecdsa.hpp"
|
||||
#include "secp256k1/sha256.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace secp256k1 {
|
||||
|
||||
using fast::Scalar;
|
||||
using fast::Point;
|
||||
using fast::FieldElement;
|
||||
|
||||
// ── Half-order for low-S check ───────────────────────────────────────────────
|
||||
// n/2 = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
|
||||
static const Scalar HALF_ORDER = Scalar::from_hex(
|
||||
"7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0");
|
||||
|
||||
// ── Signature Methods ────────────────────────────────────────────────────────
|
||||
|
||||
std::pair<std::array<std::uint8_t, 72>, std::size_t> ECDSASignature::to_der() const {
|
||||
auto r_bytes = r.to_bytes();
|
||||
auto s_bytes = s.to_bytes();
|
||||
|
||||
// Find actual length (skip leading zeros, add 0x00 pad if high bit set)
|
||||
auto encode_int = [](const std::array<uint8_t, 32>& val,
|
||||
uint8_t* out) -> size_t {
|
||||
size_t start = 0;
|
||||
while (start < 31 && val[start] == 0) ++start;
|
||||
bool need_pad = (val[start] & 0x80) != 0;
|
||||
size_t len = 32 - start + (need_pad ? 1 : 0);
|
||||
|
||||
out[0] = 0x02; // INTEGER tag
|
||||
out[1] = static_cast<uint8_t>(len);
|
||||
size_t pos = 2;
|
||||
if (need_pad) out[pos++] = 0x00;
|
||||
std::memcpy(out + pos, val.data() + start, 32 - start);
|
||||
return 2 + len;
|
||||
};
|
||||
|
||||
std::array<uint8_t, 72> buf{};
|
||||
uint8_t r_enc[35]{}, s_enc[35]{};
|
||||
size_t r_len = encode_int(r_bytes, r_enc);
|
||||
size_t s_len = encode_int(s_bytes, s_enc);
|
||||
|
||||
buf[0] = 0x30; // SEQUENCE tag
|
||||
buf[1] = static_cast<uint8_t>(r_len + s_len);
|
||||
std::memcpy(buf.data() + 2, r_enc, r_len);
|
||||
std::memcpy(buf.data() + 2 + r_len, s_enc, s_len);
|
||||
|
||||
return {buf, 2 + r_len + s_len};
|
||||
}
|
||||
|
||||
std::array<std::uint8_t, 64> ECDSASignature::to_compact() const {
|
||||
std::array<uint8_t, 64> out{};
|
||||
auto rb = r.to_bytes();
|
||||
auto sb = s.to_bytes();
|
||||
std::memcpy(out.data(), rb.data(), 32);
|
||||
std::memcpy(out.data() + 32, sb.data(), 32);
|
||||
return out;
|
||||
}
|
||||
|
||||
ECDSASignature ECDSASignature::from_compact(const std::array<std::uint8_t, 64>& data) {
|
||||
std::array<uint8_t, 32> rb{}, sb{};
|
||||
std::memcpy(rb.data(), data.data(), 32);
|
||||
std::memcpy(sb.data(), data.data() + 32, 32);
|
||||
return {Scalar::from_bytes(rb), Scalar::from_bytes(sb)};
|
||||
}
|
||||
|
||||
ECDSASignature ECDSASignature::normalize() const {
|
||||
if (is_low_s()) return *this;
|
||||
return {r, s.negate()};
|
||||
}
|
||||
|
||||
bool ECDSASignature::is_low_s() const {
|
||||
// s <= n/2 ?
|
||||
auto s_bytes = s.to_bytes();
|
||||
auto half_bytes = HALF_ORDER.to_bytes();
|
||||
for (size_t i = 0; i < 32; ++i) {
|
||||
if (s_bytes[i] < half_bytes[i]) return true;
|
||||
if (s_bytes[i] > half_bytes[i]) return false;
|
||||
}
|
||||
return true; // equal
|
||||
}
|
||||
|
||||
// ── RFC 6979 Deterministic Nonce ─────────────────────────────────────────────
|
||||
// Simplified RFC 6979 using HMAC-SHA256.
|
||||
|
||||
namespace {
|
||||
|
||||
// HMAC-SHA256
|
||||
struct HMAC_SHA256 {
|
||||
static std::array<uint8_t, 32> compute(const uint8_t* key, size_t key_len,
|
||||
const uint8_t* msg, size_t msg_len) {
|
||||
uint8_t ipad[64]{}, opad[64]{};
|
||||
uint8_t k_buf[64]{};
|
||||
|
||||
if (key_len > 64) {
|
||||
auto h = SHA256::hash(key, key_len);
|
||||
std::memcpy(k_buf, h.data(), 32);
|
||||
} else {
|
||||
std::memcpy(k_buf, key, key_len);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
ipad[i] = k_buf[i] ^ 0x36;
|
||||
opad[i] = k_buf[i] ^ 0x5c;
|
||||
}
|
||||
|
||||
// inner = SHA256(ipad || msg)
|
||||
SHA256 inner;
|
||||
inner.update(ipad, 64);
|
||||
inner.update(msg, msg_len);
|
||||
auto inner_hash = inner.finalize();
|
||||
|
||||
// outer = SHA256(opad || inner)
|
||||
SHA256 outer;
|
||||
outer.update(opad, 64);
|
||||
outer.update(inner_hash.data(), 32);
|
||||
return outer.finalize();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Scalar rfc6979_nonce(const Scalar& private_key,
|
||||
const std::array<uint8_t, 32>& msg_hash) {
|
||||
// RFC 6979 Section 3.2
|
||||
auto x_bytes = private_key.to_bytes();
|
||||
|
||||
// Step a: h1 = msg_hash (already hashed)
|
||||
// Step b: V = 0x01 * 32
|
||||
uint8_t V[32];
|
||||
std::memset(V, 0x01, 32);
|
||||
|
||||
// Step c: K = 0x00 * 32
|
||||
uint8_t K[32];
|
||||
std::memset(K, 0x00, 32);
|
||||
|
||||
// Step d: K = HMAC(K, V || 0x00 || x || h1)
|
||||
{
|
||||
uint8_t buf[97]; // 32 + 1 + 32 + 32
|
||||
std::memcpy(buf, V, 32);
|
||||
buf[32] = 0x00;
|
||||
std::memcpy(buf + 33, x_bytes.data(), 32);
|
||||
std::memcpy(buf + 65, msg_hash.data(), 32);
|
||||
auto h = HMAC_SHA256::compute(K, 32, buf, 97);
|
||||
std::memcpy(K, h.data(), 32);
|
||||
}
|
||||
|
||||
// Step e: V = HMAC(K, V)
|
||||
{
|
||||
auto h = HMAC_SHA256::compute(K, 32, V, 32);
|
||||
std::memcpy(V, h.data(), 32);
|
||||
}
|
||||
|
||||
// Step f: K = HMAC(K, V || 0x01 || x || h1)
|
||||
{
|
||||
uint8_t buf[97];
|
||||
std::memcpy(buf, V, 32);
|
||||
buf[32] = 0x01;
|
||||
std::memcpy(buf + 33, x_bytes.data(), 32);
|
||||
std::memcpy(buf + 65, msg_hash.data(), 32);
|
||||
auto h = HMAC_SHA256::compute(K, 32, buf, 97);
|
||||
std::memcpy(K, h.data(), 32);
|
||||
}
|
||||
|
||||
// Step g: V = HMAC(K, V)
|
||||
{
|
||||
auto h = HMAC_SHA256::compute(K, 32, V, 32);
|
||||
std::memcpy(V, h.data(), 32);
|
||||
}
|
||||
|
||||
// Step h: loop
|
||||
for (int attempt = 0; attempt < 100; ++attempt) {
|
||||
// V = HMAC(K, V)
|
||||
auto h = HMAC_SHA256::compute(K, 32, V, 32);
|
||||
std::memcpy(V, h.data(), 32);
|
||||
|
||||
std::array<uint8_t, 32> t;
|
||||
std::memcpy(t.data(), V, 32);
|
||||
|
||||
auto candidate = Scalar::from_bytes(t);
|
||||
if (!candidate.is_zero()) {
|
||||
// Check candidate < order (from_bytes already reduces mod n,
|
||||
// but we also need to ensure it wasn't zero before reduction)
|
||||
return candidate;
|
||||
}
|
||||
|
||||
// K = HMAC(K, V || 0x00)
|
||||
uint8_t buf[33];
|
||||
std::memcpy(buf, V, 32);
|
||||
buf[32] = 0x00;
|
||||
auto k2 = HMAC_SHA256::compute(K, 32, buf, 33);
|
||||
std::memcpy(K, k2.data(), 32);
|
||||
|
||||
// V = HMAC(K, V)
|
||||
auto v2 = HMAC_SHA256::compute(K, 32, V, 32);
|
||||
std::memcpy(V, v2.data(), 32);
|
||||
}
|
||||
|
||||
return Scalar::zero(); // should never reach
|
||||
}
|
||||
|
||||
// ── ECDSA Sign ───────────────────────────────────────────────────────────────
|
||||
|
||||
ECDSASignature ecdsa_sign(const std::array<uint8_t, 32>& msg_hash,
|
||||
const Scalar& private_key) {
|
||||
if (private_key.is_zero()) return {Scalar::zero(), Scalar::zero()};
|
||||
|
||||
// z = message hash interpreted as scalar
|
||||
auto z = Scalar::from_bytes(msg_hash);
|
||||
|
||||
// Generate deterministic nonce
|
||||
auto k = rfc6979_nonce(private_key, msg_hash);
|
||||
if (k.is_zero()) return {Scalar::zero(), Scalar::zero()};
|
||||
|
||||
// R = k * G
|
||||
auto R = Point::generator().scalar_mul(k);
|
||||
if (R.is_infinity()) return {Scalar::zero(), Scalar::zero()};
|
||||
|
||||
// r = R.x mod n
|
||||
auto r_fe = R.x();
|
||||
auto r_bytes = r_fe.to_bytes();
|
||||
auto r = Scalar::from_bytes(r_bytes);
|
||||
if (r.is_zero()) return {Scalar::zero(), Scalar::zero()};
|
||||
|
||||
// s = k^{-1} * (z + r * d) mod n
|
||||
auto k_inv = k.inverse();
|
||||
auto s = k_inv * (z + r * private_key);
|
||||
if (s.is_zero()) return {Scalar::zero(), Scalar::zero()};
|
||||
|
||||
// Normalize to low-S (BIP-62)
|
||||
ECDSASignature sig{r, s};
|
||||
return sig.normalize();
|
||||
}
|
||||
|
||||
// ── ECDSA Verify ─────────────────────────────────────────────────────────────
|
||||
|
||||
bool ecdsa_verify(const std::array<uint8_t, 32>& msg_hash,
|
||||
const Point& public_key,
|
||||
const ECDSASignature& sig) {
|
||||
// Check r, s in [1, n-1]
|
||||
if (sig.r.is_zero() || sig.s.is_zero()) return false;
|
||||
|
||||
// z = message hash as scalar
|
||||
auto z = Scalar::from_bytes(msg_hash);
|
||||
|
||||
// w = s^{-1} mod n
|
||||
auto w = sig.s.inverse();
|
||||
|
||||
// u1 = z * w mod n
|
||||
auto u1 = z * w;
|
||||
|
||||
// u2 = r * w mod n
|
||||
auto u2 = sig.r * w;
|
||||
|
||||
// R' = u1 * G + u2 * Q
|
||||
auto P1 = Point::generator().scalar_mul(u1);
|
||||
auto P2 = public_key.scalar_mul(u2);
|
||||
auto R_prime = P1.add(P2);
|
||||
|
||||
if (R_prime.is_infinity()) return false;
|
||||
|
||||
// v = R'.x mod n
|
||||
auto v_bytes = R_prime.x().to_bytes();
|
||||
auto v = Scalar::from_bytes(v_bytes);
|
||||
|
||||
return v == sig.r;
|
||||
}
|
||||
|
||||
} // namespace secp256k1
|
||||
@ -487,6 +487,54 @@ bool Scalar::operator==(const Scalar& rhs) const noexcept {
|
||||
return limbs_ == rhs.limbs_;
|
||||
}
|
||||
|
||||
Scalar Scalar::inverse() const {
|
||||
// Fermat's little theorem: a^{-1} = a^{n-2} mod n
|
||||
// n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
||||
// n-2 = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD036413F
|
||||
//
|
||||
// Fixed addition-chain exponentiation (no secret-dependent branches).
|
||||
// Uses square-and-multiply with a hand-crafted chain for n-2.
|
||||
// The binary representation of n-2 has 256 bits; we process them MSB→LSB.
|
||||
|
||||
if (is_zero()) return Scalar::zero();
|
||||
|
||||
// n-2 limbs (little-endian):
|
||||
// [0] = 0xBFD25E8CD036413F
|
||||
// [1] = 0xBAAEDCE6AF48A03B
|
||||
// [2] = 0xFFFFFFFFFFFFFFFE
|
||||
// [3] = 0xFFFFFFFFFFFFFFFF
|
||||
constexpr uint64_t nm2[4] = {
|
||||
0xBFD25E8CD036413FULL,
|
||||
0xBAAEDCE6AF48A03BULL,
|
||||
0xFFFFFFFFFFFFFFFEULL,
|
||||
0xFFFFFFFFFFFFFFFFULL
|
||||
};
|
||||
|
||||
Scalar result = Scalar::one();
|
||||
Scalar base = *this;
|
||||
|
||||
// Square-and-multiply, LSB first is simpler but MSB first is standard.
|
||||
// We do MSB→LSB (bit 255 down to 0).
|
||||
for (int i = 255; i >= 0; --i) {
|
||||
result *= result; // square
|
||||
int limb_idx = i / 64;
|
||||
int bit_idx = i % 64;
|
||||
if ((nm2[limb_idx] >> bit_idx) & 1) {
|
||||
result *= base;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Scalar Scalar::negate() const {
|
||||
if (is_zero()) return Scalar::zero();
|
||||
return Scalar(sub_impl(ORDER, limbs_), true);
|
||||
}
|
||||
|
||||
bool Scalar::is_even() const noexcept {
|
||||
return (limbs_[0] & 1) == 0;
|
||||
}
|
||||
|
||||
std::uint8_t Scalar::bit(std::size_t index) const {
|
||||
if (index >= 256) {
|
||||
return 0;
|
||||
|
||||
209
cpu/src/schnorr.cpp
Normal file
209
cpu/src/schnorr.cpp
Normal file
@ -0,0 +1,209 @@
|
||||
#include "secp256k1/schnorr.hpp"
|
||||
#include "secp256k1/sha256.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace secp256k1 {
|
||||
|
||||
using fast::Scalar;
|
||||
using fast::Point;
|
||||
using fast::FieldElement;
|
||||
|
||||
// ── Tagged Hash (BIP-340) ────────────────────────────────────────────────────
|
||||
|
||||
std::array<uint8_t, 32> tagged_hash(const char* tag,
|
||||
const void* data, std::size_t len) {
|
||||
// tag_hash = SHA256(tag)
|
||||
auto tag_hash = SHA256::hash(tag, std::strlen(tag));
|
||||
|
||||
// H_tag(msg) = SHA256(tag_hash || tag_hash || msg)
|
||||
SHA256 ctx;
|
||||
ctx.update(tag_hash.data(), 32);
|
||||
ctx.update(tag_hash.data(), 32);
|
||||
ctx.update(data, len);
|
||||
return ctx.finalize();
|
||||
}
|
||||
|
||||
// ── Schnorr Signature ────────────────────────────────────────────────────────
|
||||
|
||||
std::array<uint8_t, 64> SchnorrSignature::to_bytes() const {
|
||||
std::array<uint8_t, 64> out{};
|
||||
std::memcpy(out.data(), r.data(), 32);
|
||||
auto s_bytes = s.to_bytes();
|
||||
std::memcpy(out.data() + 32, s_bytes.data(), 32);
|
||||
return out;
|
||||
}
|
||||
|
||||
SchnorrSignature SchnorrSignature::from_bytes(const std::array<uint8_t, 64>& data) {
|
||||
SchnorrSignature sig{};
|
||||
std::memcpy(sig.r.data(), data.data(), 32);
|
||||
std::array<uint8_t, 32> s_bytes{};
|
||||
std::memcpy(s_bytes.data(), data.data() + 32, 32);
|
||||
sig.s = Scalar::from_bytes(s_bytes);
|
||||
return sig;
|
||||
}
|
||||
|
||||
// ── X-only pubkey ────────────────────────────────────────────────────────────
|
||||
|
||||
std::array<uint8_t, 32> schnorr_pubkey(const Scalar& private_key) {
|
||||
auto P = Point::generator().scalar_mul(private_key);
|
||||
auto uncomp = P.to_uncompressed();
|
||||
|
||||
// Check if Y is even (BIP-340: use key with even Y)
|
||||
// Y is bytes [33..64] of uncompressed key, last byte parity
|
||||
bool y_is_odd = (uncomp[64] & 1) != 0;
|
||||
|
||||
// X-only: just the x-coordinate
|
||||
auto x_bytes = P.x().to_bytes();
|
||||
|
||||
// If Y is odd, the caller should negate the key; but for pubkey output
|
||||
// we just return X regardless (BIP-340 convention)
|
||||
(void)y_is_odd;
|
||||
return x_bytes;
|
||||
}
|
||||
|
||||
// ── BIP-340 Sign ─────────────────────────────────────────────────────────────
|
||||
|
||||
SchnorrSignature schnorr_sign(const Scalar& private_key,
|
||||
const std::array<uint8_t, 32>& msg,
|
||||
const std::array<uint8_t, 32>& aux_rand) {
|
||||
// Step 1: d' = private_key
|
||||
auto d_prime = private_key;
|
||||
if (d_prime.is_zero()) return SchnorrSignature{};
|
||||
|
||||
// Step 2: P = d' * G
|
||||
auto P = Point::generator().scalar_mul(d_prime);
|
||||
auto P_uncomp = P.to_uncompressed();
|
||||
bool p_y_odd = (P_uncomp[64] & 1) != 0;
|
||||
|
||||
// Step 3: d = d' if has_even_y(P), else n - d'
|
||||
auto d = p_y_odd ? d_prime.negate() : d_prime;
|
||||
|
||||
// Step 4: t = d XOR tagged_hash("BIP0340/aux", aux_rand)
|
||||
auto t_hash = tagged_hash("BIP0340/aux", aux_rand.data(), 32);
|
||||
auto d_bytes = d.to_bytes();
|
||||
uint8_t t[32];
|
||||
for (int i = 0; i < 32; ++i) t[i] = d_bytes[i] ^ t_hash[i];
|
||||
|
||||
// Step 5: rand = tagged_hash("BIP0340/nonce", t || pubkey_x || msg)
|
||||
auto px = P.x().to_bytes();
|
||||
uint8_t nonce_input[96];
|
||||
std::memcpy(nonce_input, t, 32);
|
||||
std::memcpy(nonce_input + 32, px.data(), 32);
|
||||
std::memcpy(nonce_input + 64, msg.data(), 32);
|
||||
auto rand_hash = tagged_hash("BIP0340/nonce", nonce_input, 96);
|
||||
auto k_prime = Scalar::from_bytes(rand_hash);
|
||||
if (k_prime.is_zero()) return SchnorrSignature{};
|
||||
|
||||
// Step 6: R = k' * G
|
||||
auto R = Point::generator().scalar_mul(k_prime);
|
||||
auto R_uncomp = R.to_uncompressed();
|
||||
bool r_y_odd = (R_uncomp[64] & 1) != 0;
|
||||
|
||||
// Step 7: k = k' if has_even_y(R), else n - k'
|
||||
auto k = r_y_odd ? k_prime.negate() : k_prime;
|
||||
|
||||
// Step 8: e = tagged_hash("BIP0340/challenge", R.x || pubkey_x || msg) mod n
|
||||
auto rx = R.x().to_bytes();
|
||||
uint8_t challenge_input[96];
|
||||
std::memcpy(challenge_input, rx.data(), 32);
|
||||
std::memcpy(challenge_input + 32, px.data(), 32);
|
||||
std::memcpy(challenge_input + 64, msg.data(), 32);
|
||||
auto e_hash = tagged_hash("BIP0340/challenge", challenge_input, 96);
|
||||
auto e = Scalar::from_bytes(e_hash);
|
||||
|
||||
// Step 9: sig = (R.x, k + e * d) mod n
|
||||
auto s = k + e * d;
|
||||
|
||||
SchnorrSignature sig{};
|
||||
sig.r = rx;
|
||||
sig.s = s;
|
||||
return sig;
|
||||
}
|
||||
|
||||
// ── BIP-340 Verify ───────────────────────────────────────────────────────────
|
||||
|
||||
bool schnorr_verify(const std::array<uint8_t, 32>& pubkey_x,
|
||||
const std::array<uint8_t, 32>& msg,
|
||||
const SchnorrSignature& sig) {
|
||||
// Step 1: Check r < p and s < n
|
||||
// (from_bytes already reduces, so just check non-zero for r)
|
||||
auto r_fe = FieldElement::from_bytes(sig.r);
|
||||
|
||||
if (sig.s.is_zero()) return false;
|
||||
|
||||
// Step 2: e = tagged_hash("BIP0340/challenge", r || pubkey_x || msg) mod n
|
||||
uint8_t challenge_input[96];
|
||||
std::memcpy(challenge_input, sig.r.data(), 32);
|
||||
std::memcpy(challenge_input + 32, pubkey_x.data(), 32);
|
||||
std::memcpy(challenge_input + 64, msg.data(), 32);
|
||||
auto e_hash = tagged_hash("BIP0340/challenge", challenge_input, 96);
|
||||
auto e = Scalar::from_bytes(e_hash);
|
||||
|
||||
// Step 3: Lift x-only pubkey to point
|
||||
// P = lift_x(pubkey_x): find y such that y² = x³ + 7, pick even y
|
||||
auto px_fe = FieldElement::from_bytes(pubkey_x);
|
||||
|
||||
// y² = x³ + 7
|
||||
auto x3 = px_fe.square() * px_fe;
|
||||
auto seven = FieldElement::from_uint64(7);
|
||||
auto y2 = x3 + seven;
|
||||
|
||||
// Compute y = y2^((p+1)/4) mod p
|
||||
// p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
|
||||
// (p+1)/4 = 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFF0C
|
||||
// We use the square root via exponentiation
|
||||
// For secp256k1: sqrt(a) = a^((p+1)/4) because p ≡ 3 (mod 4)
|
||||
auto exp = FieldElement::from_hex(
|
||||
"3fffffffffffffffffffffffffffffffffffffffffffffffffffffffbfffff0c");
|
||||
|
||||
// Compute y = y2^exp by repeated squaring
|
||||
// This is expensive but correct
|
||||
auto y = FieldElement::one();
|
||||
auto base = y2;
|
||||
|
||||
// We need to exponentiate — use the existing field inverse chain approach
|
||||
// Actually, we can use: y = y2^((p+1)/4)
|
||||
// Since FieldElement already has inverse() which computes a^(p-2),
|
||||
// we can compute sqrt differently. But let's just do pow.
|
||||
auto exp_bytes = exp.to_bytes();
|
||||
for (int i = 0; i < 256; ++i) {
|
||||
y = y.square();
|
||||
int byte_idx = i / 8;
|
||||
int bit_idx = 7 - (i % 8);
|
||||
if ((exp_bytes[byte_idx] >> bit_idx) & 1) {
|
||||
y = y * base;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify: y² == y2
|
||||
auto y_check = y.square();
|
||||
if (y_check != y2) return false; // not a quadratic residue
|
||||
|
||||
// Ensure even Y (BIP-340 convention)
|
||||
auto y_bytes = y.to_bytes();
|
||||
bool y_odd = (y_bytes[31] & 1) != 0;
|
||||
if (y_odd) {
|
||||
// negate y: y = p - y
|
||||
y = FieldElement::zero() - y;
|
||||
}
|
||||
|
||||
auto P = Point::from_affine(px_fe, y);
|
||||
|
||||
// Step 4: R = s*G - e*P
|
||||
auto sG = Point::generator().scalar_mul(sig.s);
|
||||
auto eP = P.scalar_mul(e);
|
||||
auto neg_eP = eP.negate();
|
||||
auto R = sG.add(neg_eP);
|
||||
|
||||
if (R.is_infinity()) return false;
|
||||
|
||||
// Step 5: Check R has even y
|
||||
auto R_uncomp = R.to_uncompressed();
|
||||
if ((R_uncomp[64] & 1) != 0) return false; // odd y
|
||||
|
||||
// Step 6: Check R.x == r
|
||||
auto R_x = R.x().to_bytes();
|
||||
return R_x == sig.r;
|
||||
}
|
||||
|
||||
} // namespace secp256k1
|
||||
222
cpu/tests/test_ecdsa_schnorr.cpp
Normal file
222
cpu/tests/test_ecdsa_schnorr.cpp
Normal file
@ -0,0 +1,222 @@
|
||||
// ============================================================================
|
||||
// Test: ECDSA sign/verify + Schnorr BIP-340 sign/verify
|
||||
// ============================================================================
|
||||
|
||||
#include "secp256k1/ecdsa.hpp"
|
||||
#include "secp256k1/schnorr.hpp"
|
||||
#include "secp256k1/sha256.hpp"
|
||||
#include "secp256k1/scalar.hpp"
|
||||
#include "secp256k1/point.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <array>
|
||||
|
||||
using namespace secp256k1;
|
||||
using fast::Scalar;
|
||||
using fast::Point;
|
||||
|
||||
static int tests_run = 0;
|
||||
static int tests_passed = 0;
|
||||
|
||||
#define CHECK(cond, msg) do { \
|
||||
++tests_run; \
|
||||
if (cond) { ++tests_passed; printf(" [PASS] %s\n", msg); } \
|
||||
else { printf(" [FAIL] %s\n", msg); } \
|
||||
} while(0)
|
||||
|
||||
// ── SHA-256 Tests ────────────────────────────────────────────────────────────
|
||||
|
||||
static void test_sha256() {
|
||||
printf("\n--- SHA-256 ---\n");
|
||||
|
||||
// NIST test vector: SHA256("abc")
|
||||
auto h = SHA256::hash("abc", 3);
|
||||
uint8_t expected[] = {
|
||||
0xba, 0x78, 0x16, 0xbf, 0x8f, 0x01, 0xcf, 0xea,
|
||||
0x41, 0x41, 0x40, 0xde, 0x5d, 0xae, 0x22, 0x23,
|
||||
0xb0, 0x03, 0x61, 0xa3, 0x96, 0x17, 0x7a, 0x9c,
|
||||
0xb4, 0x10, 0xff, 0x61, 0xf2, 0x00, 0x15, 0xad
|
||||
};
|
||||
CHECK(std::memcmp(h.data(), expected, 32) == 0, "SHA256(\"abc\") matches NIST vector");
|
||||
|
||||
// Empty string
|
||||
auto h2 = SHA256::hash("", 0);
|
||||
uint8_t expected2[] = {
|
||||
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,
|
||||
0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
|
||||
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
|
||||
0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55
|
||||
};
|
||||
CHECK(std::memcmp(h2.data(), expected2, 32) == 0, "SHA256(\"\") matches NIST vector");
|
||||
}
|
||||
|
||||
// ── Scalar::inverse Tests ────────────────────────────────────────────────────
|
||||
|
||||
static void test_scalar_inverse() {
|
||||
printf("\n--- Scalar::inverse ---\n");
|
||||
|
||||
auto a = Scalar::from_uint64(7);
|
||||
auto a_inv = a.inverse();
|
||||
auto product = a * a_inv;
|
||||
CHECK(product == Scalar::one(), "7 * 7^{-1} = 1 mod n");
|
||||
|
||||
auto b = Scalar::from_hex(
|
||||
"e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35");
|
||||
auto b_inv = b.inverse();
|
||||
auto product2 = b * b_inv;
|
||||
CHECK(product2 == Scalar::one(), "random * random^{-1} = 1 mod n");
|
||||
|
||||
auto zero_inv = Scalar::zero().inverse();
|
||||
CHECK(zero_inv.is_zero(), "inverse(0) = 0");
|
||||
}
|
||||
|
||||
// ── Scalar::negate Tests ─────────────────────────────────────────────────────
|
||||
|
||||
static void test_scalar_negate() {
|
||||
printf("\n--- Scalar::negate ---\n");
|
||||
|
||||
auto a = Scalar::from_uint64(42);
|
||||
auto neg_a = a.negate();
|
||||
auto sum = a + neg_a;
|
||||
CHECK(sum.is_zero(), "a + (-a) = 0");
|
||||
|
||||
CHECK(Scalar::zero().negate().is_zero(), "negate(0) = 0");
|
||||
}
|
||||
|
||||
// ── ECDSA Tests ──────────────────────────────────────────────────────────────
|
||||
|
||||
static void test_ecdsa_sign_verify() {
|
||||
printf("\n--- ECDSA Sign/Verify ---\n");
|
||||
|
||||
// Key pair
|
||||
auto priv = Scalar::from_hex(
|
||||
"e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35");
|
||||
auto pub = Point::generator().scalar_mul(priv);
|
||||
|
||||
// Message hash = SHA256("Hello, secp256k1!")
|
||||
auto msg_hash = SHA256::hash("Hello, secp256k1!", 17);
|
||||
|
||||
// Sign
|
||||
auto sig = ecdsa_sign(msg_hash, priv);
|
||||
CHECK(!sig.r.is_zero() && !sig.s.is_zero(), "signature is non-zero");
|
||||
CHECK(sig.is_low_s(), "signature has low-S (BIP-62)");
|
||||
|
||||
// Verify
|
||||
bool valid = ecdsa_verify(msg_hash, pub, sig);
|
||||
CHECK(valid, "verify(sign(msg, priv), pub) = true");
|
||||
|
||||
// Wrong message
|
||||
auto wrong_hash = SHA256::hash("Wrong message", 13);
|
||||
bool invalid_msg = ecdsa_verify(wrong_hash, pub, sig);
|
||||
CHECK(!invalid_msg, "verify with wrong message = false");
|
||||
|
||||
// Wrong key
|
||||
auto wrong_pub = Point::generator().scalar_mul(Scalar::from_uint64(999));
|
||||
bool invalid_key = ecdsa_verify(msg_hash, wrong_pub, sig);
|
||||
CHECK(!invalid_key, "verify with wrong pubkey = false");
|
||||
|
||||
// Compact round-trip
|
||||
auto compact = sig.to_compact();
|
||||
auto sig2 = ECDSASignature::from_compact(compact);
|
||||
CHECK(sig2.r == sig.r && sig2.s == sig.s, "compact encoding round-trip");
|
||||
|
||||
// DER encoding
|
||||
auto [der, der_len] = sig.to_der();
|
||||
CHECK(der_len > 0 && der[0] == 0x30, "DER encoding starts with SEQUENCE tag");
|
||||
}
|
||||
|
||||
static void test_ecdsa_deterministic() {
|
||||
printf("\n--- ECDSA Deterministic (RFC 6979) ---\n");
|
||||
|
||||
auto priv = Scalar::from_hex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001");
|
||||
auto msg = SHA256::hash("test", 4);
|
||||
|
||||
// Sign twice with same key + message → same signature (deterministic)
|
||||
auto sig1 = ecdsa_sign(msg, priv);
|
||||
auto sig2 = ecdsa_sign(msg, priv);
|
||||
CHECK(sig1.r == sig2.r && sig1.s == sig2.s,
|
||||
"same key + message → same signature (deterministic)");
|
||||
|
||||
// Different message → different signature
|
||||
auto msg2 = SHA256::hash("test2", 5);
|
||||
auto sig3 = ecdsa_sign(msg2, priv);
|
||||
CHECK(sig3.r != sig1.r || sig3.s != sig1.s,
|
||||
"different message → different signature");
|
||||
}
|
||||
|
||||
// ── Schnorr Tests ────────────────────────────────────────────────────────────
|
||||
|
||||
static void test_schnorr_sign_verify() {
|
||||
printf("\n--- Schnorr BIP-340 Sign/Verify ---\n");
|
||||
|
||||
auto priv = Scalar::from_hex(
|
||||
"e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35");
|
||||
|
||||
// BIP-340 x-only pubkey
|
||||
auto pubkey_x = schnorr_pubkey(priv);
|
||||
{
|
||||
std::array<uint8_t, 32> zero_arr{};
|
||||
CHECK(pubkey_x != zero_arr, "x-only pubkey is non-zero");
|
||||
}
|
||||
|
||||
// Message
|
||||
std::array<uint8_t, 32> msg{};
|
||||
auto h = SHA256::hash("Hello Schnorr!", 14);
|
||||
std::memcpy(msg.data(), h.data(), 32);
|
||||
|
||||
// Aux randomness (zeros for determinism)
|
||||
std::array<uint8_t, 32> aux{};
|
||||
|
||||
// Sign
|
||||
auto sig = schnorr_sign(priv, msg, aux);
|
||||
|
||||
// Verify
|
||||
bool valid = schnorr_verify(pubkey_x, msg, sig);
|
||||
CHECK(valid, "schnorr_verify(sign(msg, priv), pubkey) = true");
|
||||
|
||||
// Wrong message
|
||||
std::array<uint8_t, 32> wrong_msg{};
|
||||
wrong_msg[0] = 0xFF;
|
||||
bool invalid = schnorr_verify(pubkey_x, wrong_msg, sig);
|
||||
CHECK(!invalid, "schnorr_verify with wrong message = false");
|
||||
|
||||
// Round-trip
|
||||
auto sig_bytes = sig.to_bytes();
|
||||
auto sig2 = SchnorrSignature::from_bytes(sig_bytes);
|
||||
CHECK(sig2.r == sig.r && sig2.s == sig.s, "schnorr signature round-trip");
|
||||
}
|
||||
|
||||
static void test_tagged_hash() {
|
||||
printf("\n--- Tagged Hash (BIP-340) ---\n");
|
||||
|
||||
auto h1 = tagged_hash("BIP0340/challenge", "test", 4);
|
||||
auto h2 = tagged_hash("BIP0340/challenge", "test", 4);
|
||||
CHECK(h1 == h2, "tagged_hash is deterministic");
|
||||
|
||||
auto h3 = tagged_hash("BIP0340/aux", "test", 4);
|
||||
CHECK(h1 != h3, "different tags → different hashes");
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
int main() {
|
||||
printf("================================================================\n");
|
||||
printf(" ECDSA + Schnorr (BIP-340) Test Suite\n");
|
||||
printf("================================================================\n");
|
||||
|
||||
test_sha256();
|
||||
test_scalar_inverse();
|
||||
test_scalar_negate();
|
||||
test_ecdsa_sign_verify();
|
||||
test_ecdsa_deterministic();
|
||||
test_tagged_hash();
|
||||
test_schnorr_sign_verify();
|
||||
|
||||
printf("\n================================================================\n");
|
||||
printf(" Results: %d / %d passed\n", tests_passed, tests_run);
|
||||
printf("================================================================\n");
|
||||
|
||||
return (tests_passed == tests_run) ? 0 : 1;
|
||||
}
|
||||
@ -1,8 +1,11 @@
|
||||
# Examples CMakeLists.txt
|
||||
# Placeholder for examples
|
||||
|
||||
message(STATUS "Examples directory (placeholder)")
|
||||
|
||||
# ESP32 example is a separate ESP-IDF project and not built with the main CMake
|
||||
# See examples/esp32_test/ for ESP32 example
|
||||
# ESP32 / STM32 examples are separate projects (ESP-IDF / STM32CubeMX)
|
||||
# See examples/esp32_test/ and examples/stm32_test/
|
||||
|
||||
# ── Desktop example ──────────────────────────────────────────────────────────
|
||||
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "xtensa|Xtensa" AND NOT SECP256K1_PLATFORM_STM32)
|
||||
add_executable(example_basic_usage basic_usage/main.cpp)
|
||||
target_link_libraries(example_basic_usage PRIVATE fastsecp256k1)
|
||||
message(STATUS "Examples: basic_usage enabled")
|
||||
endif()
|
||||
|
||||
95
examples/basic_usage/main.cpp
Normal file
95
examples/basic_usage/main.cpp
Normal file
@ -0,0 +1,95 @@
|
||||
// ============================================================================
|
||||
// UltrafastSecp256k1 — Desktop Quick-Start Example
|
||||
// ============================================================================
|
||||
// Demonstrates basic usage: key generation, point operations, serialization.
|
||||
// Build: cmake --build <build_dir> --target example_basic_usage
|
||||
// ============================================================================
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
|
||||
#include "secp256k1/field.hpp"
|
||||
#include "secp256k1/scalar.hpp"
|
||||
#include "secp256k1/point.hpp"
|
||||
#include "secp256k1/selftest.hpp"
|
||||
|
||||
using namespace secp256k1::fast;
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
static void print_hex(const char* label, const uint8_t* data, size_t len) {
|
||||
printf(" %s: ", label);
|
||||
for (size_t i = 0; i < len; ++i) printf("%02x", data[i]);
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
static double now_us() {
|
||||
using clk = std::chrono::high_resolution_clock;
|
||||
static auto t0 = clk::now();
|
||||
return std::chrono::duration<double, std::micro>(clk::now() - t0).count();
|
||||
}
|
||||
|
||||
// ── main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
int main() {
|
||||
printf("=== UltrafastSecp256k1 — Basic Usage Example ===\n\n");
|
||||
|
||||
// 1) Self-test
|
||||
printf("[1] Running self-test...\n");
|
||||
bool ok = Selftest(false);
|
||||
printf(" Result: %s\n\n", ok ? "PASS" : "FAIL");
|
||||
if (!ok) return 1;
|
||||
|
||||
// 2) Create a private key (scalar)
|
||||
printf("[2] Key generation\n");
|
||||
auto priv = Scalar::from_hex(
|
||||
"e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35");
|
||||
auto pub = Point::generator().scalar_mul(priv);
|
||||
|
||||
auto compressed = pub.to_compressed();
|
||||
auto uncompressed = pub.to_uncompressed();
|
||||
printf(" Private key : %s\n", priv.to_hex().c_str());
|
||||
print_hex("Public (comp)", compressed.data(), compressed.size());
|
||||
print_hex("Public (uncomp)", uncompressed.data(), uncompressed.size());
|
||||
printf("\n");
|
||||
|
||||
// 3) Point arithmetic
|
||||
printf("[3] Point arithmetic\n");
|
||||
auto G = Point::generator();
|
||||
auto G2 = G.dbl();
|
||||
auto G3 = G2.add(G);
|
||||
printf(" G x = %s\n", G.x().to_hex().c_str());
|
||||
printf(" 2G x = %s\n", G2.x().to_hex().c_str());
|
||||
printf(" 3G x = %s\n", G3.x().to_hex().c_str());
|
||||
printf("\n");
|
||||
|
||||
// 4) Scalar arithmetic
|
||||
printf("[4] Scalar arithmetic\n");
|
||||
auto a = Scalar::from_uint64(7);
|
||||
auto b = Scalar::from_uint64(13);
|
||||
auto c = a * b;
|
||||
printf(" 7 * 13 mod n = %s\n", c.to_hex().c_str());
|
||||
printf(" Expected 91 = ...5b (last byte)\n\n");
|
||||
|
||||
// 5) Micro-benchmark: scalar_mul(k, G)
|
||||
printf("[5] Benchmark: k * G (1000 iterations)\n");
|
||||
constexpr int ITERS = 1000;
|
||||
auto k = Scalar::from_hex(
|
||||
"0000000000000000000000000000000000000000000000000000000000000001");
|
||||
volatile uint8_t sink = 0;
|
||||
|
||||
double t0 = now_us();
|
||||
for (int i = 0; i < ITERS; ++i) {
|
||||
auto P = G.scalar_mul(k);
|
||||
sink ^= P.to_compressed()[0];
|
||||
k += Scalar::one();
|
||||
}
|
||||
double elapsed = now_us() - t0;
|
||||
printf(" Total : %.1f ms\n", elapsed / 1000.0);
|
||||
printf(" Per op: %.1f us\n", elapsed / ITERS);
|
||||
printf(" (sink = 0x%02x)\n\n", (unsigned)sink);
|
||||
|
||||
printf("=== Done ===\n");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user