diff --git a/README.md b/README.md index d61f64c..1a79c75 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,16 @@ let publicKey = try! secp256k1.Recovery.PublicKey(messageData, signature: recove let signature = try! recoverySignature.normalize ``` +# Combine Public Keys + +```swift +let privateKey = try! secp256k1.Signing.PrivateKey() +let publicKey = try! secp256k1.Signing.PrivateKey().public + +// The Combine API arguments are an array of PublicKey objects and an optional format +publicKey.combine([privateKey.publicKey], format: .uncompressed) +``` + # Danger These APIs should not be considered stable and may change at any time. diff --git a/Sources/secp256k1/Combine.swift b/Sources/secp256k1/Combine.swift new file mode 120000 index 0000000..11fcbca --- /dev/null +++ b/Sources/secp256k1/Combine.swift @@ -0,0 +1 @@ +../zkp/Combine.swift \ No newline at end of file diff --git a/Sources/zkp/Combine.swift b/Sources/zkp/Combine.swift new file mode 100644 index 0000000..fd7bee1 --- /dev/null +++ b/Sources/zkp/Combine.swift @@ -0,0 +1,60 @@ +// +// Combine.swift +// GigaBitcoin/secp256k1.swift +// +// Copyright (c) 2023 GigaBitcoin LLC +// Distributed under the MIT software license +// +// See the accompanying file LICENSE for information +// + +import Foundation + +public extension secp256k1.Signing.PublicKey { + /// Create a new `PublicKey` by combining the current public key with an array of public keys. + /// - Parameters: + /// - pubkeys: the array of public key objects to be combined with + /// - format: the format of the combined `PublicKey` object + /// - Returns: combined `PublicKey` object + func combine(_ pubkeys: [Self], format: secp256k1.Format = .compressed) throws -> Self { + let context = secp256k1.Context.rawRepresentation + let allPubKeys = [self] + pubkeys + var pubKeyLen = format.length + var combinedKey = secp256k1_pubkey() + var combinedBytes = [UInt8](repeating: 0, count: pubKeyLen) + + guard withUnsafePointersToPubKeys(allPubKeys.map { $0.rawRepresentation }, { ptrsToCombine in + secp256k1_ec_pubkey_combine(context, &combinedKey, ptrsToCombine, ptrsToCombine.count).boolValue + }), secp256k1_ec_pubkey_serialize(context, &combinedBytes, &pubKeyLen, &combinedKey, format.rawValue).boolValue else { + throw secp256k1Error.underlyingCryptoError + } + + return try Self(dataRepresentation: combinedBytes, format: format) + } +} + +extension secp256k1.Signing.PublicKey { + /// Executes a closure with an array of `UnsafePointer?` for pointer operations on an array of `secp256k1_pubkey`. + /// - Parameters: + /// - pubKeys: An array of `secp256k1_pubkey` to be converted to `UnsafePointer?`. + /// - body: A closure that takes an array of `UnsafePointer?` and returns a result of type `Result`. + /// - Returns: The result of the closure of type `Result`. + func withUnsafePointersToPubKeys( + _ pubKeys: [secp256k1_pubkey], + _ body: ([UnsafePointer?]) -> Result + ) -> Result { + let pointers = pubKeys.map { pubKey -> UnsafePointer? in + let mutablePubKey = UnsafeMutablePointer.allocate(capacity: 1) + mutablePubKey.initialize(to: pubKey) + return UnsafePointer(mutablePubKey) + } + + defer { + for ptr in pointers { + ptr?.deallocate() + } + } + + return body(pointers) + } +} diff --git a/Sources/zkp/ECDH.swift b/Sources/zkp/ECDH.swift index 3eed786..e369aab 100644 --- a/Sources/zkp/ECDH.swift +++ b/Sources/zkp/ECDH.swift @@ -155,12 +155,11 @@ extension secp256k1.KeyAgreement.PrivateKey: DiffieHellmanKeyAgreement { format: secp256k1.Format = .compressed ) throws -> SharedSecret { let context = secp256k1.Context.rawRepresentation - var publicKey = secp256k1_pubkey() + var publicKey = publicKeyShare.rawRepresentation var sharedSecret = [UInt8](repeating: 0, count: format.length) var data = [UInt8](repeating: format == .compressed ? 1 : 0, count: 1) - guard secp256k1_ec_pubkey_parse(context, &publicKey, publicKeyShare.bytes, publicKeyShare.bytes.count).boolValue, - secp256k1_ecdh(context, &sharedSecret, &publicKey, baseKey.key.bytes, hashClosure(), &data).boolValue else { + guard secp256k1_ecdh(context, &sharedSecret, &publicKey, baseKey.key.bytes, hashClosure(), &data).boolValue else { throw secp256k1Error.underlyingCryptoError } diff --git a/Sources/zkp/ECDSA.swift b/Sources/zkp/ECDSA.swift index faaf587..d3716d9 100644 --- a/Sources/zkp/ECDSA.swift +++ b/Sources/zkp/ECDSA.swift @@ -207,12 +207,11 @@ extension secp256k1.Signing.PublicKey: DigestValidator { public func isValidSignature(_ signature: secp256k1.Signing.ECDSASignature, for digest: D) -> Bool { let context = secp256k1.Context.rawRepresentation var ecdsaSignature = secp256k1_ecdsa_signature() - var publicKey = secp256k1_pubkey() + var publicKey = rawRepresentation signature.dataRepresentation.copyToUnsafeMutableBytes(of: &ecdsaSignature.data) - return secp256k1_ec_pubkey_parse(context, &publicKey, bytes, bytes.count).boolValue && - secp256k1_ecdsa_verify(context, &ecdsaSignature, Array(digest), &publicKey).boolValue + return secp256k1_ecdsa_verify(context, &ecdsaSignature, Array(digest), &publicKey).boolValue } } diff --git a/Sources/zkp/Tweak.swift b/Sources/zkp/Tweak.swift index 5e177c1..8442d93 100644 --- a/Sources/zkp/Tweak.swift +++ b/Sources/zkp/Tweak.swift @@ -72,12 +72,11 @@ public extension secp256k1.Signing.PublicKey { /// - Returns: tweaked `PublicKey` object func add(_ tweak: [UInt8], format: secp256k1.Format = .compressed) throws -> Self { let context = secp256k1.Context.rawRepresentation - var pubKey = secp256k1_pubkey() + var pubKey = rawRepresentation var pubKeyLen = format.length var pubKeyBytes = [UInt8](repeating: 0, count: pubKeyLen) - guard secp256k1_ec_pubkey_parse(context, &pubKey, bytes, pubKeyLen).boolValue, - secp256k1_ec_pubkey_tweak_add(context, &pubKey, tweak).boolValue, + guard secp256k1_ec_pubkey_tweak_add(context, &pubKey, tweak).boolValue, secp256k1_ec_pubkey_serialize(context, &pubKeyBytes, &pubKeyLen, &pubKey, format.rawValue).boolValue else { throw secp256k1Error.underlyingCryptoError } @@ -92,12 +91,11 @@ public extension secp256k1.Signing.PublicKey { /// - Returns: tweaked `PublicKey` object func multiply(_ tweak: [UInt8], format: secp256k1.Format = .compressed) throws -> Self { let context = secp256k1.Context.rawRepresentation - var pubKey = secp256k1_pubkey() + var pubKey = rawRepresentation var pubKeyLen = format.length var pubKeyBytes = [UInt8](repeating: 0, count: pubKeyLen) - guard secp256k1_ec_pubkey_parse(context, &pubKey, bytes, pubKeyLen).boolValue, - secp256k1_ec_pubkey_tweak_mul(context, &pubKey, tweak).boolValue, + guard secp256k1_ec_pubkey_tweak_mul(context, &pubKey, tweak).boolValue, secp256k1_ec_pubkey_serialize(context, &pubKeyBytes, &pubKeyLen, &pubKey, format.rawValue).boolValue else { throw secp256k1Error.underlyingCryptoError } diff --git a/Sources/zkp/secp256k1.swift b/Sources/zkp/secp256k1.swift index dbe99e0..5d9b8e8 100644 --- a/Sources/zkp/secp256k1.swift +++ b/Sources/zkp/secp256k1.swift @@ -171,7 +171,7 @@ extension secp256k1 { /// A raw representation of the backing public key @usableFromInline var rawRepresentation: secp256k1_pubkey { var pubKey = secp256k1_pubkey() - dataRepresentation.copyToUnsafeMutableBytes(of: &pubKey.data) + _ = secp256k1_ec_pubkey_parse(secp256k1.Context.rawRepresentation, &pubKey, bytes, bytes.count) return pubKey } diff --git a/Tests/zkpTests/secp256k1Tests.swift b/Tests/zkpTests/secp256k1Tests.swift index ed61d49..09872c1 100644 --- a/Tests/zkpTests/secp256k1Tests.swift +++ b/Tests/zkpTests/secp256k1Tests.swift @@ -396,7 +396,7 @@ final class secp256k1Tests: XCTestCase { let set0 = Set(array) - array = [UInt8](repeating: 1, count: Int.random(in: 10...100_000)) + array = [UInt8](repeating: 1, count: Int.random(in: 10...100000)) XCTAssertGreaterThan(array.count, 9) @@ -601,6 +601,54 @@ final class secp256k1Tests: XCTestCase { XCTAssertEqual(outputKey.bytes, outputKeyBytes) } + func testPubkeyCombine() { + let publicKeyBytes1 = try! "021b4f0e9851971998e732078544c96b36c3d01cedf7caa332359d6f1d83567014".bytes + let publicKeyBytes2 = try! "0260303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752".bytes + + let publicKey1 = try! secp256k1.Signing.PublicKey(dataRepresentation: publicKeyBytes1, format: .compressed) + let publicKey2 = try! secp256k1.Signing.PublicKey(dataRepresentation: publicKeyBytes2, format: .compressed) + + let combinedPublicKey = try! publicKey1.combine([publicKey2]) + + // Define the expected combined key + let expectedCombinedKey = try! "03d6a3a9d62c7650fcac18f9ee68c7a004ebad71b7581b683062213ad9f37ddb28".bytes + + XCTAssertEqual(combinedPublicKey.dataRepresentation.bytes, expectedCombinedKey) + } + + func testPubkeyCombineBindings() { + // Initialize context + let context = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_NONE))! + + // Destroy context after execution + defer { secp256k1_context_destroy(context) } + + // Setup private and public key variables + var pubKeyLen = 33 + var cPubKey1 = secp256k1_pubkey() + var cPubKey2 = secp256k1_pubkey() + + let publicKeyBytes1 = try! "021b4f0e9851971998e732078544c96b36c3d01cedf7caa332359d6f1d83567014".bytes + let publicKeyBytes2 = try! "0260303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752".bytes + + // Verify the context and keys are setup correctly + XCTAssertEqual(secp256k1_ec_pubkey_parse(context, &cPubKey1, publicKeyBytes1, pubKeyLen), 1) + XCTAssertEqual(secp256k1_ec_pubkey_parse(context, &cPubKey2, publicKeyBytes2, pubKeyLen), 1) + + let pubKeys: [UnsafePointer?] = [UnsafePointer(&cPubKey1), UnsafePointer(&cPubKey2)] + var combinedKey = secp256k1_pubkey() + var combinedKeyBytes = [UInt8](repeating: 0, count: pubKeyLen) + + // Combine the two public keys + XCTAssertEqual(secp256k1_ec_pubkey_combine(context, &combinedKey, pubKeys, 2), 1) + XCTAssertEqual(secp256k1_ec_pubkey_serialize(context, &combinedKeyBytes, &pubKeyLen, &combinedKey, secp256k1.Format.compressed.rawValue), 1) + + // Define the expected combined key + let expectedCombinedKey = try! "03d6a3a9d62c7650fcac18f9ee68c7a004ebad71b7581b683062213ad9f37ddb28".bytes + + XCTAssertEqual(combinedKeyBytes, expectedCombinedKey) + } + static var allTests = [ ("testUncompressedKeypairCreation", testUncompressedKeypairCreation), ("testCompressedKeypairCreation", testCompressedKeypairCreation), @@ -637,6 +685,8 @@ final class secp256k1Tests: XCTestCase { ("testTapscript", testTapscript), ("testCompactSizePrefix", testCompactSizePrefix), ("testSchnorrNegating", testSchnorrNegating), - ("testTaprootDerivation", testTaprootDerivation) + ("testTaprootDerivation", testTaprootDerivation), + ("testPubkeyCombine", testPubkeyCombine), + ("testPubkeyCombineBindings", testPubkeyCombineBindings) ] }