From 16c63999b6a2dbbca2390834993124ed46e6e7c3 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Fri, 30 Jan 2026 12:28:59 +0100 Subject: [PATCH 01/21] remove library evolution --- Package.swift | 1 - Tests/K1Tests/Util/Wycheproof.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 78f2960..36e65b8 100644 --- a/Package.swift +++ b/Package.swift @@ -103,7 +103,6 @@ if development { .unsafeFlags([ "-Xfrontend", "-warn-concurrency", "-Xfrontend", "-enable-actor-data-race-checks", - "-enable-library-evolution", ]) ) } diff --git a/Tests/K1Tests/Util/Wycheproof.swift b/Tests/K1Tests/Util/Wycheproof.swift index 25ce3b6..f40ea32 100644 --- a/Tests/K1Tests/Util/Wycheproof.swift +++ b/Tests/K1Tests/Util/Wycheproof.swift @@ -82,7 +82,7 @@ extension XCTestCase { hashFunction: HF.Type, skipIfContainsFlags: [String] = [], skipIfContainsComment: [String] = [], - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws -> ResultOfTestGroup { guard group.key.curve == "secp256k1" else { From 15b0588701d3e325d54450917a59a06b1aac2eed Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Fri, 30 Jan 2026 14:47:49 +0100 Subject: [PATCH 02/21] first apinotes usage works, renames and removes nullability --- .../xcode/xcshareddata/xcschemes/K1.xcscheme | 10 ++++++ Package.swift | 9 +++++ .../K1/Support/FFI/API/ECDH/FFI+ECDH.swift | 6 ++-- .../K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift | 2 +- .../ECDSA+NonRecovery+Wrapped.swift | 2 +- .../NonRecovery/FFI+ECDSA+NonRecovery.swift | 2 +- .../Recovery/ECDSA+Recovery+Wrapped.swift | 2 +- .../ECDSA/Recovery/FFI+ECDSA+Recovery.swift | 2 +- .../Keys/PrivateKey/PrivateKey+Wrapped.swift | 2 +- .../API/Keys/PublicKey/FFI+PublicKey.swift | 2 +- .../Keys/PublicKey/PublicKey+Wrapped.swift | 2 +- .../Support/FFI/API/Schnorr/FFI+Schnorr.swift | 2 +- .../K1/Support/FFI/Internals/FFI+Call.swift | 2 +- .../Support/FFI/Internals/FFI+Context.swift | 2 +- .../K1/Support/FFI/Internals/FFI+Format.swift | 2 +- .../K1/Support/FFI/Internals/FFI+Raw.swift | 2 +- Sources/secp256k1/include/Secp256k1.apinotes | 9 +++++ Sources/secp256k1/include/module.modulemap | 4 +++ Sources/secp256k1/include/secp256k1.h | 2 +- Sources/secp256k1/src/ecdh_variants.c | 36 ++++++++++++++++--- Sources/secp256k1/src/ecdh_variants.h | 19 ++++++++-- Tests/ApinotesTests/ApinotesTests.swift | 15 ++++++++ justfile | 4 +++ 23 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 Sources/secp256k1/include/Secp256k1.apinotes create mode 100644 Sources/secp256k1/include/module.modulemap create mode 100644 Tests/ApinotesTests/ApinotesTests.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme index d7b025a..9c61637 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme @@ -66,6 +66,16 @@ ReferencedContainer = "container:"> + + + + ((@convention(c) (UnsafeMutablePointer?, UnsafePointer?, UnsafePointer?, UnsafeMutableRawPointer?) -> Int32)?) { switch self { case .libsecp256kDefault: return secp256k1_ecdh_hash_function_default - case .ansiX963: return ecdh_asn1_x963 - case .noHashWholePoint: return ecdh_unsafe_whole_point + case .ansiX963: return ecdh_hash_function_asn1_x963 + case .noHashWholePoint: return ecdh_hash_function_unsafe_whole_point } } diff --git a/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift b/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift index 4284049..8261a67 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: - FFI.ECDSA extension FFI { diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift index 384fb3a..a6ade66 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: - FFI.ECDSA.Wrapped extension FFI.ECDSA { diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift index ea5e643..966783c 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: Deserialize extension FFI.ECDSA { diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift index 31bcb37..23d2ae1 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: - FFI.ECDSAWithKeyRecovery.Wrapped extension FFI.ECDSAWithKeyRecovery { diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift index dab52be..5823eca 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: Deserialize extension FFI.ECDSAWithKeyRecovery { diff --git a/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift index e2b26fc..ced20f3 100644 --- a/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: - FFI.PrivateKey extension FFI { diff --git a/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift b/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift index 8e7ae40..d5f2f13 100644 --- a/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: Deserialize extension FFI.PublicKey { diff --git a/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift index 63e0cbb..d2e0ccf 100644 --- a/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: - FFI.PublicKey extension FFI { diff --git a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift index 027efdf..9e75737 100644 --- a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift +++ b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: - FFI.Schnorr extension FFI { diff --git a/Sources/K1/Support/FFI/Internals/FFI+Call.swift b/Sources/K1/Support/FFI/Internals/FFI+Call.swift index a9a688a..e76ac5d 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Call.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Call.swift @@ -1,5 +1,5 @@ import CryptoKit -import secp256k1 +import Secp256k1 // MARK: - FFI final class FFI { diff --git a/Sources/K1/Support/FFI/Internals/FFI+Context.swift b/Sources/K1/Support/FFI/Internals/FFI+Context.swift index 5c6e6db..ff9a1d8 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Context.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Context.swift @@ -1,5 +1,5 @@ // FFI to C -import secp256k1 +import Secp256k1 // MARK: - FFI.Context extension FFI { diff --git a/Sources/K1/Support/FFI/Internals/FFI+Format.swift b/Sources/K1/Support/FFI/Internals/FFI+Format.swift index fca91fd..23fdca3 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Format.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Format.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: Format extension K1.Format { diff --git a/Sources/K1/Support/FFI/Internals/FFI+Raw.swift b/Sources/K1/Support/FFI/Internals/FFI+Raw.swift index 38c3c15..6876196 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Raw.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Raw.swift @@ -1,5 +1,5 @@ import Foundation -import secp256k1 +import Secp256k1 // MARK: - Raw enum Raw {} diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes new file mode 100644 index 0000000..fd945d8 --- /dev/null +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -0,0 +1,9 @@ +Name: Secp256k1 +Functions: +- Name: ecdh_hash_function_asn1_x963_apinotes_test + SwiftName: ecdhHashASN1x963(output:x:) + Parameters: + - Position: 0 + Nullability: N # output + - Position: 1 + Nullability: N # x32 diff --git a/Sources/secp256k1/include/module.modulemap b/Sources/secp256k1/include/module.modulemap new file mode 100644 index 0000000..06b21e6 --- /dev/null +++ b/Sources/secp256k1/include/module.modulemap @@ -0,0 +1,4 @@ +module Secp256k1 { + header "secp256k1.h" + export * +} diff --git a/Sources/secp256k1/include/secp256k1.h b/Sources/secp256k1/include/secp256k1.h index 1d6d15a..11699dc 100644 --- a/Sources/secp256k1/include/secp256k1.h +++ b/Sources/secp256k1/include/secp256k1.h @@ -1,8 +1,8 @@ +#include "../libsecp256k1/include/secp256k1.h" #include "../libsecp256k1/include/secp256k1_ecdh.h" #include "../libsecp256k1/include/secp256k1_extrakeys.h" #include "../libsecp256k1/include/secp256k1_preallocated.h" #include "../libsecp256k1/include/secp256k1_recovery.h" #include "../libsecp256k1/include/secp256k1_schnorrsig.h" -#include "../libsecp256k1/include/secp256k1.h" #include "../src/ecdh_variants.h" diff --git a/Sources/secp256k1/src/ecdh_variants.c b/Sources/secp256k1/src/ecdh_variants.c index 6b4df1e..adc0738 100644 --- a/Sources/secp256k1/src/ecdh_variants.c +++ b/Sources/secp256k1/src/ecdh_variants.c @@ -8,7 +8,15 @@ #include "./ecdh_variants.h" #include -int ecdh_unsafe_whole_point(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data) { +static int ecdh_hash_function_asn1_x963_impl( + unsigned char *output, + const unsigned char *x32 +) { + memcpy(output, x32, 32); + return 1; +} + +int ecdh_hash_function_unsafe_whole_point(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data) { (void)data; /* Save x and y as uncompressed public key */ output[0] = 0x04; @@ -17,8 +25,26 @@ int ecdh_unsafe_whole_point(unsigned char *output, const unsigned char *x32, con return 1; } -int ecdh_asn1_x963(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data) { - (void)data; - memcpy(output, x32, 32); - return 1; +int ecdh_hash_function_asn1_x963( + unsigned char *output, + const unsigned char *x32, + const unsigned char *y32, + void *data +) { + (void)y32; + (void)data; + return ecdh_hash_function_asn1_x963_impl( + output, + x32 + ); +} + +int ecdh_hash_function_asn1_x963_apinotes_test( + unsigned char *output, + const unsigned char *x32 +) { + return ecdh_hash_function_asn1_x963_impl( + output, + x32 + ); } diff --git a/Sources/secp256k1/src/ecdh_variants.h b/Sources/secp256k1/src/ecdh_variants.h index 32c5d26..7acb160 100644 --- a/Sources/secp256k1/src/ecdh_variants.h +++ b/Sources/secp256k1/src/ecdh_variants.h @@ -21,7 +21,7 @@ * y32: pointer to a 32-byte y coordinate * data: arbitrary data pointer that is passed through */ -int ecdh_unsafe_whole_point(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data); +int ecdh_hash_function_unsafe_whole_point(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data); /** The ASN1 X9.63 ECDH variant which returns only the X component of ECDH secret (unhashed). * @@ -34,6 +34,21 @@ int ecdh_unsafe_whole_point(unsigned char *output, const unsigned char *x32, con * y32: pointer to a 32-byte y coordinate * data: arbitrary data pointer that is passed through */ -int ecdh_asn1_x963(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data); +int ecdh_hash_function_asn1_x963(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data); + +/** + The ASN1 X9.63 ECDH variant which returns only the X component of ECDH secret (unhashed). + + * Returns: 1 if the point was successfully hashed. + * 0 will cause secp256k1_ecdh to fail and return 0. + * Other return values are not allowed, and the behaviour of + * secp256k1_ecdh is undefined for other return values. + * Out: output: pointer to an array to be filled by the function + * In: x32: pointer to a 32-byte x coordinate + */ +int ecdh_hash_function_asn1_x963_apinotes_test( + unsigned char *output, + const unsigned char *x32 +); #endif /* ecdh_variants_h */ diff --git a/Tests/ApinotesTests/ApinotesTests.swift b/Tests/ApinotesTests/ApinotesTests.swift new file mode 100644 index 0000000..eb2e63e --- /dev/null +++ b/Tests/ApinotesTests/ApinotesTests.swift @@ -0,0 +1,15 @@ +import Testing +import Secp256k1 +import Foundation + +@Suite("Apinotes Tests") +struct Test { + @Test + func `C get Swiftified by apinotes file with non-nullability`() { + var output = [UInt8](repeating: 0, count: 32) + var x = [UInt8](repeating: 0xde, count: 32) + let expected = x + #expect(ecdhHashASN1x963(output: &output, x: &x) == 1) + #expect(output == expected) + } +} diff --git a/justfile b/justfile index 96823a9..8da1f31 100644 --- a/justfile +++ b/justfile @@ -32,6 +32,10 @@ bootstrap: dev: bootstrap init +synthesize-interface: + xcrun swift-synthesize-interface -I Sources/secp256k1/include -module-name Secp256k1 -target arm64-apple-macos15 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.sdk + + format: swiftformat --config .swiftformat "{{ROOT_DIR}}" From 28c66d06604aadc39c83f44090c9f07374fb90f7 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sat, 31 Jan 2026 18:56:39 +0100 Subject: [PATCH 03/21] Swift macros to build API using InlineArray --- .swiftformat | 1 - .../xcode/xcshareddata/xcschemes/K1.xcscheme | 10 ++ Package.resolved | 15 +++ Package.swift | 32 ++++++- Sources/K1Macros/MakeSafe.swift | 2 + Sources/K1MacrosImpl/K1MacrosPlugin.swift | 7 ++ Sources/K1MacrosImpl/MakeSafeMacro.swift | 93 +++++++++++++++++++ Sources/secp256k1/include/Secp256k1.apinotes | 34 ++++++- Sources/secp256k1/src/ecdh_variants.c | 14 +++ Sources/secp256k1/src/ecdh_variants.h | 10 ++ Tests/ApinotesTests/ApinotesTests.swift | 70 ++++++++++++-- justfile | 6 +- 12 files changed, 279 insertions(+), 15 deletions(-) create mode 100644 Package.resolved create mode 100644 Sources/K1Macros/MakeSafe.swift create mode 100644 Sources/K1MacrosImpl/K1MacrosPlugin.swift create mode 100644 Sources/K1MacrosImpl/MakeSafeMacro.swift diff --git a/.swiftformat b/.swiftformat index 1450d37..47f4e72 100644 --- a/.swiftformat +++ b/.swiftformat @@ -6,7 +6,6 @@ --tabwidth 4 --stripunusedargs closure-only ---enable marktypes --disable redundantNilInit,redundantSelf,extensionAccessControl,simplifyGenericConstraints --lineaftermarks false --ifdef no-indent diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme index 9c61637..a3ccaf4 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme @@ -66,6 +66,16 @@ ReferencedContainer = "container:"> + + + + [DeclSyntax] { + // ---- Parse args: (safeName, unsafeFn, count) ---- + let args = Array(node.arguments) + guard args.count == 3 else { + throw MakeSafeMacroError.message( + #"Expected 3 arguments: #makeSafe("safeName", unsafeFunction, count)"# + ) + } + + // 1) safeName: allows only "frobnicateThree" (must be String) + let safeNameExpr = args[0].expression + let safeName: String = try { + if let str = safeNameExpr.as(StringLiteralExprSyntax.self) { + // Accept single-segment string literal + guard + str.segments.count == 1, + let seg = str.segments.first?.as(StringSegmentSyntax.self) + else { + throw MakeSafeMacroError.message("safeName must be a simple string literal.") + } + return seg.content.text + } + if let ref = safeNameExpr.as(DeclReferenceExprSyntax.self) { + return ref.baseName.text + } + throw MakeSafeMacroError.message( + #"safeName must be a string literal or identifier, e.g. "frobnicateThree" or frobnicateThree"# + ) + }() + + // 2) unsafeFn: must be an identifier (e.g. frobnicateThreeUnsafe) + let unsafeExpr = args[1].expression + guard let unsafeRef = unsafeExpr.as(DeclReferenceExprSyntax.self) else { + throw MakeSafeMacroError.message( + #"unsafeFn must be an identifier, e.g. frobnicateThreeUnsafe"# + ) + } + let unsafeName = unsafeRef.baseName.text + + // 3) count: must be an integer literal + let countExpr = args[2].expression + guard let intLit = countExpr.as(IntegerLiteralExprSyntax.self) else { + throw MakeSafeMacroError.message(#"count must be an integer literal, e.g. 3"#) + } + let countText = intLit.literal.text + + // If count == 0, baseAddress! would be nil + if let countValue = Int(countText), countValue == 0 { + throw MakeSafeMacroError.message("count must be >= 1 for this wrapper (uses baseAddress!).") + } + + let decl: DeclSyntax = """ + @available(macOS 26.0, *) + func \(raw: safeName)( + into destination: inout InlineArray<\(raw: countText), UInt8>, + from source: InlineArray<\(raw: countText), UInt8> + ) { + var mutableSpan = destination.mutableSpan + mutableSpan.withUnsafeMutableBufferPointer { dst in + source.span.withUnsafeBufferPointer { src in + \(raw: unsafeName)( + into: dst.baseAddress!, + from: src.baseAddress! + ) + } + } + } + """ + + return [decl] + } +} + +// MARK: MakeSafeMacroError +enum MakeSafeMacroError: Error, CustomStringConvertible { + case message(String) +} + +extension MakeSafeMacroError { + var description: String { + switch self { + case let .message(message): return message + } + } +} diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index fd945d8..14f6c11 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -3,7 +3,33 @@ Functions: - Name: ecdh_hash_function_asn1_x963_apinotes_test SwiftName: ecdhHashASN1x963(output:x:) Parameters: - - Position: 0 - Nullability: N # output - - Position: 1 - Nullability: N # x32 + - Position: 0 # output + Nullability: N + - Position: 1 # x32 + Nullability: N +- Name: fill_with_fives + SwiftName: fillWithFives(span:) + Parameters: + - Position: 0 # buf + Lifetimebound: true + BoundsSafety: + Kind: counted_by + BoundedBy: "len" + Nullability: N + - Position: 1 # len + Nullability: N +- Name: clone_buf_of_len_three + SwiftName: cloneSpanOfLength3Unsafe(into:from:) + Parameters: + - Position: 0 # destination + Lifetimebound: true + BoundsSafety: + Kind: counted_by + BoundedBy: 3 + Nullability: N + - Position: 1 # source + Lifetimebound: true + BoundsSafety: + Kind: counted_by + BoundedBy: 3 + Nullability: N diff --git a/Sources/secp256k1/src/ecdh_variants.c b/Sources/secp256k1/src/ecdh_variants.c index adc0738..e86ccbf 100644 --- a/Sources/secp256k1/src/ecdh_variants.c +++ b/Sources/secp256k1/src/ecdh_variants.c @@ -48,3 +48,17 @@ int ecdh_hash_function_asn1_x963_apinotes_test( x32 ); } + +void fill_with_fives( + unsigned char *buf, + int len +) { + memset(buf, 5, len); +} + +void clone_buf_of_len_three( + unsigned char *destination, + const unsigned char *source +) { + memcpy(destination, source, 3); +} diff --git a/Sources/secp256k1/src/ecdh_variants.h b/Sources/secp256k1/src/ecdh_variants.h index 7acb160..dd053c9 100644 --- a/Sources/secp256k1/src/ecdh_variants.h +++ b/Sources/secp256k1/src/ecdh_variants.h @@ -51,4 +51,14 @@ int ecdh_hash_function_asn1_x963_apinotes_test( const unsigned char *x32 ); +void fill_with_fives( + unsigned char *buf, + int len +); + +void clone_buf_of_len_three( + unsigned char *destination, + const unsigned char *source +); + #endif /* ecdh_variants_h */ diff --git a/Tests/ApinotesTests/ApinotesTests.swift b/Tests/ApinotesTests/ApinotesTests.swift index eb2e63e..d98f3e5 100644 --- a/Tests/ApinotesTests/ApinotesTests.swift +++ b/Tests/ApinotesTests/ApinotesTests.swift @@ -1,15 +1,73 @@ import Testing import Secp256k1 import Foundation +import K1Macros + +struct Safe {} +extension Safe { + #makeSafe("cloneSpanOfLength3", cloneSpanOfLength3Unsafe, 3) +} + @Suite("Apinotes Tests") struct Test { + @available(macOS 26.0, *) @Test - func `C get Swiftified by apinotes file with non-nullability`() { - var output = [UInt8](repeating: 0, count: 32) - var x = [UInt8](repeating: 0xde, count: 32) - let expected = x - #expect(ecdhHashASN1x963(output: &output, x: &x) == 1) - #expect(output == expected) + func `C get Swiftified by apinotes file with non-nullability and MutableSpan`() { + + expectAllEqual(to: 5) { (bytes: inout InlineArray<3, UInt8>) in + var span = bytes.mutableSpan + fillWithFives(span: &span) + } } + + @available(macOS 26.0, *) + @Test + func `C fixed length`() { + let source: InlineArray<3, UInt8> = [7, 7, 7] + var destination: InlineArray<3, UInt8> = [0, 0, 0] + + Safe().cloneSpanOfLength3( + into: &destination, + from: source + ) + + for index in 0..( + to expectedElement: Element, + defaultElement: Element, + initBytes: (inout InlineArray) -> Void +) -> Bool { + var bytes: InlineArray = .init(repeating: defaultElement) + initBytes(&bytes) + for index in 0..( + to expectedElement: Element, + initBytes: @escaping (inout InlineArray) -> Void, + file: StaticString = #filePath, + line: UInt = #line +) { + #expect( + allEqual( + to: expectedElement, + defaultElement: .zero, + initBytes: initBytes + ), + "in file: \(file), line: \(line)" + ) } diff --git a/justfile b/justfile index 8da1f31..4e8f2f9 100644 --- a/justfile +++ b/justfile @@ -3,12 +3,12 @@ ROOT_DIR := justfile_directory() default: testdebug testdebug: - swift test + swift test --enable-experimental-prebuilts test: clean testdebug clean testprod testprod: - swift test -c release -Xswiftc -enable-testing + swift test -c release -Xswiftc -enable-testing --enable-experimental-prebuilts rmsubmod: rm -rf "$(ROOT_DIR)Sources/secp256k1/libsecp256k1" @@ -43,7 +43,7 @@ gyb: ./scripts/generate_boilerplate_files_with_gyb.sh bump-dep dryRun="false": - swift run \ + swift run --enable-experimental-prebuilts \ --package-path scripts/update-libsecp \ update-libsecp \ {{ if dryRun == "true" { "--dry-run" } else { "" } }} From 29144258e40b1068955c11d443a21578ef732a5c Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sun, 1 Feb 2026 12:20:43 +0100 Subject: [PATCH 04/21] Apinotes ornament ecdh with nullability --- .../K1/Support/FFI/API/ECDH/FFI+ECDH.swift | 28 +++++++-------- Sources/secp256k1/include/Secp256k1.apinotes | 23 ++++++++----- Sources/secp256k1/include/secp256k1.h | 1 + Sources/secp256k1/src/apinotes_tutorial.c | 34 +++++++++++++++++++ Sources/secp256k1/src/apinotes_tutorial.h | 31 +++++++++++++++++ Sources/secp256k1/src/ecdh_variants.c | 28 ++------------- Sources/secp256k1/src/ecdh_variants.h | 22 +----------- 7 files changed, 98 insertions(+), 69 deletions(-) create mode 100644 Sources/secp256k1/src/apinotes_tutorial.c create mode 100644 Sources/secp256k1/src/apinotes_tutorial.h diff --git a/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift b/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift index 1719c83..75dd624 100644 --- a/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift +++ b/Sources/K1/Support/FFI/API/ECDH/FFI+ECDH.swift @@ -73,23 +73,23 @@ extension FFI.ECDH { ) { context in if var arbitraryData { arbitraryData.withUnsafeMutableBytes { ptr in - secp256k1_ecdh( - context, - &sharedPublicPointBytes, // output - &publicKeyRaw, // pubkey - privateKey.secureBytes.backing.bytes, // seckey - hashFp.hashfp(), // hashfp - ptr.baseAddress // properly formed pointer + ecdh( + context: context, + outputSharedPointBytes: &sharedPublicPointBytes, + publicKey: &publicKeyRaw, + privateKeyBytes: privateKey.secureBytes.backing.bytes, + hashFunction: hashFp.hashfp(), + arbitraryData: ptr.baseAddress ) } } else { - secp256k1_ecdh( - context, - &sharedPublicPointBytes, - &publicKeyRaw, - privateKey.secureBytes.backing.bytes, - hashFp.hashfp(), - nil // No arbitrary data + ecdh( + context: context, + outputSharedPointBytes: &sharedPublicPointBytes, + publicKey: &publicKeyRaw, + privateKeyBytes: privateKey.secureBytes.backing.bytes, + hashFunction: hashFp.hashfp(), + arbitraryData: nil // No arbitrary data ) } } diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index 14f6c11..5403d97 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -1,5 +1,20 @@ Name: Secp256k1 Functions: +- Name: secp256k1_ecdh + SwiftName: ecdh(context:outputSharedPointBytes:publicKey:privateKeyBytes:hashFunction:arbitraryData:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # output + Nullability: N + - Position: 2 # pubkey + Nullability: N + - Position: 3 # seckey + Nullability: N + - Position: 4 # hashfp + Nullability: O + - Position: 5 # data + Nullability: O - Name: ecdh_hash_function_asn1_x963_apinotes_test SwiftName: ecdhHashASN1x963(output:x:) Parameters: @@ -22,14 +37,6 @@ Functions: SwiftName: cloneSpanOfLength3Unsafe(into:from:) Parameters: - Position: 0 # destination - Lifetimebound: true - BoundsSafety: - Kind: counted_by - BoundedBy: 3 Nullability: N - Position: 1 # source - Lifetimebound: true - BoundsSafety: - Kind: counted_by - BoundedBy: 3 Nullability: N diff --git a/Sources/secp256k1/include/secp256k1.h b/Sources/secp256k1/include/secp256k1.h index 11699dc..7fe6d1f 100644 --- a/Sources/secp256k1/include/secp256k1.h +++ b/Sources/secp256k1/include/secp256k1.h @@ -6,3 +6,4 @@ #include "../libsecp256k1/include/secp256k1_schnorrsig.h" #include "../src/ecdh_variants.h" +#include "../src/apinotes_tutorial.h" diff --git a/Sources/secp256k1/src/apinotes_tutorial.c b/Sources/secp256k1/src/apinotes_tutorial.c new file mode 100644 index 0000000..b3fdf85 --- /dev/null +++ b/Sources/secp256k1/src/apinotes_tutorial.c @@ -0,0 +1,34 @@ +// +// apinotes_tutorial.c +// K1 +// +// Created by Alexander Cyon on 2026-02-01. +// + +#include "./apinotes_tutorial.h" +#include "./ecdh_variants.h" +#include + +int ecdh_hash_function_asn1_x963_apinotes_test( + unsigned char *output, + const unsigned char *x32 +) { + return _ecdh_hash_function_asn1_x963_impl( + output, + x32 + ); +} + +void fill_with_fives( + unsigned char *buf, + int len +) { + memset(buf, 5, len); +} + +void clone_buf_of_len_three( + unsigned char *destination, + const unsigned char *source +) { + memcpy(destination, source, 3); +} diff --git a/Sources/secp256k1/src/apinotes_tutorial.h b/Sources/secp256k1/src/apinotes_tutorial.h new file mode 100644 index 0000000..2837646 --- /dev/null +++ b/Sources/secp256k1/src/apinotes_tutorial.h @@ -0,0 +1,31 @@ +#ifndef apinotes_tutorial_h +#define apinotes_tutorial_h + +#include "../libsecp256k1/include/secp256k1_ecdh.h" +/** + The ASN1 X9.63 ECDH variant which returns only the X component of ECDH secret (unhashed). + + * Returns: 1 if the point was successfully hashed. + * 0 will cause secp256k1_ecdh to fail and return 0. + * Other return values are not allowed, and the behaviour of + * secp256k1_ecdh is undefined for other return values. + * Out: output: pointer to an array to be filled by the function + * In: x32: pointer to a 32-byte x coordinate + */ +int ecdh_hash_function_asn1_x963_apinotes_test( + unsigned char *output, + const unsigned char *x32 +); + +void fill_with_fives( + unsigned char *buf, + int len +); + +void clone_buf_of_len_three( + unsigned char *destination, + const unsigned char *source +); + + +#endif /* apinotes_tutorial_h */ diff --git a/Sources/secp256k1/src/ecdh_variants.c b/Sources/secp256k1/src/ecdh_variants.c index e86ccbf..ef70b06 100644 --- a/Sources/secp256k1/src/ecdh_variants.c +++ b/Sources/secp256k1/src/ecdh_variants.c @@ -8,7 +8,7 @@ #include "./ecdh_variants.h" #include -static int ecdh_hash_function_asn1_x963_impl( +int _ecdh_hash_function_asn1_x963_impl( unsigned char *output, const unsigned char *x32 ) { @@ -33,32 +33,8 @@ int ecdh_hash_function_asn1_x963( ) { (void)y32; (void)data; - return ecdh_hash_function_asn1_x963_impl( + return _ecdh_hash_function_asn1_x963_impl( output, x32 ); } - -int ecdh_hash_function_asn1_x963_apinotes_test( - unsigned char *output, - const unsigned char *x32 -) { - return ecdh_hash_function_asn1_x963_impl( - output, - x32 - ); -} - -void fill_with_fives( - unsigned char *buf, - int len -) { - memset(buf, 5, len); -} - -void clone_buf_of_len_three( - unsigned char *destination, - const unsigned char *source -) { - memcpy(destination, source, 3); -} diff --git a/Sources/secp256k1/src/ecdh_variants.h b/Sources/secp256k1/src/ecdh_variants.h index dd053c9..f3f5131 100644 --- a/Sources/secp256k1/src/ecdh_variants.h +++ b/Sources/secp256k1/src/ecdh_variants.h @@ -36,29 +36,9 @@ int ecdh_hash_function_unsafe_whole_point(unsigned char *output, const unsigned */ int ecdh_hash_function_asn1_x963(unsigned char *output, const unsigned char *x32, const unsigned char *y32, void *data); -/** - The ASN1 X9.63 ECDH variant which returns only the X component of ECDH secret (unhashed). - - * Returns: 1 if the point was successfully hashed. - * 0 will cause secp256k1_ecdh to fail and return 0. - * Other return values are not allowed, and the behaviour of - * secp256k1_ecdh is undefined for other return values. - * Out: output: pointer to an array to be filled by the function - * In: x32: pointer to a 32-byte x coordinate - */ -int ecdh_hash_function_asn1_x963_apinotes_test( +int _ecdh_hash_function_asn1_x963_impl( unsigned char *output, const unsigned char *x32 ); -void fill_with_fives( - unsigned char *buf, - int len -); - -void clone_buf_of_len_three( - unsigned char *destination, - const unsigned char *source -); - #endif /* ecdh_variants_h */ From 36d6ed2998e7af2e09bbae47b18f8bdf2276a3ed Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sun, 1 Feb 2026 12:35:27 +0100 Subject: [PATCH 05/21] Apinotes ornament `secp256k1_xonly_pubkey_from_pubkey` -> `xOnlyKeyFromPublicKey(context:outputXOnlyKey:parity:publicKey:)` with nullability --- Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift | 10 +++++----- Sources/secp256k1/include/Secp256k1.apinotes | 11 +++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift index 9e75737..96913a9 100644 --- a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift +++ b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift @@ -17,11 +17,11 @@ extension FFI.Schnorr { var publicKeyX = secp256k1_xonly_pubkey() var publicKeyRaw = publicKey.raw try FFI.call(ifFailThrow: .xonlyPublicKeyFromPublicKey) { context in - secp256k1_xonly_pubkey_from_pubkey( - context, - &publicKeyX, - nil, - &publicKeyRaw + xOnlyKeyFromPublicKey( + context: context, + outputXOnlyKey: &publicKeyX, + parity: nil, + publicKey: &publicKeyRaw ) } diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index 5403d97..0bf7e53 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -1,5 +1,16 @@ Name: Secp256k1 Functions: +- Name: secp256k1_xonly_pubkey_from_pubkey + SwiftName: xOnlyKeyFromPublicKey(context:outputXOnlyKey:parity:publicKey:) + Parameters: + - Position: 0 # ctx (secp256k1_context) + Nullability: N + - Position: 1 # output (secp256k1_xonly_pubkey) + Nullability: N + - Position: 2 # Parity (int) + Nullability: O + - Position: 3 # publicKey (secp256k1_pubkey) + Nullability: N - Name: secp256k1_ecdh SwiftName: ecdh(context:outputSharedPointBytes:publicKey:privateKeyBytes:hashFunction:arbitraryData:) Parameters: From aa7157908037f5657b7c64f0cf1de209d2714f3f Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sun, 1 Feb 2026 15:25:00 +0100 Subject: [PATCH 06/21] Apinotes ornament `secp256k1_schnorrsig_verify` -> `verifySchnorrSignature` with nullability --- Package.swift | 1 + Sources/K1/K1/Keys/Keys.generated.swift | 15 ----- .../K1/Validation/Validation.generated.swift | 48 +++++++++++++- Sources/K1/K1/Validation/Validation.swift.gyb | 64 +++++++++++++++++++ .../Support/FFI/API/Schnorr/FFI+Schnorr.swift | 17 +++-- Sources/K1/Support/Misc/Span+Data.swift | 33 ++++++++++ Sources/secp256k1/include/Secp256k1.apinotes | 20 +++++- .../include/{secp256k1.h => k1_umbrella.h} | 0 Sources/secp256k1/include/module.modulemap | 3 +- 9 files changed, 173 insertions(+), 28 deletions(-) create mode 100644 Sources/K1/Support/Misc/Span+Data.swift rename Sources/secp256k1/include/{secp256k1.h => k1_umbrella.h} (100%) diff --git a/Package.swift b/Package.swift index ba59402..677231a 100644 --- a/Package.swift +++ b/Package.swift @@ -77,6 +77,7 @@ let package = Package( "libsecp256k1/SECURITY.md", ], cSettings: [ + .headerSearchPath("libsecp256k1/include"), // Basic config values that are universal and require no dependencies. // https://github.com/bitcoin-core/secp256k1/blob/master/src/basic-config.h#L12-L13 .define("ECMULT_WINDOW_SIZE", to: "15"), diff --git a/Sources/K1/K1/Keys/Keys.generated.swift b/Sources/K1/K1/Keys/Keys.generated.swift index 67678d5..d5c246f 100644 --- a/Sources/K1/K1/Keys/Keys.generated.swift +++ b/Sources/K1/K1/Keys/Keys.generated.swift @@ -896,28 +896,13 @@ extension K1.ECDSAWithKeyRecovery { } } -// MARK: - K1.KeyAgreement.PrivateKey + _K1PrivateKeyProtocol extension K1.KeyAgreement.PrivateKey: _K1PrivateKeyProtocol {} - -// MARK: - K1.KeyAgreement.PublicKey + _K1PublicKeyProtocol extension K1.KeyAgreement.PublicKey: _K1PublicKeyProtocol {} - -// MARK: - K1.Schnorr.PrivateKey + _K1PrivateKeyProtocol extension K1.Schnorr.PrivateKey: _K1PrivateKeyProtocol {} - -// MARK: - K1.Schnorr.PublicKey + _K1PublicKeyProtocol extension K1.Schnorr.PublicKey: _K1PublicKeyProtocol {} - -// MARK: - K1.ECDSA.PrivateKey + _K1PrivateKeyProtocol extension K1.ECDSA.PrivateKey: _K1PrivateKeyProtocol {} - -// MARK: - K1.ECDSA.PublicKey + _K1PublicKeyProtocol extension K1.ECDSA.PublicKey: _K1PublicKeyProtocol {} - -// MARK: - K1.ECDSAWithKeyRecovery.PrivateKey + _K1PrivateKeyProtocol extension K1.ECDSAWithKeyRecovery.PrivateKey: _K1PrivateKeyProtocol {} - -// MARK: - K1.ECDSAWithKeyRecovery.PublicKey + _K1PublicKeyProtocol extension K1.ECDSAWithKeyRecovery.PublicKey: _K1PublicKeyProtocol {} // swiftlint:enable all diff --git a/Sources/K1/K1/Validation/Validation.generated.swift b/Sources/K1/K1/Validation/Validation.generated.swift index 1e0a64b..71ab718 100644 --- a/Sources/K1/K1/Validation/Validation.generated.swift +++ b/Sources/K1/K1/Validation/Validation.generated.swift @@ -149,19 +149,63 @@ extension K1.Schnorr.PublicKey { /// - Returns: A Boolean value that’s true if the Schnorr signature is valid for the given _hashed_ data. public func isValidSignature( _ signature: K1.Schnorr.Signature, - hashed: some DataProtocol + hashed: Span ) -> Bool { do { return try FFI.Schnorr.isValid( signature: signature.wrapped, publicKey: self.impl.wrapped, - message: [UInt8](hashed) + message: hashed ) } catch { return false } } + /// Verifies Schnorr signature on some _hash_ over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The Schnorr signature to check against the _hashed_ data. + /// - hashed: The _hashed_ data covered by the signature. + /// - Returns: A Boolean value that’s true if the Schnorr signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.Schnorr.Signature, + hashed bytes: [UInt8] + ) -> Bool { + do { + return try withSpanFromArray(bytes) { span in + try FFI.Schnorr.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: span + ) + } + } catch { + return false + } + } + + /// Verifies Schnorr signature on some _hash_ over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The Schnorr signature to check against the _hashed_ data. + /// - hashed: The _hashed_ data covered by the signature. + /// - Returns: A Boolean value that’s true if the Schnorr signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.Schnorr.Signature, + hashed data: some DataProtocol + ) -> Bool { + do { + return try withSpanFromData(data) { span in + try FFI.Schnorr.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: span + ) + } + } catch { + return false + } + } + /// Verifies Schnorr signature on a digest over the `secp256k1` elliptic curve. /// - Parameters: /// - signature: The Schnorr signature to check against the given digest. diff --git a/Sources/K1/K1/Validation/Validation.swift.gyb b/Sources/K1/K1/Validation/Validation.swift.gyb index d44ddde..8a0235f 100644 --- a/Sources/K1/K1/Validation/Validation.swift.gyb +++ b/Sources/K1/K1/Validation/Validation.swift.gyb @@ -50,6 +50,69 @@ import protocol CryptoKit.Digest extension K1.${FEATURE}.PublicKey { /// Verifies ${VARIANT} signature on some _hash_ over the `secp256k1` elliptic curve. /// - Parameters: +% if FEATURE == "Schnorr": + /// - signature: The ${VARIANT} signature to check against the _hashed_ data. + /// - hashed: The _hashed_ data covered by the signature.${DOC_VALIDATION_PARAM_OPTIONS} + /// - Returns: A Boolean value that’s true if the ${VARIANT} signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.${FEATURE}.Signature, + hashed: Span${VALIDATION_OPTIONS_ARG} + ) -> Bool { + do { + return try FFI.${FEATURE}.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: hashed${VALIDATION_OPTIONS_FWD} + ) + } catch { + return false + } + } + + /// Verifies ${VARIANT} signature on some _hash_ over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The ${VARIANT} signature to check against the _hashed_ data. + /// - hashed: The _hashed_ data covered by the signature.${DOC_VALIDATION_PARAM_OPTIONS} + /// - Returns: A Boolean value that’s true if the ${VARIANT} signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.${FEATURE}.Signature, + hashed bytes: [UInt8]${VALIDATION_OPTIONS_ARG} + ) -> Bool { + do { + return try withSpanFromArray(bytes) { span in + try FFI.${FEATURE}.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: span${VALIDATION_OPTIONS_FWD} + ) + } + } catch { + return false + } + } + + /// Verifies ${VARIANT} signature on some _hash_ over the `secp256k1` elliptic curve. + /// - Parameters: + /// - signature: The ${VARIANT} signature to check against the _hashed_ data. + /// - hashed: The _hashed_ data covered by the signature.${DOC_VALIDATION_PARAM_OPTIONS} + /// - Returns: A Boolean value that’s true if the ${VARIANT} signature is valid for the given _hashed_ data. + public func isValidSignature( + _ signature: K1.${FEATURE}.Signature, + hashed data: some DataProtocol${VALIDATION_OPTIONS_ARG} + ) -> Bool { + do { + return try withSpanFromData(data) { span in + try FFI.${FEATURE}.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: span${VALIDATION_OPTIONS_FWD} + ) + } + } catch { + return false + } + } +% else: /// - signature: The ${VARIANT} signature to check against the _hashed_ data. /// - hashed: The _hashed_ data covered by the signature.${DOC_VALIDATION_PARAM_OPTIONS} /// - Returns: A Boolean value that’s true if the ${VARIANT} signature is valid for the given _hashed_ data. @@ -67,6 +130,7 @@ extension K1.${FEATURE}.PublicKey { return false } } +% end /// Verifies ${VARIANT} signature on a digest over the `secp256k1` elliptic curve. /// - Parameters: diff --git a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift index 96913a9..2e18fd4 100644 --- a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift +++ b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift @@ -11,27 +11,26 @@ extension FFI.Schnorr { static func isValid( signature: FFI.Schnorr.Wrapped, publicKey: FFI.PublicKey.Wrapped, - message: [UInt8] + message: Span ) throws -> Bool { try FFI.toC { ffi -> Bool in var publicKeyX = secp256k1_xonly_pubkey() var publicKeyRaw = publicKey.raw try FFI.call(ifFailThrow: .xonlyPublicKeyFromPublicKey) { context in - xOnlyKeyFromPublicKey( + xOnlyPublicKeyFromPublicKey( context: context, - outputXOnlyKey: &publicKeyX, + outputXOnlyPublicKey: &publicKeyX, parity: nil, publicKey: &publicKeyRaw ) } return ffi.validate { context in - secp256k1_schnorrsig_verify( - context, - signature.bytes, - message, - message.count, - &publicKeyX + verifySchnorrSignature( + context: context, + signatureBytes: signature.bytes, + msg: message, + xOnlyPublicKey: &publicKeyX ) } } diff --git a/Sources/K1/Support/Misc/Span+Data.swift b/Sources/K1/Support/Misc/Span+Data.swift new file mode 100644 index 0000000..e4d9765 --- /dev/null +++ b/Sources/K1/Support/Misc/Span+Data.swift @@ -0,0 +1,33 @@ +import Foundation + +// FIXME: These are temporary, and should be removed. We should upgrade whole code base to use Span/InlineArray as much as possible. + +// MARK: - Shared helpers +@usableFromInline +func withSpanFromArray( + _ bytes: [UInt8], + _ body: (Span) throws -> R +) rethrows -> R { + try bytes.withUnsafeBufferPointer { buf in + try body(Span(_unsafeElements: buf)) + } +} + +@usableFromInline +func withSpanFromData( + _ data: some DataProtocol, + _ body: (Span) throws -> R +) rethrows -> R { + if let contiguous = data as? any ContiguousBytes { + return try contiguous.withUnsafeBytes { raw in + let buf = raw.bindMemory(to: UInt8.self) + return try body(Span(_unsafeElements: buf)) + } + } + + var copy = [UInt8](data) + return try copy.withUnsafeMutableBufferPointer { buf in + let readonly = UnsafeBufferPointer(buf) + return try body(Span(_unsafeElements: readonly)) + } +} diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index 0bf7e53..eb50339 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -1,7 +1,25 @@ Name: Secp256k1 Functions: +- Name: secp256k1_schnorrsig_verify + # Keep the original arity in SwiftName; SafeInterop will still fuse msg/msglen into Span. + SwiftName: verifySchnorrSignature(context:signatureBytes:msg:msglen:xOnlyPublicKey:) + Parameters: + - Position: 0 # ctx (secp256k1_context) + Nullability: N + - Position: 1 # sig64 + Nullability: N + - Position: 2 # msg + Lifetimebound: true + BoundsSafety: + Kind: counted_by + BoundedBy: "msglen" + Nullability: N + - Position: 3 # msglen + Nullability: N + - Position: 4 # pubkey (secp256k1_xonly_pubkey) + Nullability: N - Name: secp256k1_xonly_pubkey_from_pubkey - SwiftName: xOnlyKeyFromPublicKey(context:outputXOnlyKey:parity:publicKey:) + SwiftName: xOnlyPublicKeyFromPublicKey(context:outputXOnlyPublicKey:parity:publicKey:) Parameters: - Position: 0 # ctx (secp256k1_context) Nullability: N diff --git a/Sources/secp256k1/include/secp256k1.h b/Sources/secp256k1/include/k1_umbrella.h similarity index 100% rename from Sources/secp256k1/include/secp256k1.h rename to Sources/secp256k1/include/k1_umbrella.h diff --git a/Sources/secp256k1/include/module.modulemap b/Sources/secp256k1/include/module.modulemap index 06b21e6..4dc9902 100644 --- a/Sources/secp256k1/include/module.modulemap +++ b/Sources/secp256k1/include/module.modulemap @@ -1,4 +1,5 @@ module Secp256k1 { - header "secp256k1.h" + umbrella header "k1_umbrella.h" export * + module * { export * } } From edc3a0c662e87a61052d3763d50b74e632e6a4f1 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sun, 1 Feb 2026 15:31:33 +0100 Subject: [PATCH 07/21] Apinotes ornament `secp256k1_keypair_create` -> `keypairFromPrivateKey(context:outputKeyPair:privateKeyBytes:)` with nullability --- Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift | 8 ++++---- Sources/K1/Support/Misc/K1+Error.swift | 2 +- Sources/secp256k1/include/Secp256k1.apinotes | 11 ++++++++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift index 2e18fd4..b616708 100644 --- a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift +++ b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift @@ -57,10 +57,10 @@ extension FFI.Schnorr { try FFI.call( ifFailThrow: .keypairCreate ) { context in - secp256k1_keypair_create( - context, - &keyPair, - privateKey.secureBytes.backing.bytes + keypairFromPrivateKey( + context: context, + outputKeyPair: &keyPair, + privateKeyBytes: privateKey.secureBytes.backing.bytes ) } diff --git a/Sources/K1/Support/Misc/K1+Error.swift b/Sources/K1/Support/Misc/K1+Error.swift index 2b49130..289d9e5 100644 --- a/Sources/K1/Support/Misc/K1+Error.swift +++ b/Sources/K1/Support/Misc/K1+Error.swift @@ -84,7 +84,7 @@ extension FFI { /// `secp256k1_xonly_pubkey_from_pubkey` failed case xonlyPublicKeyFromPublicKey - /// `secp256k1_keypair_create` failed + /// `keypairFromPrivateKey` (`secp256k1_keypair_create`) failed case keypairCreate /// `secp256k1_schnorrsig_sign32` failed diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index eb50339..6ab755e 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -1,7 +1,16 @@ Name: Secp256k1 Functions: +- Name: secp256k1_keypair_create + SwiftName: keypairFromPrivateKey(context:outputKeyPair:privateKeyBytes:) + Parameters: + - Position: 0 # ctx (secp256k1_context) + Nullability: N + - Position: 1 # keypair output (secp256k1_keypair) + Nullability: N + - Position: 2 # seckey + Nullability: N + - Name: secp256k1_schnorrsig_verify - # Keep the original arity in SwiftName; SafeInterop will still fuse msg/msglen into Span. SwiftName: verifySchnorrSignature(context:signatureBytes:msg:msglen:xOnlyPublicKey:) Parameters: - Position: 0 # ctx (secp256k1_context) From be59287888d50c5150567cbcc723dac87919ebc4 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sun, 1 Feb 2026 15:38:45 +0100 Subject: [PATCH 08/21] Apinotes ornament `secp256k1_schnorrsig_sign32` -> `schnorrSign(context:outputSignatureBytes:message:keypair:auxiliaryRandomData:)` with nullability --- .../K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift | 12 ++++++------ Sources/secp256k1/include/Secp256k1.apinotes | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift index b616708..59ac1ad 100644 --- a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift +++ b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift @@ -67,12 +67,12 @@ extension FFI.Schnorr { try FFI.call( ifFailThrow: .schnorrSign ) { context in - secp256k1_schnorrsig_sign32( - context, - &signatureOut, - message, - &keyPair, - options.auxiliaryRandomData.bytes + schnorrSign( + context: context, + outputSignatureBytes: &signatureOut, + message: message, + keypair: &keyPair, + auxiliaryRandomData: options.auxiliaryRandomData.bytes ) } diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index 6ab755e..6063e17 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -1,5 +1,19 @@ Name: Secp256k1 Functions: +- Name: secp256k1_schnorrsig_sign32 + SwiftName: schnorrSign(context:outputSignatureBytes:message:keypair:auxiliaryRandomData:) + Parameters: + - Position: 0 # ctx (secp256k1_context) + Nullability: N + - Position: 1 # sig64 + Nullability: N + - Position: 2 # msg32 + Nullability: N + - Position: 3 # keypair + Nullability: N + - Position: 4 # aux_rand32 + Nullability: O + - Name: secp256k1_keypair_create SwiftName: keypairFromPrivateKey(context:outputKeyPair:privateKeyBytes:) Parameters: From 4561a1dba810daba604b3b149f46b0ef5b65a221 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sun, 1 Feb 2026 16:34:15 +0100 Subject: [PATCH 09/21] Apinotes ornament `secp256k1_ecdsa_sign_recoverable` -> `ecdsaSignRecoverable(context:outputSignature:hashedMessageBytes: privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:)` and `secp256k1_ecdsa_sign` -> `ecdsaSignNonRecoverable(context:outputSignature:hashedMessageBytes: privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:)` with nullability --- .../K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift | 4 +-- .../ECDSA+NonRecovery+Wrapped.swift | 4 +-- .../Recovery/ECDSA+Recovery+Wrapped.swift | 13 ++++++-- Sources/secp256k1/include/Secp256k1.apinotes | 33 +++++++++++++++++++ 4 files changed, 48 insertions(+), 6 deletions(-) diff --git a/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift b/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift index 8261a67..f887f0f 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift @@ -25,7 +25,7 @@ protocol WrappedECDSASignature { var raw: Raw { get } // swiftlint:disable:next line_length - static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 + static func sign() -> ECDSAFunctionPointer } // MARK: ECDSA Shared @@ -70,7 +70,7 @@ extension K1.ECDSA.SigningOptions { extension K1.ECDSA.SigningOptions.NonceFunction { // swiftlint:disable:next line_length - fileprivate func function() -> (@convention(c) (UnsafeMutablePointer?, UnsafePointer?, UnsafePointer?, UnsafePointer?, UnsafeMutableRawPointer?, UInt32) -> Int32)? { + fileprivate func function() -> secp256k1_nonce_function? { switch self { case .deterministic: return secp256k1_nonce_function_rfc6979 diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift index a6ade66..1156a02 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift @@ -16,8 +16,8 @@ extension FFI.ECDSA { // MARK: Sign extension FFI.ECDSA.Wrapped { // swiftlint:disable:next line_length - static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 { - secp256k1_ecdsa_sign + static func sign() -> ECDSAFunctionPointer { + ecdsaSignNonRecoverable(context:outputSignature:hashedMessageBytes: privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:) } } diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift index 23d2ae1..d181670 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift @@ -13,11 +13,20 @@ extension FFI.ECDSAWithKeyRecovery { } } +typealias ECDSAFunctionPointer = ( + OpaquePointer, + UnsafeMutablePointer, + UnsafePointer, + UnsafePointer, + secp256k1_nonce_function?, + UnsafeRawPointer? +) -> Int32 + // MARK: Sign extension FFI.ECDSAWithKeyRecovery.Wrapped { // swiftlint:disable:next line_length - static func sign() -> (OpaquePointer, UnsafeMutablePointer, UnsafePointer, UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer?) -> Int32 { - secp256k1_ecdsa_sign_recoverable + static func sign() -> ECDSAFunctionPointer { + ecdsaSignRecoverable(context:outputSignature:hashedMessageBytes: privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:) } } diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index 6063e17..ccc863d 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -1,5 +1,38 @@ Name: Secp256k1 + Functions: +- Name: secp256k1_ecdsa_sign + SwiftName: ecdsaSignNonRecoverable(context:outputSignature:hashedMessageBytes:privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sig / outputSignature + Nullability: N + - Position: 2 # msghash32 + Nullability: N + - Position: 3 # seckey + Nullability: N + - Position: 4 # noncefp + Nullability: O + - Position: 5 # ndata + Nullability: O + +- Name: secp256k1_ecdsa_sign_recoverable + SwiftName: ecdsaSignRecoverable(context:outputSignature:hashedMessageBytes:privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sig / outputSignature + Nullability: N + - Position: 2 # msghash32 + Nullability: N + - Position: 3 # seckey + Nullability: N + - Position: 4 # noncefp + Nullability: O + - Position: 5 # ndata + Nullability: O + - Name: secp256k1_schnorrsig_sign32 SwiftName: schnorrSign(context:outputSignatureBytes:message:keypair:auxiliaryRandomData:) Parameters: From ff99e8d0f82ca6f47409200367d470e3a3e06b87 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sun, 1 Feb 2026 16:46:49 +0100 Subject: [PATCH 10/21] Apinotes ornament of types `secp256k1_ecdsa_signature` -> `ECDSASignatureRaw` and `secp256k1_ecdsa_recoverable_signature` -> `ECDSARecoverableSignatureRaw` --- Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift | 14 +++++++------- .../NonRecovery/ECDSA+NonRecovery+Wrapped.swift | 2 +- .../ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift | 4 ++-- .../ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift | 2 +- .../API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift | 2 +- Sources/K1/Support/FFI/Internals/FFI+Raw.swift | 12 ++++++------ Sources/secp256k1/include/Secp256k1.apinotes | 12 ++++++++++++ 7 files changed, 30 insertions(+), 18 deletions(-) diff --git a/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift b/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift index f887f0f..80eecf2 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/FFI+ECDSA.swift @@ -7,20 +7,20 @@ extension FFI { public enum ECDSA {} } -// MARK: - RawECDSASignature -protocol RawECDSASignature { +// MARK: - EmptyInitializable +protocol EmptyInitializable { init() } -// MARK: - secp256k1_ecdsa_recoverable_signature + RawECDSASignature -extension secp256k1_ecdsa_recoverable_signature: RawECDSASignature {} +// MARK: - ECDSARecoverableSignatureRaw + EmptyInitializable +extension ECDSARecoverableSignatureRaw: EmptyInitializable {} -// MARK: - secp256k1_ecdsa_signature + RawECDSASignature -extension secp256k1_ecdsa_signature: RawECDSASignature {} +// MARK: - ECDSASignatureRaw + EmptyInitializable +extension ECDSASignatureRaw: EmptyInitializable {} // MARK: - WrappedECDSASignature protocol WrappedECDSASignature { - associatedtype Raw: RawECDSASignature + associatedtype Raw: EmptyInitializable init(raw: Raw) var raw: Raw { get } diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift index 1156a02..1d4eda4 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift @@ -5,7 +5,7 @@ import Secp256k1 extension FFI.ECDSA { struct Wrapped: @unchecked Sendable, ContiguousBytes, WrappedECDSASignature { // swiftlint:disable:next nesting - typealias Raw = secp256k1_ecdsa_signature + typealias Raw = ECDSASignatureRaw let raw: Raw init(raw: Raw) { self.raw = raw diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift index 966783c..6326208 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift @@ -83,7 +83,7 @@ extension FFI.ECDSA { throw K1.Error.incorrectParameterSize } var compact = [UInt8](nonRecoverableCompact) - var recoverable = secp256k1_ecdsa_recoverable_signature() + var recoverable = ECDSARecoverableSignatureRaw() try FFI.call(ifFailThrow: .recoverableSignatureParseCompact) { context in secp256k1_ecdsa_recoverable_signature_parse_compact( context, @@ -116,7 +116,7 @@ extension FFI.ECDSA { try FFI.toC { ffi -> Bool in var publicKeyRaw = publicKey.raw var maybeMalleable = signature.raw - var normalized = secp256k1_ecdsa_signature() + var normalized = ECDSASignatureRaw() let codeForSignatureWasMalleable = 1 let signatureWasMalleableResult = ffi.callWithResultCode { context in diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift index d181670..8f48ded 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift @@ -5,7 +5,7 @@ import Secp256k1 extension FFI.ECDSAWithKeyRecovery { struct Wrapped: @unchecked Sendable, ContiguousBytes, WrappedECDSASignature { // swiftlint:disable:next nesting - typealias Raw = secp256k1_ecdsa_recoverable_signature + typealias Raw = ECDSARecoverableSignatureRaw let raw: Raw init(raw: Raw) { self.raw = raw diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift index 5823eca..87a9811 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift @@ -60,7 +60,7 @@ extension FFI.ECDSAWithKeyRecovery { static func nonRecoverable( _ wrapped: Wrapped ) throws -> FFI.ECDSA.Wrapped { - var nonRecoverable = secp256k1_ecdsa_signature() + var nonRecoverable = ECDSASignatureRaw() var recoverable = wrapped.raw try FFI.call( diff --git a/Sources/K1/Support/FFI/Internals/FFI+Raw.swift b/Sources/K1/Support/FFI/Internals/FFI+Raw.swift index 6876196..2cfcc47 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Raw.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Raw.swift @@ -7,14 +7,14 @@ enum Raw {} extension Raw { static func recoverableSignature( _ rawRepresentation: some DataProtocol - ) throws -> secp256k1_ecdsa_recoverable_signature { + ) throws -> ECDSARecoverableSignatureRaw { let expected = K1.ECDSAWithKeyRecovery.Signature.Compact.byteCount guard rawRepresentation.count == expected else { throw K1.Error.incorrectParameterSize } - var raw = secp256k1_ecdsa_recoverable_signature() + var raw = ECDSARecoverableSignatureRaw() withUnsafeMutableBytes(of: &raw.data) { pointer in pointer.copyBytes( from: rawRepresentation.prefix(pointer.count) @@ -25,8 +25,8 @@ extension Raw { static func nonRecoverableSignature( compactBytes: [UInt8] - ) throws -> secp256k1_ecdsa_signature { - var raw = secp256k1_ecdsa_signature() + ) throws -> ECDSASignatureRaw { + var raw = ECDSASignatureRaw() try FFI.call(ifFailThrow: .ecdsaSignatureParseCompact) { context in secp256k1_ecdsa_signature_parse_compact( @@ -41,8 +41,8 @@ extension Raw { static func nonRecoverableSignature( derBytes: [UInt8] - ) throws -> secp256k1_ecdsa_signature { - var raw = secp256k1_ecdsa_signature() + ) throws -> ECDSASignatureRaw { + var raw = ECDSASignatureRaw() try FFI.call(ifFailThrow: .ecdsaSignatureParseDER) { context in secp256k1_ecdsa_signature_parse_der( diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index ccc863d..6f238b4 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -1,5 +1,12 @@ Name: Secp256k1 +Tags: +- Name: secp256k1_ecdsa_signature + SwiftName: ECDSASignatureRaw + +- Name: secp256k1_ecdsa_recoverable_signature + SwiftName: ECDSARecoverableSignatureRaw + Functions: - Name: secp256k1_ecdsa_sign SwiftName: ecdsaSignNonRecoverable(context:outputSignature:hashedMessageBytes:privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:) @@ -74,6 +81,7 @@ Functions: Nullability: N - Position: 4 # pubkey (secp256k1_xonly_pubkey) Nullability: N + - Name: secp256k1_xonly_pubkey_from_pubkey SwiftName: xOnlyPublicKeyFromPublicKey(context:outputXOnlyPublicKey:parity:publicKey:) Parameters: @@ -85,6 +93,7 @@ Functions: Nullability: O - Position: 3 # publicKey (secp256k1_pubkey) Nullability: N + - Name: secp256k1_ecdh SwiftName: ecdh(context:outputSharedPointBytes:publicKey:privateKeyBytes:hashFunction:arbitraryData:) Parameters: @@ -100,6 +109,7 @@ Functions: Nullability: O - Position: 5 # data Nullability: O + - Name: ecdh_hash_function_asn1_x963_apinotes_test SwiftName: ecdhHashASN1x963(output:x:) Parameters: @@ -107,6 +117,7 @@ Functions: Nullability: N - Position: 1 # x32 Nullability: N + - Name: fill_with_fives SwiftName: fillWithFives(span:) Parameters: @@ -118,6 +129,7 @@ Functions: Nullability: N - Position: 1 # len Nullability: N + - Name: clone_buf_of_len_three SwiftName: cloneSpanOfLength3Unsafe(into:from:) Parameters: From c84f0530ea908c1474051ce8f54da9e5b7a23bcc Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sun, 1 Feb 2026 16:55:28 +0100 Subject: [PATCH 11/21] Apinotes rename type `secp256k1_pubkey` -> `PublicKeyRaw` and `secp256k1_keypair` -> `KeypairRaw` and map function `secp256k1_ecdsa_recover` -> `recoverPublicKeyFromECDSASignature(context:publicKey:signature:hashedMessage:)` --- .../NonRecovery/FFI+ECDSA+NonRecovery.swift | 12 ++++++------ .../ECDSA/Recovery/FFI+ECDSA+Recovery.swift | 12 ++++++------ .../Keys/PrivateKey/PrivateKey+Wrapped.swift | 2 +- .../FFI/API/Keys/PublicKey/FFI+PublicKey.swift | 2 +- .../API/Keys/PublicKey/PublicKey+Wrapped.swift | 6 +++--- .../Support/FFI/API/Schnorr/FFI+Schnorr.swift | 2 +- Sources/secp256k1/include/Secp256k1.apinotes | 18 ++++++++++++++++++ 7 files changed, 36 insertions(+), 18 deletions(-) diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift index 6326208..815ef6d 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift @@ -92,13 +92,13 @@ extension FFI.ECDSA { recoveryID ) } - var publicKeyRaw = secp256k1_pubkey() + var publicKeyRaw = PublicKeyRaw() try FFI.call(ifFailThrow: .recover) { context in - secp256k1_ecdsa_recover( - context, - &publicKeyRaw, - &recoverable, - message + recoverPublicKeyFromECDSASignature( + context: context, + publicKey: &publicKeyRaw, + signature: &recoverable, + hashedMessage: message ) } return FFI.PublicKey.Wrapped(raw: publicKeyRaw) diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift index 87a9811..3a17e57 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift @@ -87,15 +87,15 @@ extension FFI.ECDSAWithKeyRecovery { throw K1.Error.incorrectParameterSize } var rawSignature = wrapped.raw - var rawPublicKey = secp256k1_pubkey() + var rawPublicKey = PublicKeyRaw() try FFI.call( ifFailThrow: .recover ) { context in - secp256k1_ecdsa_recover( - context, - &rawPublicKey, - &rawSignature, - message + recoverPublicKeyFromECDSASignature( + context: context, + publicKey: &rawPublicKey, + signature: &rawSignature, + hashedMessage: message ) } return FFI.PublicKey.Wrapped(raw: rawPublicKey) diff --git a/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift index ced20f3..629124f 100644 --- a/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift @@ -21,7 +21,7 @@ extension FFI { self.secureBytes = secureBytes var secureBytes = secureBytes self.publicKey = try secureBytes.withUnsafeMutableBytes { seckey in - var raw = secp256k1_pubkey() + var raw = PublicKeyRaw() try FFI.call(ifFailThrow: .publicKeyCreate) { context in secp256k1_ec_pubkey_create( diff --git a/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift b/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift index d5f2f13..a1a50b5 100644 --- a/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift @@ -52,7 +52,7 @@ extension FFI.PublicKey { } private static func _deserialize(bytes: [UInt8]) throws -> Wrapped { - var raw = secp256k1_pubkey() + var raw = PublicKeyRaw() try FFI.call( ifFailThrow: .publicKeyParse ) { context in diff --git a/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift index d2e0ccf..0d9f6e4 100644 --- a/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift @@ -10,7 +10,7 @@ extension FFI { extension FFI.PublicKey { struct Wrapped: @unchecked Sendable, ContiguousBytes { // swiftlint:disable:next nesting - typealias Raw = secp256k1_pubkey + typealias Raw = PublicKeyRaw let raw: Raw init(raw: Raw) { self.raw = raw @@ -70,11 +70,11 @@ extension FFI.PublicKey.Wrapped { throw K1.Error.invalidParameter } - var result = secp256k1_pubkey() + var result = PublicKeyRaw() var mutableKeys = keys.map(\.raw) try mutableKeys.withUnsafeMutableBufferPointer { keysBuffer in - var keyPointers = [UnsafePointer?]() + var keyPointers = [UnsafePointer?]() keyPointers.reserveCapacity(keys.count) for index in 0 ..< keys.count { diff --git a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift index 59ac1ad..6c0e0f5 100644 --- a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift +++ b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift @@ -52,7 +52,7 @@ extension FFI.Schnorr { var signatureOut = [UInt8](repeating: 0, count: FFI.Schnorr.Wrapped.byteCount) - var keyPair = secp256k1_keypair() + var keyPair = KeypairRaw() try FFI.call( ifFailThrow: .keypairCreate diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index 6f238b4..4e46c27 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -7,7 +7,25 @@ Tags: - Name: secp256k1_ecdsa_recoverable_signature SwiftName: ECDSARecoverableSignatureRaw +- Name: secp256k1_keypair + SwiftName: KeypairRaw + +- Name: secp256k1_pubkey + SwiftName: PublicKeyRaw + Functions: +- Name: secp256k1_ecdsa_recover + SwiftName: recoverPublicKeyFromECDSASignature(context:publicKey:signature:hashedMessage:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # pubkey + Nullability: N + - Position: 2 # sig + Nullability: N + - Position: 3 # msghash32 + Nullability: N + - Name: secp256k1_ecdsa_sign SwiftName: ecdsaSignNonRecoverable(context:outputSignature:hashedMessageBytes:privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:) Parameters: From 1a340188395042550b7e6a2ae5367e38892c3477 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sun, 1 Feb 2026 17:01:13 +0100 Subject: [PATCH 12/21] Apinotes ornament `secp256k1_ecdsa_recoverable_signature_parse_compact` -> `parseRecoverableECDSASignatureFromCompactBytes(context:outputRecoveredSignature:compactBytes:recoveryID:)` --- .../ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift | 14 +++++++------- .../API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift | 10 +++++----- Sources/secp256k1/include/Secp256k1.apinotes | 12 ++++++++++++ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift index 815ef6d..93dc113 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift @@ -83,13 +83,13 @@ extension FFI.ECDSA { throw K1.Error.incorrectParameterSize } var compact = [UInt8](nonRecoverableCompact) - var recoverable = ECDSARecoverableSignatureRaw() + var recovered = ECDSARecoverableSignatureRaw() try FFI.call(ifFailThrow: .recoverableSignatureParseCompact) { context in - secp256k1_ecdsa_recoverable_signature_parse_compact( - context, - &recoverable, - &compact, - recoveryID + parseRecoverableECDSASignatureFromCompactBytes( + context: context, + outputRecoveredSignature: &recovered, + compactBytes: &compact, + recoveryID: recoveryID ) } var publicKeyRaw = PublicKeyRaw() @@ -97,7 +97,7 @@ extension FFI.ECDSA { recoverPublicKeyFromECDSASignature( context: context, publicKey: &publicKeyRaw, - signature: &recoverable, + signature: &recovered, hashedMessage: message ) } diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift index 3a17e57..9ef0e85 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift @@ -21,11 +21,11 @@ extension FFI.ECDSAWithKeyRecovery { try FFI.call( ifFailThrow: .recoverableSignatureParseCompact ) { context in - secp256k1_ecdsa_recoverable_signature_parse_compact( - context, - &raw, - rs, - recid + parseRecoverableECDSASignatureFromCompactBytes( + context: context, + outputRecoveredSignature: &raw, + compactBytes: rs, + recoveryID: recid ) } return .init(raw: raw) diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index 4e46c27..07e0d1c 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -14,6 +14,18 @@ Tags: SwiftName: PublicKeyRaw Functions: +- Name: secp256k1_ecdsa_recoverable_signature_parse_compact + SwiftName: parseRecoverableECDSASignatureFromCompactBytes(context:outputRecoveredSignature:compactBytes:recoveryID:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sig + Nullability: N + - Position: 2 # input64 + Nullability: N + - Position: 3 # recid + Nullability: N + - Name: secp256k1_ecdsa_recover SwiftName: recoverPublicKeyFromECDSASignature(context:publicKey:signature:hashedMessage:) Parameters: From c4e59bffc9d68323bc87fb128634fa527334ec12 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sun, 1 Feb 2026 17:05:51 +0100 Subject: [PATCH 13/21] Apinotes ornament `secp256k1_ecdsa_recoverable_signature_serialize_compact` -> `serializeRecoverableECDSASignatureCompact(context:outputBytes:recoveryID:recoverableSignature:)` --- .../FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift | 10 +++++----- Sources/secp256k1/include/Secp256k1.apinotes | 12 ++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift index 9ef0e85..7460aeb 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift @@ -44,11 +44,11 @@ extension FFI.ECDSAWithKeyRecovery { try FFI.call( ifFailThrow: .recoverableSignatureSerializeCompact ) { context in - secp256k1_ecdsa_recoverable_signature_serialize_compact( - context, - &rs, - &recoveryID, - &rawSignature + serializeRecoverableECDSASignatureCompact( + context: context, + outputBytes: &rs, + recoveryID: &recoveryID, + recoverableSignature: &rawSignature ) } return (rs, recoveryID) diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index 07e0d1c..b374f97 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -14,6 +14,18 @@ Tags: SwiftName: PublicKeyRaw Functions: +- Name: secp256k1_ecdsa_recoverable_signature_serialize_compact + SwiftName: serializeRecoverableECDSASignatureCompact(context:outputBytes:recoveryID:recoverableSignature:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # output64 + Nullability: N + - Position: 2 # recid + Nullability: N + - Position: 3 # sig + Nullability: N + - Name: secp256k1_ecdsa_recoverable_signature_parse_compact SwiftName: parseRecoverableECDSASignatureFromCompactBytes(context:outputRecoveredSignature:compactBytes:recoveryID:) Parameters: From a507586071f76e9a5fd78e13aa3696a593544d92 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sun, 1 Feb 2026 17:08:27 +0100 Subject: [PATCH 14/21] Apinotes ornament `secp256k1_ecdsa_recoverable_signature_convert` -> `ecdsaRecoverableSignatureToNonRecoverable(context:outputNonRecoverableSignature:recoverableSignature:)` --- .../FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift | 9 +++++---- Sources/secp256k1/include/Secp256k1.apinotes | 10 ++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift index 7460aeb..bb03a20 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift @@ -57,6 +57,7 @@ extension FFI.ECDSAWithKeyRecovery { // MARK: Convert extension FFI.ECDSAWithKeyRecovery { + /// Convert a recoverable signature into a normal signature. static func nonRecoverable( _ wrapped: Wrapped ) throws -> FFI.ECDSA.Wrapped { @@ -66,10 +67,10 @@ extension FFI.ECDSAWithKeyRecovery { try FFI.call( ifFailThrow: .recoverableSignatureConvert ) { context in - secp256k1_ecdsa_recoverable_signature_convert( - context, - &nonRecoverable, - &recoverable + ecdsaRecoverableSignatureToNonRecoverable( + context: context, + outputNonRecoverableSignature: &nonRecoverable, + recoverableSignature: &recoverable ) } diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index b374f97..1ee6f13 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -14,6 +14,16 @@ Tags: SwiftName: PublicKeyRaw Functions: +- Name: secp256k1_ecdsa_recoverable_signature_convert + SwiftName: ecdsaRecoverableSignatureToNonRecoverable(context:outputNonRecoverableSignature:recoverableSignature:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sig + Nullability: N + - Position: 2 # sigin + Nullability: N + - Name: secp256k1_ecdsa_recoverable_signature_serialize_compact SwiftName: serializeRecoverableECDSASignatureCompact(context:outputBytes:recoveryID:recoverableSignature:) Parameters: From 92c9d0332ab0fbd78dffbb82449f7eecc668ffae Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sun, 1 Feb 2026 17:11:17 +0100 Subject: [PATCH 15/21] Apinotes ornament `secp256k1_ecdsa_signature_serialize_compact` -> `serializeRecoverableECDSASignatureCompact(context:outputBytes:signature:)` --- .../API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift | 8 ++++---- Sources/secp256k1/include/Secp256k1.apinotes | 10 ++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift index 93dc113..3bfa15e 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift @@ -29,10 +29,10 @@ extension FFI.ECDSA { var out = [UInt8](repeating: 0, count: Self.byteCount) var rawSignature = wrapped.raw try FFI.call(ifFailThrow: .ecdsaSignatureSerializeCompact) { context in - secp256k1_ecdsa_signature_serialize_compact( - context, - &out, - &rawSignature + serializeRecoverableECDSASignatureCompact( + context: context, + outputBytes: &out, + signature: &rawSignature ) } return Data(out) diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index 1ee6f13..b757291 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -24,6 +24,16 @@ Functions: - Position: 2 # sigin Nullability: N +- Name: secp256k1_ecdsa_signature_serialize_compact + SwiftName: serializeRecoverableECDSASignatureCompact(context:outputBytes:signature:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # output64 + Nullability: N + - Position: 2 # sig + Nullability: N + - Name: secp256k1_ecdsa_recoverable_signature_serialize_compact SwiftName: serializeRecoverableECDSASignatureCompact(context:outputBytes:recoveryID:recoverableSignature:) Parameters: From 9300b195c4db3673f7dc80de9c8dd4ffdf87d7a6 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Sun, 1 Feb 2026 17:14:43 +0100 Subject: [PATCH 16/21] Apinotes ornament `secp256k1_ecdsa_signature_serialize_der` -> `serializeECDSASignatureDER(context:outputBytes:length:signature:)` --- .../ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift | 10 +++++----- Sources/secp256k1/include/Secp256k1.apinotes | 12 ++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift index 3bfa15e..4adeb8b 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift @@ -45,11 +45,11 @@ extension FFI.ECDSA { var derSignature = [UInt8](repeating: 0, count: derMaxLength) var rawSignature = wrapped.raw try FFI.call(ifFailThrow: .ecdsaSignatureSerializeDER) { context in - secp256k1_ecdsa_signature_serialize_der( - context, - &derSignature, - &derMaxLength, - &rawSignature + serializeECDSASignatureDER( + context: context, + outputBytes: &derSignature, + length: &derMaxLength, + signature: &rawSignature ) } return Data(derSignature.prefix(derMaxLength)) diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index b757291..5190715 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -34,6 +34,18 @@ Functions: - Position: 2 # sig Nullability: N +- Name: secp256k1_ecdsa_signature_serialize_der + SwiftName: serializeECDSASignatureDER(context:outputBytes:length:signature:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # output64 + Nullability: N + - Position: 2 # recid + Nullability: N + - Position: 3 # sig + Nullability: N + - Name: secp256k1_ecdsa_recoverable_signature_serialize_compact SwiftName: serializeRecoverableECDSASignatureCompact(context:outputBytes:recoveryID:recoverableSignature:) Parameters: From 9cf0ee72635d04862834e401435539cfe873e212 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Mon, 2 Feb 2026 08:07:57 +0100 Subject: [PATCH 17/21] Apinotes mapping \ * `secp256k1_ec_pubkey_create` -> `createPublicKey(context:outputPublicKey:seckeyBytes:)` \ * `secp256k1_context_destroy` -> `destroyContext` \ * `secp256k1_ec_pubkey_create` -> `createPublicKey(context:outputPublicKey:seckeyBytes:)` \ * `secp256k1_ec_pubkey_parse` -> `parsePublicKey(context:outputPublicKey:inputBytes:inputByteCount:)` \ * `secp256k1_ec_pubkey_serialize` -> `serializePublicKey(context:outputBytes:outputByteCount:publicKey:formatFlags:)` \ * `secp256k1_ec_pubkey_cmp` -> `comparePublicKeys(context:first:second:)` \ * `secp256k1_ec_pubkey_negate` -> `negatePublicKey(context:publicKey:)` \ * `secp256k1_ec_pubkey_combine` -> `combinePublicKeys(context:outputPublicKey:inputs:inputCount:)` \ * `secp256k1_ec_pubkey_sort` -> `sortPublicKeys(context:pubkeys:count:)` \ * `secp256k1_ecdsa_signature_parse_compact` -> `parseEcdsaSignatureCompact(context:outputSignature:inputBytes:)` \ * `secp256k1_ecdsa_signature_parse_der` -> `parseEcdsaSignatureDER(context:outputSignature:input:inputlen:)` \ * `secp256k1_ecdsa_signature_normalize` -> `normalizeEcdsaSignature(context:outputSignature:inputSignature:)` \ * `secp256k1_ecdsa_verify` -> `verifyEcdsaSignature(context:signature:messageHash:publicKey:)` --- .../xcode/xcshareddata/xcschemes/K1.xcscheme | 20 -- Package.resolved | 2 +- Package.swift | 12 - .../ECDSA/ECDSASignatureNonRecoverable.swift | 16 ++ .../NonRecovery/FFI+ECDSA+NonRecovery.swift | 36 ++- .../Keys/PrivateKey/PrivateKey+Wrapped.swift | 8 +- .../API/Keys/PublicKey/FFI+PublicKey.swift | 53 ++-- .../Keys/PublicKey/PublicKey+Wrapped.swift | 11 +- .../K1/Support/FFI/Internals/FFI+Call.swift | 4 +- .../K1/Support/FFI/Internals/FFI+Raw.swift | 35 ++- .../K1/Support/Misc/InlineArray+Data.swift | 26 ++ Sources/K1/Support/Misc/Span+Data.swift | 11 + Sources/secp256k1/include/Secp256k1.apinotes | 244 +++++++++++++++--- Tests/ApinotesTests/ApinotesTests.swift | 134 +++++----- .../TestCases/ECDSA/ECDSASignatureTests.swift | 10 + justfile | 2 +- 16 files changed, 442 insertions(+), 182 deletions(-) create mode 100644 Sources/K1/Support/Misc/InlineArray+Data.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme index a3ccaf4..d7b025a 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/K1.xcscheme @@ -66,26 +66,6 @@ ReferencedContainer = "container:"> - - - - - - - - ` + /// + /// [rfc]: https://tools.ietf.org/html/rfc4754 + @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) + public init(rawRepresentation array64: InlineArray<64, UInt8>) throws { + try self.init( + wrapped: FFI.ECDSA.from(compactBytes: array64) + ) + } } // MARK: ContiguousBytes diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift index 4adeb8b..79ddb68 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift @@ -14,8 +14,26 @@ extension FFI.ECDSA { ) } + /// Compact aka `IEEE P1363` aka `R||S`. + @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) + static func from( + compactBytes array64: InlineArray<64, UInt8> + ) throws -> Wrapped { + try Wrapped( + raw: Raw.nonRecoverableSignature(compactBytes: array64) + ) + } + static func from( derRepresentation: [UInt8] + ) throws -> Wrapped { + try withSpanFromArray(derRepresentation) { span in + try Self.from(derRepresentation: span) + } + } + + static func from( + derRepresentation: Span ) throws -> Wrapped { try Wrapped( raw: Raw.nonRecoverableSignature(derBytes: derRepresentation) @@ -29,7 +47,7 @@ extension FFI.ECDSA { var out = [UInt8](repeating: 0, count: Self.byteCount) var rawSignature = wrapped.raw try FFI.call(ifFailThrow: .ecdsaSignatureSerializeCompact) { context in - serializeRecoverableECDSASignatureCompact( + serializeEcdsaSignatureCompact( context: context, outputBytes: &out, signature: &rawSignature @@ -45,10 +63,10 @@ extension FFI.ECDSA { var derSignature = [UInt8](repeating: 0, count: derMaxLength) var rawSignature = wrapped.raw try FFI.call(ifFailThrow: .ecdsaSignatureSerializeDER) { context in - serializeECDSASignatureDER( + serializeEcdsaSignatureDER( context: context, outputBytes: &derSignature, - length: &derMaxLength, + outputByteCount: &derMaxLength, signature: &rawSignature ) } @@ -120,15 +138,15 @@ extension FFI.ECDSA { let codeForSignatureWasMalleable = 1 let signatureWasMalleableResult = ffi.callWithResultCode { context in - secp256k1_ecdsa_signature_normalize(context, &normalized, &maybeMalleable) + normalizeEcdsaSignature(context: context, outputSignature: &normalized, inputSignature: &maybeMalleable) } let signatureWasMalleable = signatureWasMalleableResult == codeForSignatureWasMalleable let isSignatureValid = ffi.validate { context in - secp256k1_ecdsa_verify( - context, - &normalized, - message, - &publicKeyRaw + verifyEcdsaSignature( + context: context, + signature: &normalized, + messageHash: message, + publicKey: &publicKeyRaw ) } let acceptMalleableSignatures = options.malleabilityStrictness == .accepted diff --git a/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift index 629124f..84004a6 100644 --- a/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift @@ -24,10 +24,10 @@ extension FFI { var raw = PublicKeyRaw() try FFI.call(ifFailThrow: .publicKeyCreate) { context in - secp256k1_ec_pubkey_create( - context, - &raw, - seckey.baseAddress! + createPublicKey( + context: context, + outputPublicKey: &raw, + seckeyBytes: seckey.baseAddress! ) } diff --git a/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift b/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift index a1a50b5..f7db5dd 100644 --- a/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift @@ -16,12 +16,12 @@ extension FFI.PublicKey { static func deserialize( x963Representation contiguousBytes: some ContiguousBytes ) throws -> Wrapped { - try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in + try withSpanFromContiguousBytes(contiguousBytes) { span in let expected = Self.x963ByteCount - guard bufferPointer.count == expected else { + guard span.count == expected else { throw K1.Error.incorrectKeySize } - return try Self._deserialize(bytes: [UInt8](bufferPointer)) + return try Self._deserialize(span: span) } } @@ -29,12 +29,20 @@ extension FFI.PublicKey { static func deserialize( rawRepresentation contiguousBytes: some ContiguousBytes ) throws -> Wrapped { - try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in + try withSpanFromContiguousBytes(contiguousBytes) { span in let expected = Self.rawByteCount - guard bufferPointer.count == expected else { + guard span.count == expected else { throw K1.Error.incorrectKeySize } - return try Self.deserialize(x963Representation: [0x04] + [UInt8](bufferPointer)) + // Prepend 0x04 prefix into a small temporary buffer + var prefixed = [UInt8](repeating: 0, count: expected + 1) + prefixed[0] = 0x04 + for index in 0 ..< span.count { + prefixed[index + 1] = span[index] + } + return try prefixed.withUnsafeBufferPointer { buf in + try Self._deserialize(span: Span(_unsafeElements: buf)) + } } } @@ -42,26 +50,27 @@ extension FFI.PublicKey { static func deserialize( compressedRepresentation contiguousBytes: some ContiguousBytes ) throws -> Wrapped { - try contiguousBytes.withUnsafeBytes { bufferPointer throws -> Wrapped in + try withSpanFromContiguousBytes(contiguousBytes) { span in let expected = Self.compressedByteCount - guard bufferPointer.count == expected else { + guard span.count == expected else { throw K1.Error.incorrectKeySize } - return try Self._deserialize(bytes: [UInt8](bufferPointer)) + return try Self._deserialize(span: span) } } - private static func _deserialize(bytes: [UInt8]) throws -> Wrapped { + static func deserialize( + compressedRepresentation span: Span + ) throws -> Wrapped { + try _deserialize(span: span) + } + + private static func _deserialize(span: Span) throws -> Wrapped { var raw = PublicKeyRaw() try FFI.call( ifFailThrow: .publicKeyParse ) { context in - secp256k1_ec_pubkey_parse( - context, - &raw, - bytes, - bytes.count - ) + parsePublicKey(context: context, outputPublicKey: &raw, inputBytes: span) } return .init(raw: raw) } @@ -77,12 +86,12 @@ extension FFI.PublicKey { var out = [UInt8](repeating: 0x00, count: byteCount) var publicKeyRaw = wrapped.raw try FFI.call(ifFailThrow: .publicKeySerialize) { context in - secp256k1_ec_pubkey_serialize( - context, - &out, - &byteCount, - &publicKeyRaw, - format.rawValue + serializePublicKey( + context: context, + outputBytes: &out, + outputByteCount: &byteCount, + publicKey: &publicKeyRaw, + formatFlags: format.rawValue ) } return Data(out.prefix(byteCount)) diff --git a/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift index 0d9f6e4..06bcb47 100644 --- a/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift @@ -35,7 +35,7 @@ extension FFI.PublicKey.Wrapped { var otherRaw = other.raw return try FFI.toC { ffi in ffi.callWithResultCode { context in - secp256k1_ec_pubkey_cmp(context, &selfRaw, &otherRaw) + comparePublicKeys(context: context, first: &selfRaw, second: &otherRaw) } == 0 } } @@ -58,7 +58,7 @@ extension FFI.PublicKey.Wrapped { var result = self.raw try FFI.toC { ffi in try ffi.call(ifFailThrow: .publicKeyCreate) { context in - secp256k1_ec_pubkey_negate(context, &result) + negatePublicKey(context: context, publicKey: &result) } } return Self(raw: result) @@ -84,7 +84,12 @@ extension FFI.PublicKey.Wrapped { try keyPointers.withUnsafeBufferPointer { pointers in try FFI.toC { ffi in try ffi.call(ifFailThrow: .groupOperation) { context in - secp256k1_ec_pubkey_combine(context, &result, pointers.baseAddress!, keys.count) + combinePublicKeys( + context: context, + outputPublicKey: &result, + inputs: pointers.baseAddress!, + inputCount: keys.count + ) } } } diff --git a/Sources/K1/Support/FFI/Internals/FFI+Call.swift b/Sources/K1/Support/FFI/Internals/FFI+Call.swift index e76ac5d..8b6dedb 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Call.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Call.swift @@ -7,7 +7,7 @@ final class FFI { init() throws { guard /* "Create a secp256k1 context object." */ - let context = secp256k1_context_create(Context.sign.rawValue | Context.verify.rawValue) + let context = createContext(flags: Context.sign.rawValue | Context.verify.rawValue) else { throw K1.Error.underlyingLibsecp256k1Error(.failedToCreateContextForSecp256k1) } @@ -16,7 +16,7 @@ final class FFI { } deinit { - secp256k1_context_destroy(context) + destroyContext(context) } } diff --git a/Sources/K1/Support/FFI/Internals/FFI+Raw.swift b/Sources/K1/Support/FFI/Internals/FFI+Raw.swift index 2cfcc47..4f8bb9b 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Raw.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Raw.swift @@ -23,16 +23,34 @@ extension Raw { return raw } + @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) + static func nonRecoverableSignature( + compactBytes array: InlineArray<64, UInt8> + ) throws -> ECDSASignatureRaw { + var raw = ECDSASignatureRaw() + // FIXME: Declare a `parseEcdsaSignatureCompact` taking an InlineArray<64, UInt8> and use it directly + try array.span.withUnsafeBufferPointer { compactBytes in + try FFI.call(ifFailThrow: .ecdsaSignatureParseCompact) { context in + parseEcdsaSignatureCompact( + context: context, + outputSignature: &raw, + inputBytes: compactBytes + ) + } + } + return raw + } + static func nonRecoverableSignature( compactBytes: [UInt8] ) throws -> ECDSASignatureRaw { var raw = ECDSASignatureRaw() try FFI.call(ifFailThrow: .ecdsaSignatureParseCompact) { context in - secp256k1_ecdsa_signature_parse_compact( - context, - &raw, - compactBytes + parseEcdsaSignatureCompact( + context: context, + outputSignature: &raw, + inputBytes: compactBytes ) } @@ -40,17 +58,12 @@ extension Raw { } static func nonRecoverableSignature( - derBytes: [UInt8] + derBytes: Span ) throws -> ECDSASignatureRaw { var raw = ECDSASignatureRaw() try FFI.call(ifFailThrow: .ecdsaSignatureParseDER) { context in - secp256k1_ecdsa_signature_parse_der( - context, - &raw, - derBytes, - derBytes.count - ) + parseEcdsaSignatureDER(context: context, outputSignature: &raw, input: derBytes) } return raw diff --git a/Sources/K1/Support/Misc/InlineArray+Data.swift b/Sources/K1/Support/Misc/InlineArray+Data.swift new file mode 100644 index 0000000..b3d0f48 --- /dev/null +++ b/Sources/K1/Support/Misc/InlineArray+Data.swift @@ -0,0 +1,26 @@ +import Foundation + +@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) +extension InlineArray where Element == UInt8 { + init(hex: String) throws { + let data = try Data(hex: hex) + try self.init(data: data) + } + + init(data: Data) throws { + guard data.count == count else { + throw InlineArrayError.wrongLength(expected: count, got: data.count) + } + self = .init(repeating: 0) + data.withUnsafeBytes { (raw: UnsafeRawBufferPointer) in + for index in 0 ..< count { + self[index] = raw[index] + } + } + } +} + +@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) +enum InlineArrayError: Error { + case wrongLength(expected: Int, got: Int) +} diff --git a/Sources/K1/Support/Misc/Span+Data.swift b/Sources/K1/Support/Misc/Span+Data.swift index e4d9765..611d481 100644 --- a/Sources/K1/Support/Misc/Span+Data.swift +++ b/Sources/K1/Support/Misc/Span+Data.swift @@ -31,3 +31,14 @@ func withSpanFromData( return try body(Span(_unsafeElements: readonly)) } } + +@usableFromInline +func withSpanFromContiguousBytes( + _ bytes: C, + _ body: (Span) throws -> R +) rethrows -> R { + try bytes.withUnsafeBytes { raw in + let buf = raw.bindMemory(to: UInt8.self) + return try body(Span(_unsafeElements: buf)) + } +} diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index 5190715..0de9631 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -24,34 +24,15 @@ Functions: - Position: 2 # sigin Nullability: N -- Name: secp256k1_ecdsa_signature_serialize_compact - SwiftName: serializeRecoverableECDSASignatureCompact(context:outputBytes:signature:) - Parameters: - - Position: 0 # ctx - Nullability: N - - Position: 1 # output64 - Nullability: N - - Position: 2 # sig - Nullability: N - -- Name: secp256k1_ecdsa_signature_serialize_der - SwiftName: serializeECDSASignatureDER(context:outputBytes:length:signature:) - Parameters: - - Position: 0 # ctx - Nullability: N - - Position: 1 # output64 - Nullability: N - - Position: 2 # recid - Nullability: N - - Position: 3 # sig - Nullability: N - - Name: secp256k1_ecdsa_recoverable_signature_serialize_compact SwiftName: serializeRecoverableECDSASignatureCompact(context:outputBytes:recoveryID:recoverableSignature:) Parameters: - Position: 0 # ctx Nullability: N - Position: 1 # output64 + BoundsSafety: + Kind: counted_by + BoundedBy: "64" Nullability: N - Position: 2 # recid Nullability: N @@ -66,6 +47,9 @@ Functions: - Position: 1 # sig Nullability: N - Position: 2 # input64 + BoundsSafety: + Kind: counted_by + BoundedBy: "64" Nullability: N - Position: 3 # recid Nullability: N @@ -80,6 +64,9 @@ Functions: - Position: 2 # sig Nullability: N - Position: 3 # msghash32 + BoundsSafety: + Kind: counted_by + BoundedBy: "32" Nullability: N - Name: secp256k1_ecdsa_sign @@ -90,8 +77,14 @@ Functions: - Position: 1 # sig / outputSignature Nullability: N - Position: 2 # msghash32 + BoundsSafety: + Kind: counted_by + BoundedBy: "32" Nullability: N - Position: 3 # seckey + BoundsSafety: + Kind: counted_by + BoundedBy: "32" Nullability: N - Position: 4 # noncefp Nullability: O @@ -106,8 +99,14 @@ Functions: - Position: 1 # sig / outputSignature Nullability: N - Position: 2 # msghash32 + BoundsSafety: + Kind: counted_by + BoundedBy: "32" Nullability: N - Position: 3 # seckey + BoundsSafety: + Kind: counted_by + BoundedBy: "32" Nullability: N - Position: 4 # noncefp Nullability: O @@ -120,12 +119,21 @@ Functions: - Position: 0 # ctx (secp256k1_context) Nullability: N - Position: 1 # sig64 + BoundsSafety: + Kind: counted_by + BoundedBy: "64" Nullability: N - Position: 2 # msg32 + BoundsSafety: + Kind: counted_by + BoundedBy: "32" Nullability: N - Position: 3 # keypair Nullability: N - Position: 4 # aux_rand32 + BoundsSafety: + Kind: counted_by + BoundedBy: "32" Nullability: O - Name: secp256k1_keypair_create @@ -136,6 +144,9 @@ Functions: - Position: 1 # keypair output (secp256k1_keypair) Nullability: N - Position: 2 # seckey + BoundsSafety: + Kind: counted_by + BoundedBy: "32" Nullability: N - Name: secp256k1_schnorrsig_verify @@ -178,20 +189,15 @@ Functions: - Position: 2 # pubkey Nullability: N - Position: 3 # seckey + BoundsSafety: + Kind: counted_by + BoundedBy: "32" Nullability: N - Position: 4 # hashfp Nullability: O - Position: 5 # data Nullability: O -- Name: ecdh_hash_function_asn1_x963_apinotes_test - SwiftName: ecdhHashASN1x963(output:x:) - Parameters: - - Position: 0 # output - Nullability: N - - Position: 1 # x32 - Nullability: N - - Name: fill_with_fives SwiftName: fillWithFives(span:) Parameters: @@ -211,3 +217,181 @@ Functions: Nullability: N - Position: 1 # source Nullability: N + +# Context management +- Name: secp256k1_context_create + SwiftName: createContext(flags:) + Parameters: + - Position: 0 # flags + Nullability: N + +- Name: secp256k1_context_destroy + SwiftName: destroyContext(_:) + Parameters: + - Position: 0 # ctx + Nullability: N + +# Secret keys and pubkeys +- Name: secp256k1_ec_pubkey_create + SwiftName: createPublicKey(context:outputPublicKey:seckeyBytes:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # pubkey + Nullability: N + - Position: 2 # seckey + BoundsSafety: + Kind: counted_by + BoundedBy: "32" + Nullability: N + +- Name: secp256k1_ec_pubkey_parse + SwiftName: parsePublicKey(context:outputPublicKey:inputBytes:inputlen:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # pubkey + Nullability: N + - Position: 2 # input + Lifetimebound: true + BoundsSafety: + Kind: counted_by + BoundedBy: "inputlen" + Nullability: N + - Position: 3 # inputlen + Nullability: N + +- Name: secp256k1_ec_pubkey_serialize + SwiftName: serializePublicKey(context:outputBytes:outputByteCount:publicKey:formatFlags:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # output + Nullability: N + - Position: 2 # outputlen + Nullability: N + - Position: 3 # pubkey + Nullability: N + - Position: 4 # flags + Nullability: N + +- Name: secp256k1_ec_pubkey_cmp + SwiftName: comparePublicKeys(context:first:second:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # pubkey1 + Nullability: N + - Position: 2 # pubkey2 + Nullability: N + +- Name: secp256k1_ec_pubkey_negate + SwiftName: negatePublicKey(context:publicKey:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # pubkey + Nullability: N + +- Name: secp256k1_ec_pubkey_combine + SwiftName: combinePublicKeys(context:outputPublicKey:inputs:inputCount:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # out + Nullability: N + - Position: 2 # ins + Nullability: N + - Position: 3 # n + Nullability: N + +- Name: secp256k1_ec_pubkey_sort + SwiftName: sortPublicKeys(context:pubkeys:count:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # pubkeys + Nullability: N + - Position: 2 # len + Nullability: N + +# ECDSA signature parsing/serialization +- Name: secp256k1_ecdsa_signature_parse_compact + SwiftName: parseEcdsaSignatureCompact(context:outputSignature:inputBytes:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sig + Nullability: N + - Position: 2 # input64 + BoundsSafety: + Kind: counted_by + BoundedBy: "64" + Nullability: N + +- Name: secp256k1_ecdsa_signature_parse_der + SwiftName: parseEcdsaSignatureDER(context:outputSignature:input:inputlen:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sig + Nullability: N + - Position: 2 # input + Lifetimebound: true + BoundsSafety: + Kind: counted_by + BoundedBy: "inputlen" + Nullability: N + - Position: 3 # inputlen + Nullability: N + +- Name: secp256k1_ecdsa_signature_serialize_der + SwiftName: serializeEcdsaSignatureDER(context:outputBytes:outputByteCount:signature:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # output + Nullability: N + - Position: 2 # outputlen + Nullability: N + - Position: 3 # sig + Nullability: N + +- Name: secp256k1_ecdsa_signature_serialize_compact + SwiftName: serializeEcdsaSignatureCompact(context:outputBytes:signature:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # output64 + BoundsSafety: + Kind: counted_by + BoundedBy: "64" + Nullability: N + - Position: 2 # sig + Nullability: N + +# ECDSA verify/normalize +- Name: secp256k1_ecdsa_signature_normalize + SwiftName: normalizeEcdsaSignature(context:outputSignature:inputSignature:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sigout + Nullability: N + - Position: 2 # sigin + Nullability: N + +- Name: secp256k1_ecdsa_verify + SwiftName: verifyEcdsaSignature(context:signature:messageHash:publicKey:) + Parameters: + - Position: 0 # ctx + Nullability: N + - Position: 1 # sig + Nullability: N + - Position: 2 # msg32 + BoundsSafety: + Kind: counted_by + BoundedBy: "32" + Nullability: N + - Position: 3 # pubkey + Nullability: N diff --git a/Tests/ApinotesTests/ApinotesTests.swift b/Tests/ApinotesTests/ApinotesTests.swift index d98f3e5..dbcfc2e 100644 --- a/Tests/ApinotesTests/ApinotesTests.swift +++ b/Tests/ApinotesTests/ApinotesTests.swift @@ -1,73 +1,73 @@ import Testing -import Secp256k1 -import Foundation -import K1Macros +//import Secp256k1 +//import Foundation +//import K1Macros -struct Safe {} -extension Safe { - #makeSafe("cloneSpanOfLength3", cloneSpanOfLength3Unsafe, 3) -} +//struct Safe {} +//extension Safe { +// #makeSafe("cloneSpanOfLength3", cloneSpanOfLength3Unsafe, 3) +//} -@Suite("Apinotes Tests") -struct Test { - @available(macOS 26.0, *) - @Test - func `C get Swiftified by apinotes file with non-nullability and MutableSpan`() { +//@Suite("Apinotes Tests") +//struct Test { +// @Test +// func testCase() { +// +//// expectAllEqual(to: 5) { (bytes: inout InlineArray<3, UInt8>) in +//// var span = bytes.mutableSpan +//// fillWithFives(span: &span) +//// } +// #expect(1 == 5 - 4) +// } +// +// @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) +// @Test +// func `C fixed length`() { +// let source: InlineArray<3, UInt8> = [7, 7, 7] +// var destination: InlineArray<3, UInt8> = [0, 0, 0] +// +// Safe().cloneSpanOfLength3( +// into: &destination, +// from: source +// ) +// +// for index in 0..) in - var span = bytes.mutableSpan - fillWithFives(span: &span) - } - } - @available(macOS 26.0, *) - @Test - func `C fixed length`() { - let source: InlineArray<3, UInt8> = [7, 7, 7] - var destination: InlineArray<3, UInt8> = [0, 0, 0] - - Safe().cloneSpanOfLength3( - into: &destination, - from: source - ) - - for index in 0..( - to expectedElement: Element, - defaultElement: Element, - initBytes: (inout InlineArray) -> Void -) -> Bool { - var bytes: InlineArray = .init(repeating: defaultElement) - initBytes(&bytes) - for index in 0..( - to expectedElement: Element, - initBytes: @escaping (inout InlineArray) -> Void, - file: StaticString = #filePath, - line: UInt = #line -) { - #expect( - allEqual( - to: expectedElement, - defaultElement: .zero, - initBytes: initBytes - ), - "in file: \(file), line: \(line)" - ) -} +//@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) +//func allEqual( +// to expectedElement: Element, +// defaultElement: Element, +// initBytes: (inout InlineArray) -> Void +//) -> Bool { +// var bytes: InlineArray = .init(repeating: defaultElement) +// initBytes(&bytes) +// for index in 0..( +// to expectedElement: Element, +// initBytes: @escaping (inout InlineArray) -> Void, +// file: StaticString = #filePath, +// line: UInt = #line +//) { +// #expect( +// allEqual( +// to: expectedElement, +// defaultElement: .zero, +// initBytes: initBytes +// ), +// "in file: \(file), line: \(line)" +// ) +//} diff --git a/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift b/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift index 23ea4ee..24775b1 100644 --- a/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift +++ b/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTests.swift @@ -35,4 +35,14 @@ final class ECDSASignatureTests: XCTestCase { signatures.insert(signature) } } + + @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) + func test_ecdsa_from_inline_array() throws { + let data = try Data(hex: "74b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8") + let inlineArray = try InlineArray<64, UInt8>(data: data) + let signature = try K1.ECDSA.Signature(rawRepresentation: inlineArray) + XCTAssertEqual(signature.rawRepresentation, data) + + } + } diff --git a/justfile b/justfile index 4e8f2f9..3012035 100644 --- a/justfile +++ b/justfile @@ -43,7 +43,7 @@ gyb: ./scripts/generate_boilerplate_files_with_gyb.sh bump-dep dryRun="false": - swift run --enable-experimental-prebuilts \ + swift run \ --package-path scripts/update-libsecp \ update-libsecp \ {{ if dryRun == "true" { "--dry-run" } else { "" } }} From d5266fa8ff9d26cd7638b866a5263d35f823507f Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Mon, 2 Feb 2026 08:14:24 +0100 Subject: [PATCH 18/21] Apinotes rename `secp256k1_xonly_pubkey` -> `PublicKeyXOnlyRaw` --- .../FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift | 2 +- Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift | 2 +- Sources/K1/Support/FFI/Internals/FFI+Format.swift | 2 +- Sources/K1/Support/Misc/K1+Error.swift | 6 +++--- Sources/secp256k1/include/Secp256k1.apinotes | 5 ++++- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift index 84004a6..6485bce 100644 --- a/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift @@ -27,7 +27,7 @@ extension FFI { createPublicKey( context: context, outputPublicKey: &raw, - seckeyBytes: seckey.baseAddress! + privateKeyBytes: seckey.baseAddress! ) } diff --git a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift index 6c0e0f5..0384dc7 100644 --- a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift +++ b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift @@ -14,7 +14,7 @@ extension FFI.Schnorr { message: Span ) throws -> Bool { try FFI.toC { ffi -> Bool in - var publicKeyX = secp256k1_xonly_pubkey() + var publicKeyX = PublicKeyXOnlyRaw() var publicKeyRaw = publicKey.raw try FFI.call(ifFailThrow: .xonlyPublicKeyFromPublicKey) { context in xOnlyPublicKeyFromPublicKey( diff --git a/Sources/K1/Support/FFI/Internals/FFI+Format.swift b/Sources/K1/Support/FFI/Internals/FFI+Format.swift index 23fdca3..5e3be3f 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Format.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Format.swift @@ -18,7 +18,7 @@ extension K1.Format { // MARK: - K1.Format extension K1 { - /// Bridging type for: `secp256k1_ec_pubkey_serialize` + /// Bridging type for: `serializePublicKey(context:outputBytes:outputByteCount:publicKey:formatFlags:)` (`secp256k1_ec_pubkey_serialize`) enum Format: UInt32, CaseIterable { case compressed, uncompressed } diff --git a/Sources/K1/Support/Misc/K1+Error.swift b/Sources/K1/Support/Misc/K1+Error.swift index 289d9e5..9ea19a2 100644 --- a/Sources/K1/Support/Misc/K1+Error.swift +++ b/Sources/K1/Support/Misc/K1+Error.swift @@ -81,13 +81,13 @@ extension FFI { /// `secp256k1_ecdsa_sign_recoverable` or `secp256k1_ecdsa_sign` failed case ecdsaSign - /// `secp256k1_xonly_pubkey_from_pubkey` failed + /// `xOnlyPublicKeyFromPublicKey(context:outputXOnlyPublicKey:parity:publicKey:)` (`secp256k1_xonly_pubkey_from_pubkey`) failed case xonlyPublicKeyFromPublicKey - /// `keypairFromPrivateKey` (`secp256k1_keypair_create`) failed + /// `keypairFromPrivateKey(context:outputKeyPair:privateKeyBytes:)` (`secp256k1_keypair_create`) failed case keypairCreate - /// `secp256k1_schnorrsig_sign32` failed + /// `schnorrSign(context:outputSignatureBytes:message:keypair:auxiliaryRandomData:)` (`secp256k1_schnorrsig_sign32`) failed case schnorrSign /// `secp256k1_ec_pubkey_create` diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index 0de9631..fca29d2 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -13,6 +13,9 @@ Tags: - Name: secp256k1_pubkey SwiftName: PublicKeyRaw +- Name: secp256k1_xonly_pubkey + SwiftName: PublicKeyXOnlyRaw + Functions: - Name: secp256k1_ecdsa_recoverable_signature_convert SwiftName: ecdsaRecoverableSignatureToNonRecoverable(context:outputNonRecoverableSignature:recoverableSignature:) @@ -233,7 +236,7 @@ Functions: # Secret keys and pubkeys - Name: secp256k1_ec_pubkey_create - SwiftName: createPublicKey(context:outputPublicKey:seckeyBytes:) + SwiftName: createPublicKey(context:outputPublicKey:privateKeyBytes:) Parameters: - Position: 0 # ctx Nullability: N From 63bedc29c6648969ec06f45980c47e52be667dd8 Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Mon, 2 Feb 2026 09:54:46 +0100 Subject: [PATCH 19/21] Improved error doc --- Sources/K1/Support/Misc/K1+Error.swift | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Sources/K1/Support/Misc/K1+Error.swift b/Sources/K1/Support/Misc/K1+Error.swift index 9ea19a2..bb75bcf 100644 --- a/Sources/K1/Support/Misc/K1+Error.swift +++ b/Sources/K1/Support/Misc/K1+Error.swift @@ -45,40 +45,40 @@ extension FFI { enum Error: Int, Sendable, Swift.Error, Hashable { case failedToCreateContextForSecp256k1 - //// `secp256k1_ecdsa_signature_parse_compact` failed + /// `parseEcdsaSignatureCompact(context:outputSignature:inputBytes:)` (`secp256k1_ecdsa_signature_parse_compact`) failed case ecdsaSignatureParseCompact - /// `secp256k1_ecdsa_signature_parse_der` failed + /// `parseEcdsaSignatureDER(context:outputSignature:input:inputlen:)` (`secp256k1_ecdsa_signature_parse_der`) failed case ecdsaSignatureParseDER - /// `secp256k1_ecdsa_signature_serialize_compact` failed + /// `serializeEcdsaSignatureCompact(context:outputBytes:signature:)` (`secp256k1_ecdsa_signature_serialize_compact`) failed case ecdsaSignatureSerializeCompact - /// `secp256k1_ecdsa_signature_serialize_der` failed + /// `serializeEcdsaSignatureDER(context:outputBytes:outputByteCount:signature:)` (`secp256k1_ecdsa_signature_serialize_der`) failed case ecdsaSignatureSerializeDER - /// `secp256k1_ecdsa_recoverable_signature_parse_compact` failed + /// `parseRecoverableECDSASignatureFromCompactBytes(context:outputRecoveredSignature:compactBytes:recoveryID:)` (`secp256k1_ecdsa_recoverable_signature_parse_compact`) failed case recoverableSignatureParseCompact - /// `secp256k1_ecdsa_recoverable_signature_serialize_compact` failed + /// `serializeRecoverableECDSASignatureCompact(context:outputBytes:recoveryID:recoverableSignature:)` (`secp256k1_ecdsa_recoverable_signature_serialize_compact`) failed case recoverableSignatureSerializeCompact - /// `secp256k1_ecdsa_recoverable_signature_convert` failed + /// `ecdsaRecoverableSignatureToNonRecoverable(context:outputNonRecoverableSignature:recoverableSignature:)` (`secp256k1_ecdsa_recoverable_signature_convert`) failed case recoverableSignatureConvert - /// `secp256k1_ecdsa_recover` failed + /// `recoverPublicKeyFromECDSASignature(context:publicKey:signature:hashedMessage:)` (`secp256k1_ecdsa_recover`) failed case recover - /// `secp256k1_ec_pubkey_parse` failed + /// `parsePublicKey(context:outputPublicKey:inputBytes:inputlen:)` (`secp256k1_ec_pubkey_parse`) failed case publicKeyParse - /// `secp256k1_ec_pubkey_serialize` failed + /// `serializePublicKey(context:outputBytes:outputByteCount:publicKey:formatFlags:)` (`secp256k1_ec_pubkey_serialize`) failed case publicKeySerialize - /// `secp256k1_ecdh` failed + /// `ecdh(context:outputSharedPointBytes:publicKey:privateKeyBytes:hashFunction:arbitraryData:)` (`secp256k1_ecdh`) failed case ecdh - /// `secp256k1_ecdsa_sign_recoverable` or `secp256k1_ecdsa_sign` failed + /// `ecdsaSignRecoverable(context:outputSignature:hashedMessageBytes:privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:)` (`secp256k1_ecdsa_sign_recoverable`) or `ecdsaSignNonRecoverable(context:outputSignature:hashedMessageBytes:privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:)` (`secp256k1_ecdsa_sign`) failed case ecdsaSign /// `xOnlyPublicKeyFromPublicKey(context:outputXOnlyPublicKey:parity:publicKey:)` (`secp256k1_xonly_pubkey_from_pubkey`) failed @@ -90,7 +90,7 @@ extension FFI { /// `schnorrSign(context:outputSignatureBytes:message:keypair:auxiliaryRandomData:)` (`secp256k1_schnorrsig_sign32`) failed case schnorrSign - /// `secp256k1_ec_pubkey_create` + /// `createPublicKey(context:outputPublicKey:privateKeyBytes:)` (`secp256k1_ec_pubkey_create`) case publicKeyCreate /// Group operation (point addition, subtraction, etc.) failed From 6171381640475f6e0fca8ac44e22a3471c920d0b Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Tue, 3 Feb 2026 14:13:47 +0100 Subject: [PATCH 20/21] Made `FFI` init non throwing -> many methods in K1 are no longer throwing. Mapping returned value of many secp256k1 functions to `ResultRaw` enum using apinotes. Verify signature functions return `VerifySignatureOutcomeRaw` using apinotes. Normalize signature function return `NormalizeSignatureOutcomeRaw` using apinotes. --- .../K1/ECDSA/ECDSASignatureRecoverable.swift | 8 +- Sources/K1/K1/Keys/Keys.generated.swift | 36 ++++-- Sources/K1/K1/Keys/Keys.swift.gyb | 10 +- .../K1/K1/Keys/PublicKeyImplementation.swift | 16 +-- .../K1/Validation/Validation.generated.swift | 78 +++++------- Sources/K1/K1/Validation/Validation.swift.gyb | 60 ++++----- .../{ => Foundation}/Data+Extensions.swift | 0 .../ComparisonOutcomeRaw+Extension.swift | 23 ++++ ...rmalizeSignatureOutcomeRaw+Extension.swift | 6 + .../Secp256k1/ResultRaw+Extension.swift | 9 ++ .../VerifySignatureOutcomeRaw+Extension.swift | 9 ++ .../NonRecovery/FFI+ECDSA+NonRecovery.swift | 18 +-- .../Recovery/ECDSA+Recovery+Wrapped.swift | 2 +- .../ECDSA/Recovery/FFI+ECDSA+Recovery.swift | 34 ++--- .../Keys/PrivateKey/PrivateKey+Wrapped.swift | 1 - .../API/Keys/PublicKey/FFI+PublicKey.swift | 4 +- .../Keys/PublicKey/PublicKey+Wrapped.swift | 57 +++++---- .../Support/FFI/API/Schnorr/FFI+Schnorr.swift | 12 +- .../K1/Support/FFI/Internals/FFI+Call.swift | 120 +++++++++++++----- Sources/K1/Support/Misc/K1+Error.swift | 4 - Sources/secp256k1/include/Secp256k1.apinotes | 58 +++++---- Sources/secp256k1/include/k1_umbrella.h | 2 + Sources/secp256k1/src/ecdh_variants.h | 7 - Sources/secp256k1/src/k1_shim.c | 15 +++ Sources/secp256k1/src/k1_shim.h | 40 ++++++ .../ECDSA/ECDSASignatureTrezorTests.swift | 2 +- .../PublicKeyGroupOperationsTests.swift | 75 ++++++----- .../ECDASignaturePublicKeyRecoveryTests.swift | 2 +- justfile | 3 +- 29 files changed, 424 insertions(+), 287 deletions(-) rename Sources/K1/Support/Extensions/{ => Foundation}/Data+Extensions.swift (100%) create mode 100644 Sources/K1/Support/Extensions/Secp256k1/ComparisonOutcomeRaw+Extension.swift create mode 100644 Sources/K1/Support/Extensions/Secp256k1/NormalizeSignatureOutcomeRaw+Extension.swift create mode 100644 Sources/K1/Support/Extensions/Secp256k1/ResultRaw+Extension.swift create mode 100644 Sources/K1/Support/Extensions/Secp256k1/VerifySignatureOutcomeRaw+Extension.swift create mode 100644 Sources/secp256k1/src/k1_shim.c create mode 100644 Sources/secp256k1/src/k1_shim.h diff --git a/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift b/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift index 59f0632..6c6a0f3 100644 --- a/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift +++ b/Sources/K1/K1/ECDSA/ECDSASignatureRecoverable.swift @@ -64,10 +64,10 @@ extension K1.ECDSAWithKeyRecovery.Signature { /// Compact aka `IEEE P1363` aka `R||S` and `V` (`RecoveryID`). public func compact() throws -> Compact { // swiftlint:disable:next identifier_name - let (rs, recid) = try FFI.ECDSAWithKeyRecovery.serializeCompact( + let (rs, recid) = FFI.ECDSAWithKeyRecovery.serializeCompact( wrapped ) - return try .init( + return try Compact( compact: Data(rs), recoveryID: .init(recid: recid) ) @@ -199,8 +199,8 @@ extension K1.ECDSAWithKeyRecovery.Signature { // MARK: Conversion extension K1.ECDSAWithKeyRecovery.Signature { /// Converts this recoverable ECDSA signature to a non-recoverable version. - public func nonRecoverable() throws -> K1.ECDSA.Signature { - try K1.ECDSA.Signature( + public func nonRecoverable() -> K1.ECDSA.Signature { + K1.ECDSA.Signature( wrapped: FFI.ECDSAWithKeyRecovery.nonRecoverable(self.wrapped) ) } diff --git a/Sources/K1/K1/Keys/Keys.generated.swift b/Sources/K1/K1/Keys/Keys.generated.swift index d5c246f..e9b04c1 100644 --- a/Sources/K1/K1/Keys/Keys.generated.swift +++ b/Sources/K1/K1/Keys/Keys.generated.swift @@ -17,13 +17,16 @@ extension K1.KeyAgreement { let publicKeyImpl: K1._PublicKeyImplementation /// The corresponding public key. - public var publicKey: PublicKey { - try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) - } + public let publicKey: PublicKey init(impl: Impl) { self.impl = impl self.publicKeyImpl = impl.publicKey + do { + self.publicKey = try PublicKey(rawRepresentation: impl.publicKey.rawRepresentation) + } catch { + fatalError("Should never fail to instantiate a PublicKey from a PrivateKey, error: \(error)") + } } /// Creates a random `secp256k1` private key for key agreement. @@ -239,13 +242,16 @@ extension K1.Schnorr { let publicKeyImpl: K1._PublicKeyImplementation /// The corresponding public key. - public var publicKey: PublicKey { - try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) - } + public let publicKey: PublicKey init(impl: Impl) { self.impl = impl self.publicKeyImpl = impl.publicKey + do { + self.publicKey = try PublicKey(rawRepresentation: impl.publicKey.rawRepresentation) + } catch { + fatalError("Should never fail to instantiate a PublicKey from a PrivateKey, error: \(error)") + } } /// Creates a random `secp256k1` private key for signing. @@ -462,13 +468,16 @@ extension K1.ECDSA { let publicKeyImpl: K1._PublicKeyImplementation /// The corresponding public key. - public var publicKey: PublicKey { - try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) - } + public let publicKey: PublicKey init(impl: Impl) { self.impl = impl self.publicKeyImpl = impl.publicKey + do { + self.publicKey = try PublicKey(rawRepresentation: impl.publicKey.rawRepresentation) + } catch { + fatalError("Should never fail to instantiate a PublicKey from a PrivateKey, error: \(error)") + } } /// Creates a random `secp256k1` private key for signing. @@ -685,13 +694,16 @@ extension K1.ECDSAWithKeyRecovery { let publicKeyImpl: K1._PublicKeyImplementation /// The corresponding public key. - public var publicKey: PublicKey { - try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) - } + public let publicKey: PublicKey init(impl: Impl) { self.impl = impl self.publicKeyImpl = impl.publicKey + do { + self.publicKey = try PublicKey(rawRepresentation: impl.publicKey.rawRepresentation) + } catch { + fatalError("Should never fail to instantiate a PublicKey from a PrivateKey, error: \(error)") + } } /// Creates a random `secp256k1` private key for signing. diff --git a/Sources/K1/K1/Keys/Keys.swift.gyb b/Sources/K1/K1/Keys/Keys.swift.gyb index 0b51bfd..3c8ed89 100644 --- a/Sources/K1/K1/Keys/Keys.swift.gyb +++ b/Sources/K1/K1/Keys/Keys.swift.gyb @@ -58,13 +58,17 @@ extension K1.${FEATURE} { let publicKeyImpl: K1._PublicKeyImplementation /// The corresponding public key. - public var publicKey: PublicKey { - try! .init(rawRepresentation: publicKeyImpl.rawRepresentation) - } + public let publicKey: PublicKey + init(impl: Impl) { self.impl = impl self.publicKeyImpl = impl.publicKey + do { + self.publicKey = try PublicKey(rawRepresentation: impl.publicKey.rawRepresentation) + } catch { + fatalError("Should never fail to instantiate a PublicKey from a PrivateKey, error: \(error)") + } } /// Creates a random `secp256k1` private key for ${PURPOSE_PRIVATEKEY}. diff --git a/Sources/K1/K1/Keys/PublicKeyImplementation.swift b/Sources/K1/K1/Keys/PublicKeyImplementation.swift index 53158f5..0cbfe95 100644 --- a/Sources/K1/K1/Keys/PublicKeyImplementation.swift +++ b/Sources/K1/K1/Keys/PublicKeyImplementation.swift @@ -83,14 +83,12 @@ extension K1._PublicKeyImplementation { /// `04 || X || Y` (65 bytes) var x963Representation: Data { - // swiftlint:disable:next force_try - try! FFI.PublicKey.serialize(wrapped, format: .uncompressed) + FFI.PublicKey.serialize(wrapped, format: .uncompressed) } /// `02|03 || X` (33 bytes) var compressedRepresentation: Data { - // swiftlint:disable:next force_try - try! FFI.PublicKey.serialize(wrapped, format: .compressed) + FFI.PublicKey.serialize(wrapped, format: .compressed) } /// `DER` @@ -119,15 +117,7 @@ extension K1._PublicKeyImplementation { static func == (lhsSelf: Self, rhsSelf: Self) -> Bool { let lhs = lhsSelf.wrapped let rhs = rhsSelf.wrapped - do { - return try lhs.compare(to: rhs) - } catch { - return lhs.withUnsafeBytes { lhsBytes in - rhs.withUnsafeBytes { rhsBytes in - safeCompare(lhsBytes, rhsBytes) - } - } - } + return lhs.isEqual(to: rhs) } } diff --git a/Sources/K1/K1/Validation/Validation.generated.swift b/Sources/K1/K1/Validation/Validation.generated.swift index 71ab718..080e8d1 100644 --- a/Sources/K1/K1/Validation/Validation.generated.swift +++ b/Sources/K1/K1/Validation/Validation.generated.swift @@ -21,16 +21,12 @@ extension K1.ECDSA.PublicKey { hashed: some DataProtocol, options: K1.ECDSA.ValidationOptions = .default ) -> Bool { - do { - return try FFI.ECDSA.isValid( - signature: signature.wrapped, - publicKey: self.impl.wrapped, - message: [UInt8](hashed), - options: options - ) - } catch { - return false - } + FFI.ECDSA.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: [UInt8](hashed), + options: options + ) } /// Verifies an Elliptic Curve Digital Signature Algorithm (ECDSA) non recoverable signature on a digest over the `secp256k1` elliptic curve. @@ -87,16 +83,12 @@ extension K1.ECDSAWithKeyRecovery.PublicKey { hashed: some DataProtocol, options: K1.ECDSA.ValidationOptions = .default ) -> Bool { - do { - return try FFI.ECDSAWithKeyRecovery.isValid( - signature: signature.wrapped, - publicKey: self.impl.wrapped, - message: [UInt8](hashed), - options: options - ) - } catch { - return false - } + FFI.ECDSAWithKeyRecovery.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: [UInt8](hashed), + options: options + ) } /// Verifies an Elliptic Curve Digital Signature Algorithm (ECDSA) recoverable signature on a digest over the `secp256k1` elliptic curve. @@ -151,15 +143,11 @@ extension K1.Schnorr.PublicKey { _ signature: K1.Schnorr.Signature, hashed: Span ) -> Bool { - do { - return try FFI.Schnorr.isValid( - signature: signature.wrapped, - publicKey: self.impl.wrapped, - message: hashed - ) - } catch { - return false - } + FFI.Schnorr.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: hashed + ) } /// Verifies Schnorr signature on some _hash_ over the `secp256k1` elliptic curve. @@ -171,16 +159,12 @@ extension K1.Schnorr.PublicKey { _ signature: K1.Schnorr.Signature, hashed bytes: [UInt8] ) -> Bool { - do { - return try withSpanFromArray(bytes) { span in - try FFI.Schnorr.isValid( - signature: signature.wrapped, - publicKey: self.impl.wrapped, - message: span - ) - } - } catch { - return false + withSpanFromArray(bytes) { span in + FFI.Schnorr.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: span + ) } } @@ -193,16 +177,12 @@ extension K1.Schnorr.PublicKey { _ signature: K1.Schnorr.Signature, hashed data: some DataProtocol ) -> Bool { - do { - return try withSpanFromData(data) { span in - try FFI.Schnorr.isValid( - signature: signature.wrapped, - publicKey: self.impl.wrapped, - message: span - ) - } - } catch { - return false + withSpanFromData(data) { span in + FFI.Schnorr.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: span + ) } } diff --git a/Sources/K1/K1/Validation/Validation.swift.gyb b/Sources/K1/K1/Validation/Validation.swift.gyb index 8a0235f..e81ddeb 100644 --- a/Sources/K1/K1/Validation/Validation.swift.gyb +++ b/Sources/K1/K1/Validation/Validation.swift.gyb @@ -58,15 +58,11 @@ extension K1.${FEATURE}.PublicKey { _ signature: K1.${FEATURE}.Signature, hashed: Span${VALIDATION_OPTIONS_ARG} ) -> Bool { - do { - return try FFI.${FEATURE}.isValid( - signature: signature.wrapped, - publicKey: self.impl.wrapped, - message: hashed${VALIDATION_OPTIONS_FWD} - ) - } catch { - return false - } + FFI.${FEATURE}.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: hashed${VALIDATION_OPTIONS_FWD} + ) } /// Verifies ${VARIANT} signature on some _hash_ over the `secp256k1` elliptic curve. @@ -78,16 +74,12 @@ extension K1.${FEATURE}.PublicKey { _ signature: K1.${FEATURE}.Signature, hashed bytes: [UInt8]${VALIDATION_OPTIONS_ARG} ) -> Bool { - do { - return try withSpanFromArray(bytes) { span in - try FFI.${FEATURE}.isValid( - signature: signature.wrapped, - publicKey: self.impl.wrapped, - message: span${VALIDATION_OPTIONS_FWD} - ) - } - } catch { - return false + withSpanFromArray(bytes) { span in + FFI.${FEATURE}.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: span${VALIDATION_OPTIONS_FWD} + ) } } @@ -100,16 +92,12 @@ extension K1.${FEATURE}.PublicKey { _ signature: K1.${FEATURE}.Signature, hashed data: some DataProtocol${VALIDATION_OPTIONS_ARG} ) -> Bool { - do { - return try withSpanFromData(data) { span in - try FFI.${FEATURE}.isValid( - signature: signature.wrapped, - publicKey: self.impl.wrapped, - message: span${VALIDATION_OPTIONS_FWD} - ) - } - } catch { - return false + withSpanFromData(data) { span in + FFI.${FEATURE}.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: span${VALIDATION_OPTIONS_FWD} + ) } } % else: @@ -120,15 +108,11 @@ extension K1.${FEATURE}.PublicKey { _ signature: K1.${FEATURE}.Signature, hashed: some DataProtocol${VALIDATION_OPTIONS_ARG} ) -> Bool { - do { - return try FFI.${FEATURE}.isValid( - signature: signature.wrapped, - publicKey: self.impl.wrapped, - message: [UInt8](hashed)${VALIDATION_OPTIONS_FWD} - ) - } catch { - return false - } + FFI.${FEATURE}.isValid( + signature: signature.wrapped, + publicKey: self.impl.wrapped, + message: [UInt8](hashed)${VALIDATION_OPTIONS_FWD} + ) } % end diff --git a/Sources/K1/Support/Extensions/Data+Extensions.swift b/Sources/K1/Support/Extensions/Foundation/Data+Extensions.swift similarity index 100% rename from Sources/K1/Support/Extensions/Data+Extensions.swift rename to Sources/K1/Support/Extensions/Foundation/Data+Extensions.swift diff --git a/Sources/K1/Support/Extensions/Secp256k1/ComparisonOutcomeRaw+Extension.swift b/Sources/K1/Support/Extensions/Secp256k1/ComparisonOutcomeRaw+Extension.swift new file mode 100644 index 0000000..aaaefc8 --- /dev/null +++ b/Sources/K1/Support/Extensions/Secp256k1/ComparisonOutcomeRaw+Extension.swift @@ -0,0 +1,23 @@ +import Secp256k1 + +/// Outcome of comparing two values with each other +enum ComparisonOutcome { + /// The compared values are equal. + case equal + + /// The RHS of the compared values is greater than the LHS. + case lhsIsGreater + + /// The RHS of the compared values is greater than the LHS. + case rhsIsGreater +} + +extension ComparisonOutcome { + init(raw: ComparisonOutcomeRaw) { + switch raw { + case .SECP256K1_PUBKEY_CMP_EQUAL: self = .equal + case .SECP256K1_PUBKEY_CMP_LHS_IS_GREATER: self = .lhsIsGreater + case .SECP256K1_PUBKEY_CMP_RHS_IS_GREATER: self = .rhsIsGreater + } + } +} diff --git a/Sources/K1/Support/Extensions/Secp256k1/NormalizeSignatureOutcomeRaw+Extension.swift b/Sources/K1/Support/Extensions/Secp256k1/NormalizeSignatureOutcomeRaw+Extension.swift new file mode 100644 index 0000000..660d284 --- /dev/null +++ b/Sources/K1/Support/Extensions/Secp256k1/NormalizeSignatureOutcomeRaw+Extension.swift @@ -0,0 +1,6 @@ +import Secp256k1 + +extension NormalizeSignatureOutcomeRaw { + /// The checked signature was not normalized. + static let wasntNormalized: Self = .SECP256K1_NORMALIZE_SIG_WASNT_NORMALIZED +} diff --git a/Sources/K1/Support/Extensions/Secp256k1/ResultRaw+Extension.swift b/Sources/K1/Support/Extensions/Secp256k1/ResultRaw+Extension.swift new file mode 100644 index 0000000..acd4238 --- /dev/null +++ b/Sources/K1/Support/Extensions/Secp256k1/ResultRaw+Extension.swift @@ -0,0 +1,9 @@ +import Secp256k1 + +extension ResultRaw { + /// The call to the `secp256k1` function returned with successful result. + static let success: Self = .SECP256K1_RESULT_SUCCESS + + /// The call to the `secp256k1` function returned with failure result. + static let failure: Self = .SECP256K1_RESULT_FAILURE +} diff --git a/Sources/K1/Support/Extensions/Secp256k1/VerifySignatureOutcomeRaw+Extension.swift b/Sources/K1/Support/Extensions/Secp256k1/VerifySignatureOutcomeRaw+Extension.swift new file mode 100644 index 0000000..9393229 --- /dev/null +++ b/Sources/K1/Support/Extensions/Secp256k1/VerifySignatureOutcomeRaw+Extension.swift @@ -0,0 +1,9 @@ +import Secp256k1 + +extension VerifySignatureOutcomeRaw { + /// The checked Signature is valid. + static let signatureValid: Self = .SECP256K1_VERIFY_SIG_CORRECT + + /// The checked Signature is invalid or we failed to parse it. + static let signatureInvalid: Self = .SECP256K1_VERIFY_SIG_UNPARSABLE_OR_INCORRECT +} diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift index 79ddb68..b99a55c 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift @@ -46,7 +46,7 @@ extension FFI.ECDSA { static func compact(_ wrapped: Wrapped) throws -> Data { var out = [UInt8](repeating: 0, count: Self.byteCount) var rawSignature = wrapped.raw - try FFI.call(ifFailThrow: .ecdsaSignatureSerializeCompact) { context in + FFI.call { context in serializeEcdsaSignatureCompact( context: context, outputBytes: &out, @@ -130,25 +130,25 @@ extension FFI.ECDSA { publicKey: FFI.PublicKey.Wrapped, message: [UInt8], options: K1.ECDSA.ValidationOptions = .default - ) throws -> Bool { - try FFI.toC { ffi -> Bool in + ) -> Bool { + FFI.toC { ffi -> Bool in var publicKeyRaw = publicKey.raw var maybeMalleable = signature.raw var normalized = ECDSASignatureRaw() - let codeForSignatureWasMalleable = 1 - let signatureWasMalleableResult = ffi.callWithResultCode { context in + let signatureWasMalleable = ffi.call { context in normalizeEcdsaSignature(context: context, outputSignature: &normalized, inputSignature: &maybeMalleable) - } - let signatureWasMalleable = signatureWasMalleableResult == codeForSignatureWasMalleable - let isSignatureValid = ffi.validate { context in + } == .wasntNormalized + + let isSignatureValid = ffi.call { context in verifyEcdsaSignature( context: context, signature: &normalized, messageHash: message, publicKey: &publicKeyRaw ) - } + } == .signatureValid + let acceptMalleableSignatures = options.malleabilityStrictness == .accepted switch (isSignatureValid, signatureWasMalleable, acceptMalleableSignatures) { case (true, false, _): diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift index 8f48ded..c555a18 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift @@ -20,7 +20,7 @@ typealias ECDSAFunctionPointer = ( UnsafePointer, secp256k1_nonce_function?, UnsafeRawPointer? -) -> Int32 +) -> ResultRaw // MARK: Sign extension FFI.ECDSAWithKeyRecovery.Wrapped { diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift index bb03a20..87d7d1b 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/FFI+ECDSA+Recovery.swift @@ -36,14 +36,12 @@ extension FFI.ECDSAWithKeyRecovery { extension FFI.ECDSAWithKeyRecovery { static func serializeCompact( _ wrapped: Wrapped - ) throws -> (rs: [UInt8], recoveryID: Int32) { + ) -> (rs: [UInt8], recoveryID: Int32) { // swiftlint:disable:next identifier_name var rs = [UInt8](repeating: 0, count: FFI.ECDSA.byteCount) var recoveryID: Int32 = 0 var rawSignature = wrapped.raw - try FFI.call( - ifFailThrow: .recoverableSignatureSerializeCompact - ) { context in + FFI.call { context in serializeRecoverableECDSASignatureCompact( context: context, outputBytes: &rs, @@ -60,13 +58,11 @@ extension FFI.ECDSAWithKeyRecovery { /// Convert a recoverable signature into a normal signature. static func nonRecoverable( _ wrapped: Wrapped - ) throws -> FFI.ECDSA.Wrapped { + ) -> FFI.ECDSA.Wrapped { var nonRecoverable = ECDSASignatureRaw() var recoverable = wrapped.raw - try FFI.call( - ifFailThrow: .recoverableSignatureConvert - ) { context in + FFI.call { context in ecdsaRecoverableSignatureToNonRecoverable( context: context, outputNonRecoverableSignature: &nonRecoverable, @@ -110,19 +106,15 @@ extension FFI.ECDSAWithKeyRecovery { publicKey: FFI.PublicKey.Wrapped, message: [UInt8], options: K1.ECDSA.ValidationOptions = .default - ) throws -> Bool { - do { - let publicKeyNonRecoverable = FFI.PublicKey.Wrapped(raw: publicKey.raw) - let signatureNonRecoverable = try FFI.ECDSAWithKeyRecovery.nonRecoverable(signature) - return try FFI.ECDSA.isValid( - signature: signatureNonRecoverable, - publicKey: publicKeyNonRecoverable, - message: message, - options: options - ) - } catch { - return false - } + ) -> Bool { + let publicKeyNonRecoverable = FFI.PublicKey.Wrapped(raw: publicKey.raw) + let signatureNonRecoverable = FFI.ECDSAWithKeyRecovery.nonRecoverable(signature) + return FFI.ECDSA.isValid( + signature: signatureNonRecoverable, + publicKey: publicKeyNonRecoverable, + message: message, + options: options + ) } } diff --git a/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift index 6485bce..8ed4904 100644 --- a/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Keys/PrivateKey/PrivateKey+Wrapped.swift @@ -30,7 +30,6 @@ extension FFI { privateKeyBytes: seckey.baseAddress! ) } - return FFI.PublicKey.Wrapped(raw: raw) } } diff --git a/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift b/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift index f7db5dd..60041cd 100644 --- a/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/FFI+PublicKey.swift @@ -81,11 +81,11 @@ extension FFI.PublicKey { static func serialize( _ wrapped: Wrapped, format: K1.Format - ) throws -> Data { + ) -> Data { var byteCount = format.length var out = [UInt8](repeating: 0x00, count: byteCount) var publicKeyRaw = wrapped.raw - try FFI.call(ifFailThrow: .publicKeySerialize) { context in + FFI.call { context in serializePublicKey( context: context, outputBytes: &out, diff --git a/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift index 06bcb47..ba74a37 100644 --- a/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift @@ -30,15 +30,22 @@ extension FFI.PublicKey.Wrapped { // MARK: Comparison extension FFI.PublicKey.Wrapped { - func compare(to other: FFI.PublicKey.Wrapped) throws -> Bool { - var selfRaw = self.raw - var otherRaw = other.raw - return try FFI.toC { ffi in - ffi.callWithResultCode { context in - comparePublicKeys(context: context, first: &selfRaw, second: &otherRaw) - } == 0 + func compare(to other: FFI.PublicKey.Wrapped) -> ComparisonOutcome { + var lhs = self.raw + var rhs = other.raw + return FFI.call { context in + let outcomeRaw = comparePublicKeys( + context: context, + lhs: &lhs, + rhs: &rhs + ) + return ComparisonOutcome(raw: outcomeRaw) } } + + func isEqual(to other: FFI.PublicKey.Wrapped) -> Bool { + compare(to: other) == .equal + } } // MARK: Group Operations @@ -56,10 +63,8 @@ extension FFI.PublicKey.Wrapped { /// Negates a public key (point) on the secp256k1 curve func negate() throws -> Self { var result = self.raw - try FFI.toC { ffi in - try ffi.call(ifFailThrow: .publicKeyCreate) { context in - negatePublicKey(context: context, publicKey: &result) - } + FFI.call { context in + negatePublicKey(context: context, publicKey: &result) } return Self(raw: result) } @@ -73,24 +78,22 @@ extension FFI.PublicKey.Wrapped { var result = PublicKeyRaw() var mutableKeys = keys.map(\.raw) - try mutableKeys.withUnsafeMutableBufferPointer { keysBuffer in - var keyPointers = [UnsafePointer?]() - keyPointers.reserveCapacity(keys.count) + try FFI.call(ifFailThrow: .groupOperation) { context in + mutableKeys.withUnsafeMutableBufferPointer { keysBuffer in + var keyPointers = [UnsafePointer?]() + keyPointers.reserveCapacity(keys.count) - for index in 0 ..< keys.count { - keyPointers.append(keysBuffer.baseAddress!.advanced(by: index)) - } + for index in 0 ..< keys.count { + keyPointers.append(keysBuffer.baseAddress!.advanced(by: index)) + } - try keyPointers.withUnsafeBufferPointer { pointers in - try FFI.toC { ffi in - try ffi.call(ifFailThrow: .groupOperation) { context in - combinePublicKeys( - context: context, - outputPublicKey: &result, - inputs: pointers.baseAddress!, - inputCount: keys.count - ) - } + return keyPointers.withUnsafeBufferPointer { pointers in + combinePublicKeys( + context: context, + outputPublicKey: &result, + inputs: pointers.baseAddress!, + inputCount: keys.count + ) } } } diff --git a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift index 0384dc7..9489814 100644 --- a/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift +++ b/Sources/K1/Support/FFI/API/Schnorr/FFI+Schnorr.swift @@ -12,12 +12,12 @@ extension FFI.Schnorr { signature: FFI.Schnorr.Wrapped, publicKey: FFI.PublicKey.Wrapped, message: Span - ) throws -> Bool { - try FFI.toC { ffi -> Bool in + ) -> Bool { + FFI.toC { ffi -> Bool in var publicKeyX = PublicKeyXOnlyRaw() var publicKeyRaw = publicKey.raw - try FFI.call(ifFailThrow: .xonlyPublicKeyFromPublicKey) { context in - xOnlyPublicKeyFromPublicKey( + ffi.call { context in + _ = xOnlyPublicKeyFromPublicKey( context: context, outputXOnlyPublicKey: &publicKeyX, parity: nil, @@ -25,14 +25,14 @@ extension FFI.Schnorr { ) } - return ffi.validate { context in + return ffi.call { context in verifySchnorrSignature( context: context, signatureBytes: signature.bytes, msg: message, xOnlyPublicKey: &publicKeyX ) - } + } == .success } } } diff --git a/Sources/K1/Support/FFI/Internals/FFI+Call.swift b/Sources/K1/Support/FFI/Internals/FFI+Call.swift index 8b6dedb..4e5d55d 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Call.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Call.swift @@ -1,15 +1,56 @@ import CryptoKit import Secp256k1 +/// `secp256k1_context` +/// +/// In the best of worlds this would not be a typealias +/// for an `OpaquePointer`, mapping to a Swift class using +/// apinotes have been attempted without success, seems the +/// implementation of `secp256k1` prevents it. +/// +/// An `UnsafePointer` would also +/// have been better, but mapping to it using apinotes also +/// failed. +typealias Secp256k1ContextRaw = OpaquePointer + // MARK: - FFI + +/// A swift wrapper around an `OpaquePointer` to `secp256k1_context` +/// created with `secp256k1_context_create`. +/// +/// In the best of worlds this would not be a typealias +/// for an `OpaquePointer`, mapping to a Swift class using +/// apinotes have been attempted without success, seems the +/// implementation of `secp256k1` prevents it. +/// +/// An `UnsafePointer` would also +/// have been better, but mapping to it using apinotes also +/// failed. final class FFI { - let context: OpaquePointer - init() throws { + /// The wrapped `secp256k1_context` (`OpaquePointer`). + let context: Secp256k1ContextRaw + + /// Creates a new `secp256k1_context` using `secp256k1_context_create` to be used + /// for both signing and verification operations. + /// + /// Will crash if `secp256k1_context_create` fails, which it never should. + init() { guard - /* "Create a secp256k1 context object." */ + // Create secp256k1 context object let context = createContext(flags: Context.sign.rawValue | Context.verify.rawValue) else { - throw K1.Error.underlyingLibsecp256k1Error(.failedToCreateContextForSecp256k1) + fatalError( + """ + Failed to create context, did you run out of memory? + + `secp256k1_context_create` call failed. Which under most circumstances should never ever happen. + + Please report a bug at: + https://github.com/Sajjon/K1/issues/new + + And provide OS and Swift version details. + """ + ) } self.context = context @@ -20,45 +61,60 @@ final class FFI { } } +// MARK: Helper Methods extension FFI { - static func toC( - _ closure: (FFI) throws -> T - ) throws -> T { - let ffi = try FFI() - return try closure(ffi) + func call( + ifFailThrow error: FFI.Error, + _ method: (Secp256k1ContextRaw) -> ResultRaw + ) throws { + guard method(context) == ResultRaw.success else { + throw K1.Error.underlyingLibsecp256k1Error(error) + } } - /// Returns `true` iff result code is `1` - func validate( - _ method: (OpaquePointer) -> Int32 - ) -> Bool { - method(context) == 1 + func call( + _ method: (OpaquePointer) throws -> R + ) rethrows -> R { + try method(context) } - func callWithResultCode( - _ method: (OpaquePointer) -> Int32 - ) -> Int { - let result = method(context) - return Int(result) + func callGetResult( + _ method: (OpaquePointer) -> ResultRaw + ) -> ResultRaw { + method(context) } +} - func call( - ifFailThrow error: FFI.Error, - _ method: (OpaquePointer) -> Int32 - ) throws { - let result = callWithResultCode(method) - let successCode = 1 - guard result == successCode else { - throw K1.Error.underlyingLibsecp256k1Error(error) - } +// MARK: - Static +extension FFI { + static func toC( + _ body: (FFI) throws -> R + ) rethrows -> R { + try body(FFI()) + } + + static func call( + _ method: @escaping (OpaquePointer) throws -> R + ) rethrows -> R { + try method(FFI().context) + } + + static func callGetResult( + _ method: (OpaquePointer) -> ResultRaw + ) -> ResultRaw { + FFI().callGetResult(method) } static func call( ifFailThrow error: FFI.Error, - _ method: (OpaquePointer) -> Int32 + _ method: (OpaquePointer) -> ResultRaw ) throws { - try toC { ffi in - try ffi.call(ifFailThrow: error, method) - } + try FFI().call(ifFailThrow: error, method) + } + + static func call( + _ method: @escaping (OpaquePointer) -> Int32 + ) { + _ = method(FFI().context) } } diff --git a/Sources/K1/Support/Misc/K1+Error.swift b/Sources/K1/Support/Misc/K1+Error.swift index bb75bcf..5fd8d34 100644 --- a/Sources/K1/Support/Misc/K1+Error.swift +++ b/Sources/K1/Support/Misc/K1+Error.swift @@ -43,8 +43,6 @@ enum InternalFailure: UInt, Sendable, Swift.Error, Hashable { // MARK: - FFI.Error extension FFI { enum Error: Int, Sendable, Swift.Error, Hashable { - case failedToCreateContextForSecp256k1 - /// `parseEcdsaSignatureCompact(context:outputSignature:inputBytes:)` (`secp256k1_ecdsa_signature_parse_compact`) failed case ecdsaSignatureParseCompact @@ -129,8 +127,6 @@ extension K1.Error: CustomDebugStringConvertible { case .ecdh: return "ecdh" case .ecdsaSign: return "ECDSA sign" case .ecdsaSignatureParseCompact: return "ECDSA signature parse compact" - case .failedToCreateContextForSecp256k1: - return "create context" case .ecdsaSignatureParseDER: return "ECDSA signature parse DER" case .ecdsaSignatureSerializeCompact: diff --git a/Sources/secp256k1/include/Secp256k1.apinotes b/Sources/secp256k1/include/Secp256k1.apinotes index fca29d2..d4df9ac 100644 --- a/Sources/secp256k1/include/Secp256k1.apinotes +++ b/Sources/secp256k1/include/Secp256k1.apinotes @@ -16,6 +16,22 @@ Tags: - Name: secp256k1_xonly_pubkey SwiftName: PublicKeyXOnlyRaw +- Name: secp256k1_result + SwiftName: ResultRaw + EnumExtensibility: closed + +- Name: secp256k1_pubkey_cmp_result + SwiftName: ComparisonOutcomeRaw + EnumExtensibility: closed + +- Name: secp256k1_normalize_sig_result + SwiftName: NormalizeSignatureOutcomeRaw + EnumExtensibility: closed + +- Name: secp256k1_verify_sig_result + SwiftName: VerifySignatureOutcomeRaw + EnumExtensibility: closed + Functions: - Name: secp256k1_ecdsa_recoverable_signature_convert SwiftName: ecdsaRecoverableSignatureToNonRecoverable(context:outputNonRecoverableSignature:recoverableSignature:) @@ -44,6 +60,7 @@ Functions: - Name: secp256k1_ecdsa_recoverable_signature_parse_compact SwiftName: parseRecoverableECDSASignatureFromCompactBytes(context:outputRecoveredSignature:compactBytes:recoveryID:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx Nullability: N @@ -59,6 +76,7 @@ Functions: - Name: secp256k1_ecdsa_recover SwiftName: recoverPublicKeyFromECDSASignature(context:publicKey:signature:hashedMessage:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx Nullability: N @@ -74,6 +92,7 @@ Functions: - Name: secp256k1_ecdsa_sign SwiftName: ecdsaSignNonRecoverable(context:outputSignature:hashedMessageBytes:privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx Nullability: N @@ -96,6 +115,7 @@ Functions: - Name: secp256k1_ecdsa_sign_recoverable SwiftName: ecdsaSignRecoverable(context:outputSignature:hashedMessageBytes:privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx Nullability: N @@ -118,6 +138,7 @@ Functions: - Name: secp256k1_schnorrsig_sign32 SwiftName: schnorrSign(context:outputSignatureBytes:message:keypair:auxiliaryRandomData:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx (secp256k1_context) Nullability: N @@ -141,6 +162,7 @@ Functions: - Name: secp256k1_keypair_create SwiftName: keypairFromPrivateKey(context:outputKeyPair:privateKeyBytes:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx (secp256k1_context) Nullability: N @@ -154,6 +176,7 @@ Functions: - Name: secp256k1_schnorrsig_verify SwiftName: verifySchnorrSignature(context:signatureBytes:msg:msglen:xOnlyPublicKey:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx (secp256k1_context) Nullability: N @@ -184,6 +207,7 @@ Functions: - Name: secp256k1_ecdh SwiftName: ecdh(context:outputSharedPointBytes:publicKey:privateKeyBytes:hashFunction:arbitraryData:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx Nullability: N @@ -201,26 +225,6 @@ Functions: - Position: 5 # data Nullability: O -- Name: fill_with_fives - SwiftName: fillWithFives(span:) - Parameters: - - Position: 0 # buf - Lifetimebound: true - BoundsSafety: - Kind: counted_by - BoundedBy: "len" - Nullability: N - - Position: 1 # len - Nullability: N - -- Name: clone_buf_of_len_three - SwiftName: cloneSpanOfLength3Unsafe(into:from:) - Parameters: - - Position: 0 # destination - Nullability: N - - Position: 1 # source - Nullability: N - # Context management - Name: secp256k1_context_create SwiftName: createContext(flags:) @@ -237,6 +241,7 @@ Functions: # Secret keys and pubkeys - Name: secp256k1_ec_pubkey_create SwiftName: createPublicKey(context:outputPublicKey:privateKeyBytes:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx Nullability: N @@ -250,6 +255,7 @@ Functions: - Name: secp256k1_ec_pubkey_parse SwiftName: parsePublicKey(context:outputPublicKey:inputBytes:inputlen:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx Nullability: N @@ -278,8 +284,9 @@ Functions: - Position: 4 # flags Nullability: N -- Name: secp256k1_ec_pubkey_cmp - SwiftName: comparePublicKeys(context:first:second:) +- Name: secp256k1_ec_pubkey_cmp_result + SwiftName: comparePublicKeys(context:lhs:rhs:) + ResultType: secp256k1_pubkey_cmp_result Parameters: - Position: 0 # ctx Nullability: N @@ -298,6 +305,7 @@ Functions: - Name: secp256k1_ec_pubkey_combine SwiftName: combinePublicKeys(context:outputPublicKey:inputs:inputCount:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx Nullability: N @@ -310,6 +318,7 @@ Functions: - Name: secp256k1_ec_pubkey_sort SwiftName: sortPublicKeys(context:pubkeys:count:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx Nullability: N @@ -321,6 +330,7 @@ Functions: # ECDSA signature parsing/serialization - Name: secp256k1_ecdsa_signature_parse_compact SwiftName: parseEcdsaSignatureCompact(context:outputSignature:inputBytes:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx Nullability: N @@ -334,6 +344,7 @@ Functions: - Name: secp256k1_ecdsa_signature_parse_der SwiftName: parseEcdsaSignatureDER(context:outputSignature:input:inputlen:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx Nullability: N @@ -350,6 +361,7 @@ Functions: - Name: secp256k1_ecdsa_signature_serialize_der SwiftName: serializeEcdsaSignatureDER(context:outputBytes:outputByteCount:signature:) + ResultType: secp256k1_result Parameters: - Position: 0 # ctx Nullability: N @@ -376,6 +388,7 @@ Functions: # ECDSA verify/normalize - Name: secp256k1_ecdsa_signature_normalize SwiftName: normalizeEcdsaSignature(context:outputSignature:inputSignature:) + ResultType: secp256k1_normalize_sig_result Parameters: - Position: 0 # ctx Nullability: N @@ -386,6 +399,7 @@ Functions: - Name: secp256k1_ecdsa_verify SwiftName: verifyEcdsaSignature(context:signature:messageHash:publicKey:) + ResultType: secp256k1_verify_sig_result Parameters: - Position: 0 # ctx Nullability: N diff --git a/Sources/secp256k1/include/k1_umbrella.h b/Sources/secp256k1/include/k1_umbrella.h index 7fe6d1f..9a1e223 100644 --- a/Sources/secp256k1/include/k1_umbrella.h +++ b/Sources/secp256k1/include/k1_umbrella.h @@ -1,3 +1,5 @@ +#include "../src/k1_shim.h" + #include "../libsecp256k1/include/secp256k1.h" #include "../libsecp256k1/include/secp256k1_ecdh.h" #include "../libsecp256k1/include/secp256k1_extrakeys.h" diff --git a/Sources/secp256k1/src/ecdh_variants.h b/Sources/secp256k1/src/ecdh_variants.h index f3f5131..756c0a0 100644 --- a/Sources/secp256k1/src/ecdh_variants.h +++ b/Sources/secp256k1/src/ecdh_variants.h @@ -1,10 +1,3 @@ -// -// ecdh_variants.h -// -// -// Created by Alexander Cyon on 2022-01-31. -// - #ifndef ecdh_variants_h #define ecdh_variants_h diff --git a/Sources/secp256k1/src/k1_shim.c b/Sources/secp256k1/src/k1_shim.c new file mode 100644 index 0000000..ce9c558 --- /dev/null +++ b/Sources/secp256k1/src/k1_shim.c @@ -0,0 +1,15 @@ +#include "./k1_shim.h" +#include "../libsecp256k1/include/secp256k1.h" +#include + +//secp256k1_pubkey_cmp_result +//secp256k1_ec_pubkey_cmp_result( +// const secp256k1_context *ctx, +// const secp256k1_pubkey *lhs, +// const secp256k1_pubkey *rhs +//) { +// int r = secp256k1_ec_pubkey_cmp(ctx, lhs, rhs); +// return r < 0 ? SECP256K1_PUBKEY_CMP_RHS_IS_GREATER +// : r > 0 ? SECP256K1_PUBKEY_CMP_LHS_IS_GREATER +// : SECP256K1_PUBKEY_CMP_EQUAL; +//} diff --git a/Sources/secp256k1/src/k1_shim.h b/Sources/secp256k1/src/k1_shim.h new file mode 100644 index 0000000..2ea9982 --- /dev/null +++ b/Sources/secp256k1/src/k1_shim.h @@ -0,0 +1,40 @@ +#ifndef k1_shim_h +#define k1_shim_h + +typedef enum secp256k1_result { + SECP256K1_RESULT_SUCCESS = 1, + SECP256K1_RESULT_FAILURE = 0, +} secp256k1_result; + +typedef enum secp256k1_pubkey_cmp_result { + SECP256K1_PUBKEY_CMP_EQUAL = 0, + SECP256K1_PUBKEY_CMP_LHS_IS_GREATER = 1, + SECP256K1_PUBKEY_CMP_RHS_IS_GREATER = -1, +} secp256k1_pubkey_cmp_result; + +typedef enum secp256k1_normalize_sig_result { + SECP256K1_NORMALIZE_SIG_ALREADY_NORMALIZED = 0, + SECP256K1_NORMALIZE_SIG_WASNT_NORMALIZED = 1, +} secp256k1_normalize_sig_result; + + +typedef enum secp256k1_verify_sig_result { + SECP256K1_VERIFY_SIG_UNPARSABLE_OR_INCORRECT = 0, + SECP256K1_VERIFY_SIG_CORRECT = 1, +} secp256k1_verify_sig_result; + +#include "../libsecp256k1/include/secp256k1.h" + +static inline secp256k1_pubkey_cmp_result +secp256k1_ec_pubkey_cmp_result( + const secp256k1_context *ctx, + const secp256k1_pubkey *lhs, + const secp256k1_pubkey *rhs +) { + int r = secp256k1_ec_pubkey_cmp(ctx, lhs, rhs); + return r < 0 ? SECP256K1_PUBKEY_CMP_RHS_IS_GREATER + : r > 0 ? SECP256K1_PUBKEY_CMP_LHS_IS_GREATER + : SECP256K1_PUBKEY_CMP_EQUAL; +} + +#endif /* k1_shim_h */ diff --git a/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTrezorTests.swift b/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTrezorTests.swift index 6fb5674..f038691 100644 --- a/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTrezorTests.swift +++ b/Tests/K1Tests/TestCases/ECDSA/ECDSASignatureTrezorTests.swift @@ -52,7 +52,7 @@ private extension XCTestCase { let privateKeyRecoverable = try K1.ECDSAWithKeyRecovery.PrivateKey(rawRepresentation: privateKey.rawRepresentation) let signatureRecoverableFromMessage = try privateKeyRecoverable.signature(for: messageDigest) - try XCTAssertEqual(signatureRecoverableFromMessage.nonRecoverable(), expectedSignature) + XCTAssertEqual(signatureRecoverableFromMessage.nonRecoverable(), expectedSignature) let recid = try signatureRecoverableFromMessage.compact().recoveryID XCTAssertEqual( diff --git a/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyGroupOperationsTests.swift b/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyGroupOperationsTests.swift index 7198c88..7530eef 100644 --- a/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyGroupOperationsTests.swift +++ b/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyGroupOperationsTests.swift @@ -63,7 +63,7 @@ final class PublicKeyGroupOperationsTests: XCTestCase { ] let expectedGenerator = try K1.Schnorr.PublicKey(rawRepresentation: generatorPoint) - let actualGeneratorRaw = try FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.g, format: .uncompressed) + let actualGeneratorRaw = FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.g, format: .uncompressed) let actualGenerator = try K1.Schnorr.PublicKey(rawRepresentation: actualGeneratorRaw.dropFirst()) // Drop the 0x04 prefix XCTAssertEqual(expectedGenerator.rawRepresentation, actualGenerator.rawRepresentation) @@ -84,11 +84,11 @@ final class PublicKeyGroupOperationsTests: XCTestCase { let priv5 = try FFI.PrivateKey.Wrapped(scalar: 5) let priv6 = try FFI.PrivateKey.Wrapped(scalar: 6) - XCTAssertTrue(try gx2.compare(to: priv2.publicKey)) - XCTAssertTrue(try gx3.compare(to: priv3.publicKey)) - XCTAssertTrue(try gx4.compare(to: priv4.publicKey)) - XCTAssertTrue(try gx5.compare(to: priv5.publicKey)) - XCTAssertTrue(try gx6.compare(to: priv6.publicKey)) + XCTAssertTrue(gx2.isEqual(to: priv2.publicKey)) + XCTAssertTrue(gx3.isEqual(to: priv3.publicKey)) + XCTAssertTrue(gx4.isEqual(to: priv4.publicKey)) + XCTAssertTrue(gx5.isEqual(to: priv5.publicKey)) + XCTAssertTrue(gx6.isEqual(to: priv6.publicKey)) } func testBasicAddition() throws { @@ -98,14 +98,14 @@ final class PublicKeyGroupOperationsTests: XCTestCase { // Test that we can add two points let sum = try a + b - let sumCompressed = try FFI.PublicKey.serialize(sum, format: .compressed) + let sumCompressed = FFI.PublicKey.serialize(sum, format: .compressed) // Verify the result is a valid point (33 bytes for compressed) XCTAssertEqual(sumCompressed.count, 33) // Verify the result is different from both inputs - XCTAssertFalse(try sum.compare(to: a)) - XCTAssertFalse(try sum.compare(to: b)) + XCTAssertFalse(sum.isEqual(to: a)) + XCTAssertFalse(sum.isEqual(to: b)) } func testNegation() throws { @@ -115,10 +115,10 @@ final class PublicKeyGroupOperationsTests: XCTestCase { let negNegA = try negA.negate() // Test that -(-a) = a - XCTAssertTrue(try negNegA.compare(to: a)) - + XCTAssertTrue(negNegA.isEqual(to: a)) + // Test that a != -a (unless a is the point at infinity, which gx2 is not) - XCTAssertFalse(try a.compare(to: negA)) + XCTAssertFalse(a.isEqual(to: negA)) } func testSubtraction() throws { @@ -129,12 +129,12 @@ final class PublicKeyGroupOperationsTests: XCTestCase { // Test that g5 - g3 = g2 let g5MinusG3 = try g5 - g3 - XCTAssertTrue(try g5MinusG3.compare(to: g2), "g5 - g3 should equal g2") - + XCTAssertTrue(g5MinusG3.isEqual(to: g2), "g5 - g3 should equal g2") + // Test that g3 - g2 = g let g3MinusG2 = try g3 - g2 - XCTAssertTrue(try g3MinusG2.compare(to: FFI.PublicKey.Wrapped.g), "g3 - g2 should equal g") - + XCTAssertTrue(g3MinusG2.isEqual(to: FFI.PublicKey.Wrapped.g), "g3 - g2 should equal g") + // Test that g2 - g2 throws error (point at infinity) XCTAssertThrowsError(try g2 - g2) { error in if let ffiError = error as? FFI.Error { @@ -152,12 +152,12 @@ final class PublicKeyGroupOperationsTests: XCTestCase { // Test sum([g, g2, g3]) = g6 let sum = try FFI.PublicKey.Wrapped.sum(keys: [g, g2, g3]) - XCTAssertTrue(try sum.compare(to: g6), "sum([g, g2, g3]) should equal g6") - + XCTAssertTrue(sum.isEqual(to: g6), "sum([g, g2, g3]) should equal g6") + // Test sum with single key let sumSingle = try FFI.PublicKey.Wrapped.sum(keys: [g3]) - XCTAssertTrue(try sumSingle.compare(to: g3), "sum([g3]) should equal g3") - + XCTAssertTrue(sumSingle.isEqual(to: g3), "sum([g3]) should equal g3") + // Test that empty array throws error XCTAssertThrowsError(try FFI.PublicKey.Wrapped.sum(keys: [])) { error in if let k1Error = error as? K1.Error { @@ -171,7 +171,7 @@ final class PublicKeyGroupOperationsTests: XCTestCase { let g = FFI.PublicKey.Wrapped.g let gx2 = FFI.PublicKey.Wrapped.gx2 let gPlusG = try g + g - XCTAssertTrue(try gPlusG.compare(to: gx2), "g + g should equal gx2 (2*G) on secp256k1") + XCTAssertTrue(gPlusG.isEqual(to: gx2), "g + g should equal gx2 (2*G) on secp256k1") } func testGroupAdditionWithG2G3G4() throws { @@ -182,11 +182,11 @@ final class PublicKeyGroupOperationsTests: XCTestCase { // g2 + g3 (combine) should equal g5 (scalar multiplication) let g2PlusG3 = try g2 + g3 - XCTAssertTrue(try g2PlusG3.compare(to: g5), "g2 + g3 (combine) should equal g5 (scalar multiplication)") + XCTAssertTrue(g2PlusG3.isEqual(to: g5), "g2 + g3 (combine) should equal g5 (scalar multiplication)") // Also test that sum([g2, g3]) == g2 + g3 let sum1 = try FFI.PublicKey.Wrapped.sum(keys: [g2, g3]) - XCTAssertTrue(try sum1.compare(to: g2PlusG3)) + XCTAssertTrue(sum1.isEqual(to: g2PlusG3)) } func testGroupNegationWithG2G3G4() throws { @@ -194,15 +194,15 @@ final class PublicKeyGroupOperationsTests: XCTestCase { // Test: -g2 should be different from g2 let negG2 = try FFI.PublicKey.Wrapped.gx2.negate() - XCTAssertFalse(try negG2.compare(to: FFI.PublicKey.Wrapped.gx2)) - + XCTAssertFalse(negG2.isEqual(to: FFI.PublicKey.Wrapped.gx2)) + // Test: -g3 should be different from g3 let negG3 = try FFI.PublicKey.Wrapped.gx3.negate() - XCTAssertFalse(try negG3.compare(to: FFI.PublicKey.Wrapped.gx3)) - + XCTAssertFalse(negG3.isEqual(to: FFI.PublicKey.Wrapped.gx3)) + // Test: -g4 should be different from g4 let negG4 = try FFI.PublicKey.Wrapped.gx4.negate() - XCTAssertFalse(try negG4.compare(to: FFI.PublicKey.Wrapped.gx4)) + XCTAssertFalse(negG4.isEqual(to: FFI.PublicKey.Wrapped.gx4)) // Test: g2 + (-g2) = 0 (point at infinity) // This should throw an error because the point at infinity cannot be represented as a valid public key @@ -223,22 +223,31 @@ final class PublicKeyGroupOperationsTests: XCTestCase { let schnorrSum = try schnorrG2 + schnorrG3 // g2 + g3 should equal g5 - XCTAssertEqual(schnorrSum.compressedRepresentation, try FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx5, format: .compressed)) - + XCTAssertEqual( + schnorrSum.compressedRepresentation, + FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx5, format: .compressed) + ) + // Test ECDSA public keys let ecdsaG2 = try K1.ECDSA.PublicKey(compressedRepresentation: FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx2, format: .compressed)) let ecdsaG3 = try K1.ECDSA.PublicKey(compressedRepresentation: FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx3, format: .compressed)) let ecdsaSum = try ecdsaG2 + ecdsaG3 // g2 + g3 should equal g5 - XCTAssertEqual(ecdsaSum.compressedRepresentation, try FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx5, format: .compressed)) - + XCTAssertEqual( + ecdsaSum.compressedRepresentation, + FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx5, format: .compressed) + ) + // Test KeyAgreement public keys let keyAgreementG2 = try K1.KeyAgreement.PublicKey(compressedRepresentation: FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx2, format: .compressed)) let keyAgreementG3 = try K1.KeyAgreement.PublicKey(compressedRepresentation: FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx3, format: .compressed)) let keyAgreementSum = try keyAgreementG2 + keyAgreementG3 // g2 + g3 should equal g5 - XCTAssertEqual(keyAgreementSum.compressedRepresentation, try FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx5, format: .compressed)) + XCTAssertEqual( + keyAgreementSum.compressedRepresentation, + FFI.PublicKey.serialize(FFI.PublicKey.Wrapped.gx5, format: .compressed) + ) } } diff --git a/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift b/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift index 0fc167a..2e47013 100644 --- a/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift +++ b/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift @@ -49,7 +49,7 @@ final class ECDASignaturePublicKeyRecoveryTests: XCTestCase { let nonRecoverable = try K1.ECDSA.Signature(rawRepresentation: compactRecoverableSig.compact) - try XCTAssertEqual(nonRecoverable, recoverableSig.nonRecoverable()) + XCTAssertEqual(nonRecoverable, recoverableSig.nonRecoverable()) let nonRecovDer = nonRecoverable.derRepresentation let nonRecoveryDERHex = "3044022074b5efbb980029d7f07cc3fa119b1b95ff178887b919b60ef4f294e095e1f9ac0220566e3d0c0ee77fa15cd1a8bf3b26366908dfa42e5f0481c73f1a23a2816260f8" XCTAssertEqual(nonRecovDer.hex, nonRecoveryDERHex) diff --git a/justfile b/justfile index 3012035..6aaf861 100644 --- a/justfile +++ b/justfile @@ -33,7 +33,8 @@ bootstrap: dev: bootstrap init synthesize-interface: - xcrun swift-synthesize-interface -I Sources/secp256k1/include -module-name Secp256k1 -target arm64-apple-macos15 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.sdk + mkdir -p "{{ROOT_DIR}}/.build/clang-module-cache" + CLANG_MODULE_CACHE_PATH="{{ROOT_DIR}}/.build/clang-module-cache" xcrun swift-synthesize-interface -I Sources/secp256k1/include -module-name Secp256k1 -target arm64-apple-macos15 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX26.sdk format: From 38cd5785932e777f4eca151936463ab265be361a Mon Sep 17 00:00:00 2001 From: Alexander Cyon Date: Fri, 13 Mar 2026 13:57:56 +0100 Subject: [PATCH 21/21] [no ci] WIP --- .../ECDSA/ECDSASignatureNonRecoverable.swift | 6 +- Sources/K1/K1/Keys/Keys.generated.swift | 8 +- Sources/K1/K1/Keys/Keys.swift.gyb | 2 +- .../K1/K1/Keys/PublicKeyImplementation.swift | 4 +- .../ECDSA+NonRecovery+Wrapped.swift | 9 +- .../NonRecovery/FFI+ECDSA+NonRecovery.swift | 4 +- .../Recovery/ECDSA+Recovery+Wrapped.swift | 9 +- .../Keys/PublicKey/PublicKey+Wrapped.swift | 2 +- .../K1/Support/FFI/Internals/FFI+Raw.swift | 29 +- Sources/K1Macros/DeclareSafeApi.swift | 84 +++++ Sources/K1Macros/MakeSafe.swift | 2 - .../K1MacrosImpl/DeclareSafeApiMacro.swift | 328 ++++++++++++++++++ Sources/K1MacrosImpl/K1MacrosPlugin.swift | 2 +- Sources/K1MacrosImpl/MakeSafeMacro.swift | 93 ----- Tests/ApinotesTests/ApinotesTests.swift | 73 ---- .../PublicKeyGroupOperationsTests.swift | 12 +- .../ECDASignaturePublicKeyRecoveryTests.swift | 3 + 17 files changed, 459 insertions(+), 211 deletions(-) create mode 100644 Sources/K1Macros/DeclareSafeApi.swift delete mode 100644 Sources/K1Macros/MakeSafe.swift create mode 100644 Sources/K1MacrosImpl/DeclareSafeApiMacro.swift delete mode 100644 Sources/K1MacrosImpl/MakeSafeMacro.swift delete mode 100644 Tests/ApinotesTests/ApinotesTests.swift diff --git a/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift b/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift index 7f34046..4be4c3b 100644 --- a/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift +++ b/Sources/K1/K1/ECDSA/ECDSASignatureNonRecoverable.swift @@ -76,11 +76,7 @@ extension K1.ECDSA.Signature { /// Returns 64 bytes on format: `R || S`, as defined in [rfc4754][rfc]. In /// `libsecp256k1` this representation is called "compact". public var rawRepresentation: Data { - do { - return try FFI.ECDSA.compact(wrapped) - } catch { - fatalError("Should never fail to convert ECDSA signatures to rawRepresentation.") - } + FFI.ECDSA.compact(wrapped) } /// A Distinguished Encoding Rules (DER) encoded representation of a diff --git a/Sources/K1/K1/Keys/Keys.generated.swift b/Sources/K1/K1/Keys/Keys.generated.swift index e9b04c1..87cc432 100644 --- a/Sources/K1/K1/Keys/Keys.generated.swift +++ b/Sources/K1/K1/Keys/Keys.generated.swift @@ -219,7 +219,7 @@ extension K1.KeyAgreement { /// Negates a public key (point) on the secp256k1 curve public func negate() throws -> Self { - try Self(impl: impl.negate()) + Self(impl: impl.negate()) } /// Combines multiple public keys (points) on the secp256k1 curve @@ -445,7 +445,7 @@ extension K1.Schnorr { /// Negates a public key (point) on the secp256k1 curve public func negate() throws -> Self { - try Self(impl: impl.negate()) + Self(impl: impl.negate()) } /// Combines multiple public keys (points) on the secp256k1 curve @@ -671,7 +671,7 @@ extension K1.ECDSA { /// Negates a public key (point) on the secp256k1 curve public func negate() throws -> Self { - try Self(impl: impl.negate()) + Self(impl: impl.negate()) } /// Combines multiple public keys (points) on the secp256k1 curve @@ -897,7 +897,7 @@ extension K1.ECDSAWithKeyRecovery { /// Negates a public key (point) on the secp256k1 curve public func negate() throws -> Self { - try Self(impl: impl.negate()) + Self(impl: impl.negate()) } /// Combines multiple public keys (points) on the secp256k1 curve diff --git a/Sources/K1/K1/Keys/Keys.swift.gyb b/Sources/K1/K1/Keys/Keys.swift.gyb index 3c8ed89..83705ff 100644 --- a/Sources/K1/K1/Keys/Keys.swift.gyb +++ b/Sources/K1/K1/Keys/Keys.swift.gyb @@ -263,7 +263,7 @@ extension K1.${FEATURE} { /// Negates a public key (point) on the secp256k1 curve public func negate() throws -> Self { - try Self(impl: impl.negate()) + Self(impl: impl.negate()) } /// Combines multiple public keys (points) on the secp256k1 curve diff --git a/Sources/K1/K1/Keys/PublicKeyImplementation.swift b/Sources/K1/K1/Keys/PublicKeyImplementation.swift index 0cbfe95..867f1f5 100644 --- a/Sources/K1/K1/Keys/PublicKeyImplementation.swift +++ b/Sources/K1/K1/Keys/PublicKeyImplementation.swift @@ -143,8 +143,8 @@ extension K1._PublicKeyImplementation { } /// Negates a public key (point) on the secp256k1 curve - func negate() throws -> Self { - try Self(wrapped: wrapped.negate()) + func negate() -> Self { + Self(wrapped: wrapped.negate()) } /// Combines multiple public keys (points) on the secp256k1 curve diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift index 1d4eda4..bc89925 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/ECDSA+NonRecovery+Wrapped.swift @@ -17,7 +17,14 @@ extension FFI.ECDSA { extension FFI.ECDSA.Wrapped { // swiftlint:disable:next line_length static func sign() -> ECDSAFunctionPointer { - ecdsaSignNonRecoverable(context:outputSignature:hashedMessageBytes: privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:) + ecdsaSignNonRecoverable( + context: + outputSignature: + hashedMessageBytes: + privateKeyBytes: + nonceFunctionPointer: + arbitraryNonceData: + ) } } diff --git a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift index b99a55c..687dc76 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/NonRecovery/FFI+ECDSA+NonRecovery.swift @@ -43,7 +43,7 @@ extension FFI.ECDSA { // MARK: Serialize extension FFI.ECDSA { - static func compact(_ wrapped: Wrapped) throws -> Data { + static func compact(_ wrapped: Wrapped) -> Data { var out = [UInt8](repeating: 0, count: Self.byteCount) var rawSignature = wrapped.raw FFI.call { context in @@ -84,7 +84,7 @@ extension FFI.ECDSA { guard message.count == Curve.Field.byteCount else { throw K1.Error.incorrectParameterSize } - let nonRecoverableCompact = try FFI.ECDSA.compact(wrapped) + let nonRecoverableCompact = FFI.ECDSA.compact(wrapped) return try Self.recoverPublicKey( nonRecoverableCompact: nonRecoverableCompact, recoveryID: recoveryID, diff --git a/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift b/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift index c555a18..b01d835 100644 --- a/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/ECDSA/Recovery/ECDSA+Recovery+Wrapped.swift @@ -26,7 +26,14 @@ typealias ECDSAFunctionPointer = ( extension FFI.ECDSAWithKeyRecovery.Wrapped { // swiftlint:disable:next line_length static func sign() -> ECDSAFunctionPointer { - ecdsaSignRecoverable(context:outputSignature:hashedMessageBytes: privateKeyBytes:nonceFunctionPointer:arbitraryNonceData:) + ecdsaSignRecoverable( + context: + outputSignature: + hashedMessageBytes: + privateKeyBytes: + nonceFunctionPointer: + arbitraryNonceData: + ) } } diff --git a/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift index ba74a37..4417a47 100644 --- a/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift +++ b/Sources/K1/Support/FFI/API/Keys/PublicKey/PublicKey+Wrapped.swift @@ -61,7 +61,7 @@ extension FFI.PublicKey.Wrapped { } /// Negates a public key (point) on the secp256k1 curve - func negate() throws -> Self { + func negate() -> Self { var result = self.raw FFI.call { context in negatePublicKey(context: context, publicKey: &result) diff --git a/Sources/K1/Support/FFI/Internals/FFI+Raw.swift b/Sources/K1/Support/FFI/Internals/FFI+Raw.swift index 4f8bb9b..1d8466b 100644 --- a/Sources/K1/Support/FFI/Internals/FFI+Raw.swift +++ b/Sources/K1/Support/FFI/Internals/FFI+Raw.swift @@ -1,4 +1,5 @@ import Foundation +import K1Macros import Secp256k1 // MARK: - Raw @@ -22,27 +23,14 @@ extension Raw { } return raw } +} - @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) - static func nonRecoverableSignature( - compactBytes array: InlineArray<64, UInt8> - ) throws -> ECDSASignatureRaw { - var raw = ECDSASignatureRaw() - // FIXME: Declare a `parseEcdsaSignatureCompact` taking an InlineArray<64, UInt8> and use it directly - try array.span.withUnsafeBufferPointer { compactBytes in - try FFI.call(ifFailThrow: .ecdsaSignatureParseCompact) { context in - parseEcdsaSignatureCompact( - context: context, - outputSignature: &raw, - inputBytes: compactBytes - ) - } - } - return raw - } +// MARK: NonRecoverable Compact +extension Raw { - static func nonRecoverableSignature( - compactBytes: [UInt8] + @declareSafeApi(byteCount: 64) + static func __nonRecoverableSignature( + compactBytes: UnsafePointer ) throws -> ECDSASignatureRaw { var raw = ECDSASignatureRaw() @@ -56,7 +44,10 @@ extension Raw { return raw } +} +// MARK: NonRecoverable DER +extension Raw { static func nonRecoverableSignature( derBytes: Span ) throws -> ECDSASignatureRaw { diff --git a/Sources/K1Macros/DeclareSafeApi.swift b/Sources/K1Macros/DeclareSafeApi.swift new file mode 100644 index 0000000..22e1eee --- /dev/null +++ b/Sources/K1Macros/DeclareSafeApi.swift @@ -0,0 +1,84 @@ +/// # Usage +/// Attach to a (meant to be private) function which accepts `UnsafePointer` in order to +/// generate four functions instead accepting: +/// * `InlineArray` +/// * `Span` +/// * `Data` +/// * `[UInt8]` +/// +/// ```swift +/// @declareSafeApi(byteCount: 64) +/// static func __nonRecoverableSignature( +/// compactBytes: UnsafePointer +/// ) throws -> ECDSASignatureRaw { ... } +/// ``` +/// +/// # Example +/// ```swift +/// @declareSafeApi(byteCount: 64) +/// static func __nonRecoverableSignature( +/// compactBytes: UnsafePointer +/// ) throws -> ECDSASignatureRaw { +/// var raw = ECDSASignatureRaw() +/// +/// try FFI.call(ifFailThrow: .ecdsaSignatureParseCompact) { context in +/// parseEcdsaSignatureCompact( +/// context: context, +/// outputSignature: &raw, +/// inputBytes: compactBytes +/// ) +/// } +/// +/// return raw +/// } +/// ``` +/// +/// This macro will produce one private helper and four methods meant for +/// public/internal API: +/// +/// ```swift +/// @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) +/// static func nonRecoverableSignature( +/// compactBytes: InlineArray<64, UInt8> +/// ) throws -> ECDSASignatureRaw { +/// try Self.nonRecoverableSignature(compactBytes: compactBytes.span) +/// } +/// +/// @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) +/// static func nonRecoverableSignature( +/// compactBytes: Span +/// ) throws -> ECDSASignatureRaw { +/// try compactBytes.withUnsafeBytes { pointer in +/// try Self._nonRecoverableSignature(compactBytes: pointer) +/// } +/// } +/// +/// static func nonRecoverableSignature( +/// compactBytes: [UInt8] +/// ) throws -> ECDSASignatureRaw { +/// try Self._nonRecoverableSignature(compactBytes: compactBytes) +/// } +/// +/// static func nonRecoverableSignature( +/// compactBytes: Data +/// ) throws -> ECDSASignatureRaw { +/// try Self._nonRecoverableSignature(compactBytes: compactBytes) +/// } +/// +/// private static func _nonRecoverableSignature( +/// compactBytes: some ContiguousBytes +/// ) throws -> ECDSASignatureRaw { +/// try compactBytes.withUnsafeBytes { pointer in +/// guard pointer.count == 64 else { +/// throw K1.Error.incorrectParameterSize +/// } +/// let bytesPointer = pointer.bindMemory(to: UInt8.self) +/// guard let base = bytesPointer.baseAddress else { +/// throw K1.Error.invalidParameter +/// } +/// return try __nonRecoverableSignature(compactBytes: base) +/// } +/// } +/// ``` +@attached(peer, names: arbitrary) +public macro declareSafeApi(byteCount: Int) = #externalMacro(module: "K1MacrosImpl", type: "DeclareSafeApiMacro") diff --git a/Sources/K1Macros/MakeSafe.swift b/Sources/K1Macros/MakeSafe.swift deleted file mode 100644 index ac6ca4e..0000000 --- a/Sources/K1Macros/MakeSafe.swift +++ /dev/null @@ -1,2 +0,0 @@ -@freestanding(declaration, names: arbitrary) -public macro makeSafe(_ safeName: Any, _ unsafeFn: Any, _ count: Int) = #externalMacro(module: "K1MacrosImpl", type: "MakeSafeMacro") diff --git a/Sources/K1MacrosImpl/DeclareSafeApiMacro.swift b/Sources/K1MacrosImpl/DeclareSafeApiMacro.swift new file mode 100644 index 0000000..970fe3b --- /dev/null +++ b/Sources/K1MacrosImpl/DeclareSafeApiMacro.swift @@ -0,0 +1,328 @@ +import Foundation +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct DeclareSafeApiMacro: PeerMacro { + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else { + throw DeclareSafeApiMacroError.message("@declareSafeApi can only be attached to a function.") + } + + let byteCount = try parseByteCount(from: node) + guard byteCount > 0 else { + throw DeclareSafeApiMacroError.message("byteCount must be >= 1.") + } + + if funcDecl.signature.effectSpecifiers?.asyncSpecifier != nil { + throw DeclareSafeApiMacroError.message("@declareSafeApi does not support async functions.") + } + + guard let body = funcDecl.body else { + throw DeclareSafeApiMacroError.message("@declareSafeApi requires a function body.") + } + + let parameters = funcDecl.signature.parameterClause.parameters + guard parameters.count == 1, let param = parameters.first else { + throw DeclareSafeApiMacroError.message("@declareSafeApi currently supports exactly one parameter.") + } + + guard isUnsafePointerToUInt8(param.type) else { + throw DeclareSafeApiMacroError.message( + "Parameter must be UnsafePointer." + ) + } + + let originalName = funcDecl.name.text + let hasUnsafePrefix = originalName.hasPrefix("__") + let publicName = hasUnsafePrefix ? String(originalName.dropFirst(2)) : originalName + let unsafeName = hasUnsafePrefix ? originalName : "__\(originalName)" + let internalName = param.secondName?.text ?? (param.firstName.text == "_" ? "bytes" : param.firstName.text) + let paramDecl = parameterNameClause(firstName: param.firstName.text, secondName: param.secondName?.text, fallback: internalName) + let callArgString = callArgument(label: param.firstName.text, expr: internalName) + + let effectText = trimmed(funcDecl.signature.effectSpecifiers?.throwsClause?.description ?? "") + let returnText = trimmed(funcDecl.signature.returnClause?.description ?? "") + let signatureSuffix = [effectText, returnText].filter { !$0.isEmpty }.joined(separator: " ") + let signatureSuffixWithSpace = signatureSuffix.isEmpty ? "" : " \(signatureSuffix)" + let hasReturn = funcDecl.signature.returnClause != nil + + let tryPrefix = effectText.isEmpty ? "" : "try " + let returnPrefix = hasReturn ? "return " : "" + + let accessModifier = firstAccessModifier(in: funcDecl.modifiers) + if accessModifier == "private" || accessModifier == "fileprivate" { + throw DeclareSafeApiMacroError.message( + "@declareSafeApi cannot be applied to private/fileprivate functions. Use internal/public/open so the generated safe API is visible." + ) + } + let nonAccessModifiers = nonAccessModifiers(in: funcDecl.modifiers) + let wrapperModifiers = modifiersText(access: accessModifier, others: nonAccessModifiers) + let privateModifiers = modifiersText(access: "private", others: nonAccessModifiers) + + let filteredAttributes = AttributeListSyntax( + funcDecl.attributes.filter { element in + guard case .attribute(let attr) = element else { return true } + let nameText = trimmed(attr.attributeName.description) + let simpleName = nameText.split(separator: ".").last.map(String.init) ?? nameText + return simpleName != "declareSafeApi" + }.map { $0 } + ) + let attributesText = trimmed(filteredAttributes.description) + + var unsafeDecl: DeclSyntax? + if !hasUnsafePrefix { + let unsafeSignature = trimmed(funcDecl.signature.description) + let unsafeDeclText = """ + \(attributesText.isEmpty ? "" : attributesText + "\n")\ + \(privateModifiers)func \(unsafeName)\(unsafeSignature) \(body.description) + """ + unsafeDecl = "\(raw: unsafeDeclText)" + } + + let helperBodyLine = callLine( + expr: "\(unsafeName)(\(callArgument(label: param.firstName.text, expr: "base")))", + tryPrefix: tryPrefix, + hasReturn: hasReturn + ) + let helperBody = """ + \(returnPrefix)\(tryPrefix)\(internalName).withUnsafeBytes { pointer in + \tguard pointer.count == \(byteCount) else { + \t\tthrow K1.Error.incorrectParameterSize + \t} + \tlet bytesPointer = pointer.bindMemory(to: UInt8.self) + \tguard let base = bytesPointer.baseAddress else { + \t\tthrow K1.Error.invalidParameter + \t} + \t\(helperBodyLine) + } + """ + + let helperDecl: DeclSyntax = """ + \(raw: privateModifiers)func _\(raw: publicName)( + \(raw: paramDecl): some ContiguousBytes + )\(raw: signatureSuffixWithSpace) { + \(raw: indent(helperBody, by: 1)) + } + """ + + let availableAttribute = + "@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *)" + + let inlineArrayBodyLine = callLine( + expr: "\(publicName)(\(callArgument(label: param.firstName.text, expr: "\(internalName).span")))", + tryPrefix: tryPrefix, + hasReturn: hasReturn + ) + let byteCountText = String(byteCount) + let inlineArrayDecl: DeclSyntax = """ + \(raw: availableAttribute) + \(raw: wrapperModifiers)func \(raw: publicName)( + \(raw: paramDecl): InlineArray<\(raw: byteCountText), UInt8> + )\(raw: signatureSuffixWithSpace) { + \t\(raw: inlineArrayBodyLine) + } + """ + + let spanBodyLine = callLine( + expr: "_\(publicName)(\(callArgument(label: param.firstName.text, expr: "pointer")))", + tryPrefix: tryPrefix, + hasReturn: hasReturn + ) + let spanCallLine = """ + \(returnPrefix)\(tryPrefix)\(internalName).withUnsafeBytes { pointer in + \t\(spanBodyLine) + } + """ + let spanDecl: DeclSyntax = """ + \(raw: availableAttribute) + \(raw: wrapperModifiers)func \(raw: publicName)( + \(raw: paramDecl): Span + )\(raw: signatureSuffixWithSpace) { + \(raw: indent(spanCallLine, by: 1)) + } + """ + + let arrayBodyLine = callLine( + expr: "_\(publicName)(\(callArgString))", + tryPrefix: tryPrefix, + hasReturn: hasReturn + ) + let arrayDecl: DeclSyntax = """ + \(raw: wrapperModifiers)func \(raw: publicName)( + \(raw: paramDecl): [UInt8] + )\(raw: signatureSuffixWithSpace) { + \t\(raw: arrayBodyLine) + } + """ + + let dataBodyLine = callLine( + expr: "_\(publicName)(\(callArgString))", + tryPrefix: tryPrefix, + hasReturn: hasReturn + ) + let dataDecl: DeclSyntax = """ + \(raw: wrapperModifiers)func \(raw: publicName)( + \(raw: paramDecl): Data + )\(raw: signatureSuffixWithSpace) { + \t\(raw: dataBodyLine) + } + """ + + var decls: [DeclSyntax] = [] + if let unsafeDecl { decls.append(unsafeDecl) } + decls.append(contentsOf: [ + helperDecl, + inlineArrayDecl, + spanDecl, + arrayDecl, + dataDecl, + ]) + return decls + } +} + +// MARK: - Parsing helpers +private func parseByteCount(from node: AttributeSyntax) throws -> Int { + guard case .argumentList(let args) = node.arguments else { + throw DeclareSafeApiMacroError.message("Expected arguments: @declareSafeApi(byteCount: 64)") + } + + guard let byteArg = args.first(where: { $0.label?.text == "byteCount" }) else { + throw DeclareSafeApiMacroError.message("Missing byteCount: argument.") + } + + guard let intLit = byteArg.expression.as(IntegerLiteralExprSyntax.self), + let value = Int(intLit.literal.text) + else { + throw DeclareSafeApiMacroError.message("byteCount must be an integer literal.") + } + + return value +} + +private func isUnsafePointerToUInt8(_ type: TypeSyntax) -> Bool { + let baseType: TypeSyntax + if let opt = type.as(OptionalTypeSyntax.self) { + baseType = opt.wrappedType + } else if let opt = type.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) { + baseType = opt.wrappedType + } else { + baseType = type + } + + if let ident = baseType.as(IdentifierTypeSyntax.self), + ident.name.text == "UnsafePointer", + isUInt8Generic(ident.genericArgumentClause) + { + return true + } + + if let member = baseType.as(MemberTypeSyntax.self), + member.name.text == "UnsafePointer", + isUInt8Generic(member.genericArgumentClause) + { + return true + } + + return false +} + +private func isUInt8Generic(_ clause: GenericArgumentClauseSyntax?) -> Bool { + guard let clause, clause.arguments.count == 1, + let arg = clause.arguments.first + else { return false } + + switch arg.argument { + case .type(let typeArg): + if let ident = typeArg.as(IdentifierTypeSyntax.self), ident.name.text == "UInt8" { + return true + } + if let member = typeArg.as(MemberTypeSyntax.self), member.name.text == "UInt8" { + return true + } + return false + case .expr: + return false + } +} + +private func parameterNameClause(firstName: String, secondName: String?, fallback: String) -> String { + if let secondName { + return "\(firstName) \(secondName)" + } + if firstName == "_" { + return "_ \(fallback)" + } + return firstName +} + +private func callArgument(label: String, expr: String) -> String { + label == "_" ? expr : "\(label): \(expr)" +} + +private func modifiersText(access: String?, others: [String]) -> String { + var parts: [String] = [] + if let access, !access.isEmpty { parts.append(access) } + parts.append(contentsOf: others.filter { !$0.isEmpty }) + if parts.isEmpty { return "" } + return parts.joined(separator: " ") + " " +} + +private func firstAccessModifier(in modifiers: DeclModifierListSyntax) -> String? { + for modifier in modifiers { + let text = trimmed(modifier.name.text) + if isAccessModifier(text) { + return text + } + } + return nil +} + +private func nonAccessModifiers(in modifiers: DeclModifierListSyntax) -> [String] { + modifiers.compactMap { modifier in + let text = trimmed(modifier.description) + return isAccessModifier(text) ? nil : text + } +} + +private func isAccessModifier(_ text: String) -> Bool { + switch text { + case "public", "internal", "fileprivate", "private", "open": + return true + default: + return false + } +} + +private func callLine(expr: String, tryPrefix: String, hasReturn: Bool) -> String { + let call = "\(tryPrefix)\(expr)" + return hasReturn ? "return \(call)" : call +} + +private func trimmed(_ string: String) -> String { + string.trimmingCharacters(in: .whitespacesAndNewlines) +} + +private func indent(_ text: String, by level: Int) -> String { + let prefix = String(repeating: "\t", count: level) + return text + .split(separator: "\n", omittingEmptySubsequences: false) + .map { prefix + $0 } + .joined(separator: "\n") +} + +// MARK: - Errors +private enum DeclareSafeApiMacroError: Error, CustomStringConvertible { + case message(String) + + var description: String { + switch self { + case let .message(message): return message + } + } +} diff --git a/Sources/K1MacrosImpl/K1MacrosPlugin.swift b/Sources/K1MacrosImpl/K1MacrosPlugin.swift index 315dff4..0b904d7 100644 --- a/Sources/K1MacrosImpl/K1MacrosPlugin.swift +++ b/Sources/K1MacrosImpl/K1MacrosPlugin.swift @@ -3,5 +3,5 @@ import SwiftSyntaxMacros @main struct MyMacrosPlugin: CompilerPlugin { - let providingMacros: [Macro.Type] = [MakeSafeMacro.self] + let providingMacros: [Macro.Type] = [DeclareSafeApiMacro.self] } diff --git a/Sources/K1MacrosImpl/MakeSafeMacro.swift b/Sources/K1MacrosImpl/MakeSafeMacro.swift deleted file mode 100644 index 32cdf6e..0000000 --- a/Sources/K1MacrosImpl/MakeSafeMacro.swift +++ /dev/null @@ -1,93 +0,0 @@ -import SwiftSyntax -import SwiftSyntaxBuilder -import SwiftSyntaxMacros - -public struct MakeSafeMacro: DeclarationMacro { - public static func expansion( - of node: some FreestandingMacroExpansionSyntax, - in context: some MacroExpansionContext - ) throws -> [DeclSyntax] { - // ---- Parse args: (safeName, unsafeFn, count) ---- - let args = Array(node.arguments) - guard args.count == 3 else { - throw MakeSafeMacroError.message( - #"Expected 3 arguments: #makeSafe("safeName", unsafeFunction, count)"# - ) - } - - // 1) safeName: allows only "frobnicateThree" (must be String) - let safeNameExpr = args[0].expression - let safeName: String = try { - if let str = safeNameExpr.as(StringLiteralExprSyntax.self) { - // Accept single-segment string literal - guard - str.segments.count == 1, - let seg = str.segments.first?.as(StringSegmentSyntax.self) - else { - throw MakeSafeMacroError.message("safeName must be a simple string literal.") - } - return seg.content.text - } - if let ref = safeNameExpr.as(DeclReferenceExprSyntax.self) { - return ref.baseName.text - } - throw MakeSafeMacroError.message( - #"safeName must be a string literal or identifier, e.g. "frobnicateThree" or frobnicateThree"# - ) - }() - - // 2) unsafeFn: must be an identifier (e.g. frobnicateThreeUnsafe) - let unsafeExpr = args[1].expression - guard let unsafeRef = unsafeExpr.as(DeclReferenceExprSyntax.self) else { - throw MakeSafeMacroError.message( - #"unsafeFn must be an identifier, e.g. frobnicateThreeUnsafe"# - ) - } - let unsafeName = unsafeRef.baseName.text - - // 3) count: must be an integer literal - let countExpr = args[2].expression - guard let intLit = countExpr.as(IntegerLiteralExprSyntax.self) else { - throw MakeSafeMacroError.message(#"count must be an integer literal, e.g. 3"#) - } - let countText = intLit.literal.text - - // If count == 0, baseAddress! would be nil - if let countValue = Int(countText), countValue == 0 { - throw MakeSafeMacroError.message("count must be >= 1 for this wrapper (uses baseAddress!).") - } - - let decl: DeclSyntax = """ - @available(macOS 26.0, *) - func \(raw: safeName)( - into destination: inout InlineArray<\(raw: countText), UInt8>, - from source: InlineArray<\(raw: countText), UInt8> - ) { - var mutableSpan = destination.mutableSpan - mutableSpan.withUnsafeMutableBufferPointer { dst in - source.span.withUnsafeBufferPointer { src in - \(raw: unsafeName)( - into: dst.baseAddress!, - from: src.baseAddress! - ) - } - } - } - """ - - return [decl] - } -} - -// MARK: MakeSafeMacroError -enum MakeSafeMacroError: Error, CustomStringConvertible { - case message(String) -} - -extension MakeSafeMacroError { - var description: String { - switch self { - case let .message(message): return message - } - } -} diff --git a/Tests/ApinotesTests/ApinotesTests.swift b/Tests/ApinotesTests/ApinotesTests.swift deleted file mode 100644 index dbcfc2e..0000000 --- a/Tests/ApinotesTests/ApinotesTests.swift +++ /dev/null @@ -1,73 +0,0 @@ -import Testing -//import Secp256k1 -//import Foundation -//import K1Macros - -//struct Safe {} -//extension Safe { -// #makeSafe("cloneSpanOfLength3", cloneSpanOfLength3Unsafe, 3) -//} - - -//@Suite("Apinotes Tests") -//struct Test { -// @Test -// func testCase() { -// -//// expectAllEqual(to: 5) { (bytes: inout InlineArray<3, UInt8>) in -//// var span = bytes.mutableSpan -//// fillWithFives(span: &span) -//// } -// #expect(1 == 5 - 4) -// } -// -// @available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, *) -// @Test -// func `C fixed length`() { -// let source: InlineArray<3, UInt8> = [7, 7, 7] -// var destination: InlineArray<3, UInt8> = [0, 0, 0] -// -// Safe().cloneSpanOfLength3( -// into: &destination, -// from: source -// ) -// -// for index in 0..( -// to expectedElement: Element, -// defaultElement: Element, -// initBytes: (inout InlineArray) -> Void -//) -> Bool { -// var bytes: InlineArray = .init(repeating: defaultElement) -// initBytes(&bytes) -// for index in 0..( -// to expectedElement: Element, -// initBytes: @escaping (inout InlineArray) -> Void, -// file: StaticString = #filePath, -// line: UInt = #line -//) { -// #expect( -// allEqual( -// to: expectedElement, -// defaultElement: .zero, -// initBytes: initBytes -// ), -// "in file: \(file), line: \(line)" -// ) -//} diff --git a/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyGroupOperationsTests.swift b/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyGroupOperationsTests.swift index 7530eef..17e0109 100644 --- a/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyGroupOperationsTests.swift +++ b/Tests/K1Tests/TestCases/Keys/PublicKey/PublicKeyGroupOperationsTests.swift @@ -108,11 +108,11 @@ final class PublicKeyGroupOperationsTests: XCTestCase { XCTAssertFalse(sum.isEqual(to: b)) } - func testNegation() throws { + func testNegation() { // Test that negation works correctly let a = FFI.PublicKey.Wrapped.gx2 - let negA = try a.negate() - let negNegA = try negA.negate() + let negA = a.negate() + let negNegA = negA.negate() // Test that -(-a) = a XCTAssertTrue(negNegA.isEqual(to: a)) @@ -193,15 +193,15 @@ final class PublicKeyGroupOperationsTests: XCTestCase { // Test negation operations // Test: -g2 should be different from g2 - let negG2 = try FFI.PublicKey.Wrapped.gx2.negate() + let negG2 = FFI.PublicKey.Wrapped.gx2.negate() XCTAssertFalse(negG2.isEqual(to: FFI.PublicKey.Wrapped.gx2)) // Test: -g3 should be different from g3 - let negG3 = try FFI.PublicKey.Wrapped.gx3.negate() + let negG3 = FFI.PublicKey.Wrapped.gx3.negate() XCTAssertFalse(negG3.isEqual(to: FFI.PublicKey.Wrapped.gx3)) // Test: -g4 should be different from g4 - let negG4 = try FFI.PublicKey.Wrapped.gx4.negate() + let negG4 = FFI.PublicKey.Wrapped.gx4.negate() XCTAssertFalse(negG4.isEqual(to: FFI.PublicKey.Wrapped.gx4)) // Test: g2 + (-g2) = 0 (point at infinity) diff --git a/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift b/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift index 2e47013..87509e2 100644 --- a/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift +++ b/Tests/K1Tests/TestCases/PublicKeyRecovery/ECDASignaturePublicKeyRecoveryTests.swift @@ -1,6 +1,9 @@ import Foundation @testable import K1 import XCTest +import K1Macros +import Secp256k1 + // MARK: - ECDASignaturePublicKeyRecoveryTests /// Test vectors: