From 9d628691842d50cf5c8b69aa3f9cb17d3afd55b0 Mon Sep 17 00:00:00 2001 From: Navid Rahimi Date: Thu, 3 Sep 2026 17:58:42 +0330 Subject: [PATCH] Spark: expose address ownership proofs --- lib/flutter_libsparkmobile.dart | 70 +++++++++++++++++++ ...ter_libsparkmobile_bindings_generated.dart | 25 +++++++ src/flutter_libsparkmobile.cpp | 64 +++++++++++++++++ src/flutter_libsparkmobile.h | 9 +++ src/structs.h | 6 ++ test/flutter_libsparkmobile_test.dart | 54 ++++++++++++++ 6 files changed, 228 insertions(+) diff --git a/lib/flutter_libsparkmobile.dart b/lib/flutter_libsparkmobile.dart index 9ec2f6e..25cfa8e 100644 --- a/lib/flutter_libsparkmobile.dart +++ b/lib/flutter_libsparkmobile.dart @@ -734,6 +734,76 @@ abstract final class LibSpark { } } + static String createSparkAddressOwnershipProof({ + required String message, + required String privateKeyHex, + required int spendKeyIndex, + required int diversifier, + }) { + final messageBytes = Uint8List.fromList(utf8.encode(message)); + if (messageBytes.length > 0x7fffffff) { + throw ArgumentError.value( + message, + 'message', + 'is too long', + ); + } + RangeError.checkValueInInterval( + spendKeyIndex, + 0, + 0x7fffffff, + 'spendKeyIndex', + ); + RangeError.checkValueInInterval(diversifier, 0, 0x7fffffff, 'diversifier'); + + final privateKeyBytes = privateKeyHex.to32BytesFromHex(); + final messagePtr = messageBytes.isEmpty + ? nullptr.cast() + : messageBytes.unsignedCharPointer(); + final privateKeyPtr = privateKeyBytes.unsignedCharPointer(); + + late Pointer result; + try { + result = native_createSparkAddressOwnershipProof( + messagePtr, + messageBytes.length, + privateKeyPtr, + spendKeyIndex, + diversifier, + ); + } finally { + if (messagePtr.address != nullptr.address) { + freeDart(messagePtr, debugName: 'messagePtr'); + } + freeDart(privateKeyPtr, debugName: 'privateKeyPtr'); + } + + if (result.address == nullptr.address) { + throw Exception('Internal memory allocation failed'); + } + + try { + if (result.ref.error.address != nullptr.address) { + final error = result.ref.error.cast().toDartString(); + freeNative(result.ref.error, debugName: 'result.ref.error'); + throw Exception(error); + } + if (result.ref.proof.address == nullptr.address || + result.ref.proofLength <= 0) { + throw Exception('Failed to create Spark address ownership proof'); + } + + return result.ref.proof + .toUint8List(result.ref.proofLength) + .toHexString(); + } finally { + if (result.ref.proof.address != nullptr.address) { + freeNative(result.ref.proof, debugName: 'result.ref.proof'); + } + freeNative(result, debugName: 'result'); + } + } + static List hashTags({required List base64Tags}) { DateTime? start; int? id; diff --git a/lib/src/flutter_libsparkmobile_bindings_generated.dart b/lib/src/flutter_libsparkmobile_bindings_generated.dart index 66dd4ff..e24afd3 100644 --- a/lib/src/flutter_libsparkmobile_bindings_generated.dart +++ b/lib/src/flutter_libsparkmobile_bindings_generated.dart @@ -243,6 +243,22 @@ external ffi.Pointer native_createSparkNameScript( int withoutProof, ); +@ffi.Native< + ffi.Pointer Function( + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Int, + ffi.Int)>(symbol: 'createSparkAddressOwnershipProof') +external ffi.Pointer +native_createSparkAddressOwnershipProof( + ffi.Pointer message, + int messageLength, + ffi.Pointer spendKeyData, + int spendKeyIndex, + int diversifier, +); + @ffi.Native< ffi.Pointer Function( ffi.Pointer, @@ -623,3 +639,12 @@ final class SparkNameScript extends ffi.Struct { @ffi.Int() external int size; } + +final class SparkAddressOwnershipProofResult extends ffi.Struct { + external ffi.Pointer proof; + + @ffi.Int() + external int proofLength; + + external ffi.Pointer error; +} diff --git a/src/flutter_libsparkmobile.cpp b/src/flutter_libsparkmobile.cpp index 1ee6e9e..6cf6b94 100644 --- a/src/flutter_libsparkmobile.cpp +++ b/src/flutter_libsparkmobile.cpp @@ -2,6 +2,7 @@ #include "utils.h" #include "deps/sparkmobile/include/spark.h" #include "deps/sparkmobile/src/sparkname.h" +#include "deps/sparkmobile/bitcoin/hash.h" #include "deps/sparkmobile/bitcoin/uint256.h" #include "structs.h" #include "transaction.h" @@ -50,6 +51,69 @@ const char* getAddressFromFullViewKey(void* fullViewKeyVoid, int index, int dive } } +FFI_PLUGIN_EXPORT +SparkAddressOwnershipProofResult* createSparkAddressOwnershipProof( + const unsigned char* message, + int messageLength, + unsigned char* spendKeyData, + int spendKeyIndex, + int diversifier +) { + SparkAddressOwnershipProofResult* result = + static_cast(malloc(sizeof(SparkAddressOwnershipProofResult))); + if (!result) return nullptr; + + result->proof = nullptr; + result->proofLength = 0; + result->error = nullptr; + + try { + if (messageLength < 0 || (messageLength > 0 && !message)) { + throw std::invalid_argument("Invalid message"); + } + if (!spendKeyData || spendKeyIndex < 0 || diversifier < 0) { + throw std::invalid_argument("Invalid Spark key parameters"); + } + + const std::string messageString = messageLength == 0 + ? std::string() + : std::string( + reinterpret_cast(message), + static_cast(messageLength)); + CHashWriter messageHash(SER_GETHASH, 0); + messageHash << std::string("Zcoin Signed Message:\n") << messageString; + + Scalar messageScalar; + messageScalar.SetHex(messageHash.GetHash().GetHex()); + + spark::SpendKey spendKey = createSpendKeyFromData(spendKeyData, spendKeyIndex); + spark::FullViewKey fullViewKey(spendKey); + spark::IncomingViewKey incomingViewKey(fullViewKey); + spark::Address address(incomingViewKey, static_cast(diversifier)); + + spark::OwnershipProof proof; + address.prove_own(messageScalar, spendKey, incomingViewKey, proof); + + CDataStream proofStream(SER_NETWORK, PROTOCOL_VERSION); + proofStream << proof; + + result->proofLength = static_cast(proofStream.size()); + result->proof = static_cast(malloc(proofStream.size())); + if (!result->proof) { + free(result); + return nullptr; + } + memcpy(result->proof, proofStream.data(), proofStream.size()); + } catch (const std::exception& e) { + result->error = static_cast(malloc(strlen(e.what()) + 1)); + if (result->error) { + strcpy(result->error, e.what()); + } + } + + return result; +} + /* * FFI-friendly wrapper for spark:identifyCoin. * diff --git a/src/flutter_libsparkmobile.h b/src/flutter_libsparkmobile.h index c45355b..826383d 100644 --- a/src/flutter_libsparkmobile.h +++ b/src/flutter_libsparkmobile.h @@ -148,6 +148,15 @@ struct SparkNameScript* createSparkNameScript( int withoutProof ); +FFI_PLUGIN_EXPORT +struct SparkAddressOwnershipProofResult* createSparkAddressOwnershipProof( + const unsigned char* message, + int messageLength, + unsigned char* spendKeyData, + int spendKeyIndex, + int diversifier +); + FFI_PLUGIN_EXPORT struct SparkNameCommitmentResult* cGetSparkNameCommitment( const unsigned char* serializedSparkNameData, diff --git a/src/structs.h b/src/structs.h index b69db75..09e8b0a 100644 --- a/src/structs.h +++ b/src/structs.h @@ -255,6 +255,12 @@ struct SparkNameScript { int size; }; +struct SparkAddressOwnershipProofResult { + unsigned char* proof; + int proofLength; + char* error; +}; + //#ifdef __cplusplus //} //#endif diff --git a/test/flutter_libsparkmobile_test.dart b/test/flutter_libsparkmobile_test.dart index e223298..cca5fb9 100644 --- a/test/flutter_libsparkmobile_test.dart +++ b/test/flutter_libsparkmobile_test.dart @@ -74,6 +74,60 @@ void main() { ); }); + test('Spark address ownership proof supports Core message lengths', () { + const privateKeyHex = + 'cb02b05c71a69080b083484f1cdf407677fac00ced6438df16925e2a29b4eebf'; + + for (final message in ['', List.filled(1025, 'a').join()]) { + final proof = LibSpark.createSparkAddressOwnershipProof( + message: message, + privateKeyHex: privateKeyHex, + spendKeyIndex: 1, + diversifier: 0, + ); + expect(proof, hasLength(260)); + expect(RegExp(r'^[0-9a-f]+$').hasMatch(proof), isTrue); + } + }); + + test('Spark address ownership proof checks native integer bounds', () { + const privateKeyHex = + 'cb02b05c71a69080b083484f1cdf407677fac00ced6438df16925e2a29b4eebf'; + expect( + () => LibSpark.createSparkAddressOwnershipProof( + message: 'challenge', + privateKeyHex: privateKeyHex, + spendKeyIndex: 0x80000000, + diversifier: 0, + ), + throwsRangeError, + ); + expect( + () => LibSpark.createSparkAddressOwnershipProof( + message: 'challenge', + privateKeyHex: privateKeyHex, + spendKeyIndex: 1, + diversifier: 0x80000000, + ), + throwsRangeError, + ); + }); + + test('Spark address ownership proof is canonical hex', () { + const privateKeyHex = + 'cb02b05c71a69080b083484f1cdf407677fac00ced6438df16925e2a29b4eebf'; + + final proof = LibSpark.createSparkAddressOwnershipProof( + message: 'Spark Name ownership challenge', + privateKeyHex: privateKeyHex, + spendKeyIndex: 1, + diversifier: 0, + ); + + expect(proof, hasLength(260)); + expect(RegExp(r'^[0-9a-f]+$').hasMatch(proof), isTrue); + }); + test('mnemonic to address test', () async { // Generate key data from the mnemonic. //