diff --git a/lib/flutter_libsparkmobile.dart b/lib/flutter_libsparkmobile.dart index 25cfa8e..ab24087 100644 --- a/lib/flutter_libsparkmobile.dart +++ b/lib/flutter_libsparkmobile.dart @@ -78,7 +78,8 @@ abstract final class LibSpark { start = DateTime.now(); String function = StackTrace.current.functionName; if (enableTraceLogging) { - function += "(privateKey=REDACTED," + function += + "(privateKey=REDACTED," "index=$index," "diversifier=$diversifier," "isTestNet=$isTestNet)"; @@ -224,8 +225,9 @@ abstract final class LibSpark { /// Returns a list of spark mint recipients /// static List< - ({Uint8List scriptPubKey, int amount, bool subtractFeeFromAmount})> - createSparkMintRecipients({ + ({Uint8List scriptPubKey, int amount, bool subtractFeeFromAmount}) + > + createSparkMintRecipients({ required List<({String sparkAddress, int value, String memo})> outputs, required Uint8List serialContext, bool generate = false, @@ -238,7 +240,8 @@ abstract final class LibSpark { start = DateTime.now(); String function = StackTrace.current.functionName; if (enableTraceLogging) { - function += "(outputs=$outputs," + function += + "(outputs=$outputs," "serialContext=$serialContext," "generate=$generate)"; } @@ -257,8 +260,9 @@ abstract final class LibSpark { for (int i = 0; i < outputs.length; i++) { outputsPtr[i].value = outputs[i].value; - outputsPtr[i].address = - outputs[i].sparkAddress.toNativeUtf8().cast(); + outputsPtr[i].address = outputs[i].sparkAddress + .toNativeUtf8() + .cast(); outputsPtr[i].memo = outputs[i].memo.toNativeUtf8().cast(); } @@ -282,11 +286,9 @@ abstract final class LibSpark { } final List< - ({ - Uint8List scriptPubKey, - int amount, - bool subtractFeeFromAmount - })> ret = []; + ({Uint8List scriptPubKey, int amount, bool subtractFeeFromAmount}) + > + ret = []; for (int i = 0; i < result.ref.length; i++) { final d = result.ref.list[i]; @@ -327,40 +329,46 @@ abstract final class LibSpark { List outputScripts, int fee, List< - ({ - String serializedCoin, - String serializedCoinContext, - int groupId, - int height, - })> usedCoins, - }) createSparkSendTransaction({ + ({ + String serializedCoin, + String serializedCoinContext, + int groupId, + int height, + }) + > + usedCoins, + }) + createSparkSendTransaction({ required String privateKeyHex, int index = 1, required List<({String address, int amount, bool subtractFeeFromAmount})> - recipients, + recipients, required List< - ({ - String sparkAddress, - int amount, - bool subtractFeeFromAmount, - String memo, - })> - privateRecipients, + ({ + String sparkAddress, + int amount, + bool subtractFeeFromAmount, + String memo, + }) + > + privateRecipients, required List< - ({ - String serializedCoin, - String serializedCoinContext, - int groupId, - int height, - })> - serializedCoins, + ({ + String serializedCoin, + String serializedCoinContext, + int groupId, + int height, + }) + > + serializedCoins, required List< - ({ - int setId, - String setHash, - List<({String serializedCoin, String txHash})> set, - })> - allAnonymitySets, + ({ + int setId, + String setHash, + List<({String serializedCoin, String txHash})> set, + }) + > + allAnonymitySets, required List<({int setId, Uint8List blockHash})> idAndBlockHashes, required Uint8List txHash, required int additionalTxSize, @@ -375,7 +383,8 @@ abstract final class LibSpark { start = DateTime.now(); String function = StackTrace.current.functionName; if (enableTraceLogging) { - function += "(privateKeyHex=REDACTED," + function += + "(privateKeyHex=REDACTED," "index=$index," "recipients=$recipients," "privateRecipients=$privateRecipients," @@ -394,19 +403,21 @@ abstract final class LibSpark { } try { - final resolvedExtensionCommitment = - spendVersion.resolveExtensionCommitment(extensionCommitment); + final resolvedExtensionCommitment = spendVersion + .resolveExtensionCommitment(extensionCommitment); - final privateKeyPtr = - privateKeyHex.to32BytesFromHex().unsignedCharPointer(); + final privateKeyPtr = privateKeyHex + .to32BytesFromHex() + .unsignedCharPointer(); final recipientsPtr = malloc.allocate( sizeOf() * recipients.length, ); for (int i = 0; i < recipients.length; i++) { recipientsPtr[i].amount = recipients[i].amount; - recipientsPtr[i].subtractFee = - recipients[i].subtractFeeFromAmount ? 1 : 0; + recipientsPtr[i].subtractFee = recipients[i].subtractFeeFromAmount + ? 1 + : 0; } final privateRecipientsPtr = malloc.allocate( @@ -422,12 +433,15 @@ abstract final class LibSpark { privateRecipientsPtr[i].output.ref.value = privateRecipients[i].amount; privateRecipientsPtr[i].output.ref.memoLength = privateRecipients[i].memo.length; - privateRecipientsPtr[i].output.ref.memo = - privateRecipients[i].memo.toNativeUtf8().cast(); + privateRecipientsPtr[i].output.ref.memo = privateRecipients[i].memo + .toNativeUtf8() + .cast(); privateRecipientsPtr[i].output.ref.addressLength = privateRecipients[i].sparkAddress.length; - privateRecipientsPtr[i].output.ref.address = - privateRecipients[i].sparkAddress.toNativeUtf8().cast(); + privateRecipientsPtr[i].output.ref.address = privateRecipients[i] + .sparkAddress + .toNativeUtf8() + .cast(); } final serializedCoinsPtr = malloc.allocate( @@ -438,17 +452,17 @@ abstract final class LibSpark { serializedCoinsPtr[i].serializedCoin = malloc.allocate( sizeOf(), ); - serializedCoinsPtr[i].serializedCoin.ref.data = - b64CoinDecoded.unsignedCharPointer(); + serializedCoinsPtr[i].serializedCoin.ref.data = b64CoinDecoded + .unsignedCharPointer(); serializedCoinsPtr[i].serializedCoin.ref.length = b64CoinDecoded.length; final b64ContextDecoded = base64Decode( serializedCoins[i].serializedCoinContext, ); - serializedCoinsPtr[i].serializedCoinContext = - malloc.allocate(sizeOf()); - serializedCoinsPtr[i].serializedCoinContext.ref.data = - b64ContextDecoded.unsignedCharPointer(); + serializedCoinsPtr[i].serializedCoinContext = malloc + .allocate(sizeOf()); + serializedCoinsPtr[i].serializedCoinContext.ref.data = b64ContextDecoded + .unsignedCharPointer(); serializedCoinsPtr[i].serializedCoinContext.ref.length = b64ContextDecoded.length; @@ -472,13 +486,13 @@ abstract final class LibSpark { allAnonymitySets[i].set[j].serializedCoin, ); coverSetDataAllPtr[i].cover_set[j].length = b64CoinDecoded.length; - coverSetDataAllPtr[i].cover_set[j].data = - b64CoinDecoded.unsignedCharPointer(); + coverSetDataAllPtr[i].cover_set[j].data = b64CoinDecoded + .unsignedCharPointer(); } final setHash = base64Decode(allAnonymitySets[i].setHash); - coverSetDataAllPtr[i].cover_set_representation = - setHash.unsignedCharPointer(); + coverSetDataAllPtr[i].cover_set_representation = setHash + .unsignedCharPointer(); coverSetDataAllPtr[i].cover_set_representationLength = setHash.length; } @@ -488,13 +502,13 @@ abstract final class LibSpark { for (int i = 0; i < idAndBlockHashes.length; i++) { assert(idAndBlockHashes[i].blockHash.length == 32); idAndBlockHashesPtr[i].id = idAndBlockHashes[i].setId; - idAndBlockHashesPtr[i].hash = - idAndBlockHashes[i].blockHash.unsignedCharPointer(); + idAndBlockHashesPtr[i].hash = idAndBlockHashes[i].blockHash + .unsignedCharPointer(); } final txHashPtr = txHash.unsignedCharPointer(); - final extensionCommitmentPtr = - resolvedExtensionCommitment.unsignedCharPointer(); + final extensionCommitmentPtr = resolvedExtensionCommitment + .unsignedCharPointer(); final result = native_cCreateSparkSpendTransaction( privateKeyPtr, @@ -613,12 +627,14 @@ abstract final class LibSpark { ); final List< - ({ - String serializedCoin, - String serializedCoinContext, - int groupId, - int height, - })> usedCoins = []; + ({ + String serializedCoin, + String serializedCoinContext, + int groupId, + int height, + }) + > + usedCoins = []; for (int i = 0; i < result.ref.usedCoinsLength; i++) { final coinRef = result.ref.usedCoins[i].serializedCoin.ref; @@ -680,7 +696,8 @@ abstract final class LibSpark { start = DateTime.now(); String function = StackTrace.current.functionName; if (enableTraceLogging) { - function += "(address=$address," + function += + "(address=$address," "isTestNet=$isTestNet)"; } @@ -742,11 +759,7 @@ abstract final class LibSpark { }) { final messageBytes = Uint8List.fromList(utf8.encode(message)); if (messageBytes.length > 0x7fffffff) { - throw ArgumentError.value( - message, - 'message', - 'is too long', - ); + throw ArgumentError.value(message, 'message', 'is too long'); } RangeError.checkValueInInterval( spendKeyIndex, @@ -793,9 +806,7 @@ abstract final class LibSpark { throw Exception('Failed to create Spark address ownership proof'); } - return result.ref.proof - .toUint8List(result.ref.proofLength) - .toHexString(); + 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'); @@ -804,6 +815,52 @@ abstract final class LibSpark { } } + static bool verifySparkAddressOwnershipProof({ + required String message, + required String address, + required String proof, + required bool isTestNet, + }) { + // A serialized ownership proof is one 34-byte point and three 32-byte scalars. + if (proof.length != 260 || + address.isEmpty || + address.contains('\u0000') || + !RegExp(r'^[0-9a-fA-F]+$').hasMatch(proof)) { + return false; + } + final messageBytes = Uint8List.fromList(utf8.encode(message)); + if (messageBytes.length > 0x7fffffff) { + return false; + } + final proofBytes = Uint8List.fromList( + List.generate( + proof.length ~/ 2, + (i) => int.parse(proof.substring(i * 2, i * 2 + 2), radix: 16), + ), + ); + return using((arena) { + final messagePtr = arena(messageBytes.length + 1); + messagePtr + .cast() + .asTypedList(messageBytes.length) + .setAll(0, messageBytes); + final proofPtr = arena(proofBytes.length); + proofPtr + .cast() + .asTypedList(proofBytes.length) + .setAll(0, proofBytes); + return native_verifySparkAddressOwnershipProof( + messagePtr, + messageBytes.length, + address.toNativeUtf8(allocator: arena).cast(), + proofPtr, + proofBytes.length, + isTestNet ? 1 : 0, + ) == + 1; + }); + } + static List hashTags({required List base64Tags}) { DateTime? start; int? id; @@ -837,8 +894,10 @@ abstract final class LibSpark { final List hashes = []; for (int i = 0; i < base64Tags.length; i++) { - final hash = - result.elementAt(i * 64).cast().toDartString(length: 64); + final hash = result + .elementAt(i * 64) + .cast() + .toDartString(length: 64); hashes.add(hash); } @@ -904,13 +963,14 @@ abstract final class LibSpark { required int sendAmount, required bool subtractFeeFromAmount, required List< - ({ - String serializedCoin, - String serializedCoinContext, - int groupId, - int height, - })> - serializedCoins, + ({ + String serializedCoin, + String serializedCoinContext, + int groupId, + int height, + }) + > + serializedCoins, required int privateRecipientsCount, required int utxoNum, required int additionalTxSize, @@ -924,7 +984,8 @@ abstract final class LibSpark { start = DateTime.now(); String function = StackTrace.current.functionName; if (enableTraceLogging) { - function += "(privateKeyHex=REDACTED," + function += + "(privateKeyHex=REDACTED," "index=$index," "sendAmount=$sendAmount," "subtractFeeFromAmount=$subtractFeeFromAmount," @@ -940,8 +1001,9 @@ abstract final class LibSpark { } try { - final privateKeyPtr = - privateKeyHex.to32BytesFromHex().unsignedCharPointer(); + final privateKeyPtr = privateKeyHex + .to32BytesFromHex() + .unsignedCharPointer(); final serializedCoinsPtr = malloc.allocate( sizeOf() * serializedCoins.length, @@ -951,17 +1013,17 @@ abstract final class LibSpark { serializedCoinsPtr[i].serializedCoin = malloc.allocate( sizeOf(), ); - serializedCoinsPtr[i].serializedCoin.ref.data = - b64CoinDecoded.unsignedCharPointer(); + serializedCoinsPtr[i].serializedCoin.ref.data = b64CoinDecoded + .unsignedCharPointer(); serializedCoinsPtr[i].serializedCoin.ref.length = b64CoinDecoded.length; final b64ContextDecoded = base64Decode( serializedCoins[i].serializedCoinContext, ); - serializedCoinsPtr[i].serializedCoinContext = - malloc.allocate(sizeOf()); - serializedCoinsPtr[i].serializedCoinContext.ref.data = - b64ContextDecoded.unsignedCharPointer(); + serializedCoinsPtr[i].serializedCoinContext = malloc + .allocate(sizeOf()); + serializedCoinsPtr[i].serializedCoinContext.ref.data = b64ContextDecoded + .unsignedCharPointer(); serializedCoinsPtr[i].serializedCoinContext.ref.length = b64ContextDecoded.length; @@ -1036,7 +1098,8 @@ abstract final class LibSpark { start = DateTime.now(); String function = StackTrace.current.functionName; if (enableTraceLogging) { - function += "(" + function += + "(" "sparkNameValidityBlocks=$sparkNameValidityBlocks," "name=$name," "additionalInfo=$additionalInfo," @@ -1060,8 +1123,9 @@ abstract final class LibSpark { final ownershipDigestPtr = Uint8List.fromList( proofInput.inputHex.to32BytesFromHex().reversed.toList(), ).unsignedCharPointer(); - final privateKeyPtr = - privateKeyHex.to32BytesFromHex().unsignedCharPointer(); + final privateKeyPtr = privateKeyHex + .to32BytesFromHex() + .unsignedCharPointer(); final result = native_createSparkNameScript( sparkNameValidityBlocks, @@ -1101,8 +1165,7 @@ abstract final class LibSpark { freeNative(result.ref.script, debugName: "result.ref.script"); } - size = result.ref.size + - 20; // https://github.com/firoorg/firo/blob/dd2a537d52c177736284f568e494dafb55db4924/src/spark/sparkwallet.cpp#L1624C59-L1624C123 + size = result.ref.size + 20; // https://github.com/firoorg/firo/blob/dd2a537d52c177736284f568e494dafb55db4924/src/spark/sparkwallet.cpp#L1624C59-L1624C123 freeNative(result, debugName: "result"); @@ -1139,7 +1202,8 @@ abstract final class LibSpark { start = DateTime.now(); String function = StackTrace.current.functionName; if (enableTraceLogging) { - function += "(serializedSparkNameDataLength=" + function += + "(serializedSparkNameDataLength=" "${serializedSparkNameData.length})"; } Log.l( @@ -1158,8 +1222,8 @@ abstract final class LibSpark { ); } - final serializedSparkNameDataPtr = - serializedSparkNameData.unsignedCharPointer(); + final serializedSparkNameDataPtr = serializedSparkNameData + .unsignedCharPointer(); try { final result = native_cGetSparkNameCommitment( serializedSparkNameDataPtr, @@ -1361,8 +1425,8 @@ abstract final class LibSpark { final ret = LibSparkCoin( type: coinType, nonceHex: result.ref.nonceHex.cast().toDartString( - length: result.ref.nonceHexLength, - ), + length: result.ref.nonceHexLength, + ), address: result.ref.address.cast().toDartString(), value: BigInt.from(result.ref.value), memo: result.ref.memo.cast().toDartString(), diff --git a/lib/src/flutter_libsparkmobile_bindings_generated.dart b/lib/src/flutter_libsparkmobile_bindings_generated.dart index e24afd3..f46d89e 100644 --- a/lib/src/flutter_libsparkmobile_bindings_generated.dart +++ b/lib/src/flutter_libsparkmobile_bindings_generated.dart @@ -9,38 +9,45 @@ import 'dart:ffi' as ffi; @ffi.Native< - ffi.Pointer Function(ffi.Pointer, ffi.Int)>( - symbol: 'getFullViewKeyFromPrivateKeyData') + ffi.Pointer Function(ffi.Pointer, ffi.Int) +>(symbol: 'getFullViewKeyFromPrivateKeyData') external ffi.Pointer native_getFullViewKeyFromPrivateKeyData( ffi.Pointer keyData, int index, ); @ffi.Native< - ffi.Pointer Function(ffi.Pointer, ffi.Int)>( - symbol: 'deserializeFullViewKey') + ffi.Pointer Function(ffi.Pointer, ffi.Int) +>(symbol: 'deserializeFullViewKey') external ffi.Pointer native_deserializeFullViewKey( ffi.Pointer keyData, int keyDataLength, ); @ffi.Native< - ffi.Pointer Function(ffi.Pointer, - ffi.Pointer)>(symbol: 'serializeFullViewKey') + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ) +>(symbol: 'serializeFullViewKey') external ffi.Pointer native_serializeFullViewKey( ffi.Pointer fullViewKeyVoid, ffi.Pointer serializedSize, ); @ffi.Native)>( - symbol: 'deleteFullViewKey') -external void native_deleteFullViewKey( - ffi.Pointer fullViewKey, -); + symbol: 'deleteFullViewKey', +) +external void native_deleteFullViewKey(ffi.Pointer fullViewKey); @ffi.Native< - ffi.Pointer Function(ffi.Pointer, ffi.Int, - ffi.Int, ffi.Int)>(symbol: 'getAddress') + ffi.Pointer Function( + ffi.Pointer, + ffi.Int, + ffi.Int, + ffi.Int, + ) +>(symbol: 'getAddress') external ffi.Pointer native_getAddress( ffi.Pointer keyData, int index, @@ -49,8 +56,13 @@ external ffi.Pointer native_getAddress( ); @ffi.Native< - ffi.Pointer Function(ffi.Pointer, ffi.Int, ffi.Int, - ffi.Int)>(symbol: 'getAddressFromFullViewKey') + ffi.Pointer Function( + ffi.Pointer, + ffi.Int, + ffi.Int, + ffi.Int, + ) +>(symbol: 'getAddressFromFullViewKey') external ffi.Pointer native_getAddressFromFullViewKey( ffi.Pointer fullViewKeyVoid, int index, @@ -65,14 +77,16 @@ external ffi.Pointer native_getAddressFromFullViewKey( /// //FFI_PLUGIN_EXPORT /// //struct CIdentifiedCoinData identifyCoin(const unsigned char* serializedCoin, int serializedCoinLength, unsigned char* keyData, int index); @ffi.Native< - ffi.Pointer Function( - ffi.Pointer, - ffi.Int, - ffi.Pointer, - ffi.Int, - ffi.Pointer, - ffi.Int, - ffi.Int)>(symbol: 'idAndRecoverCoin') + ffi.Pointer Function( + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Int, + ffi.Int, + ) +>(symbol: 'idAndRecoverCoin') external ffi.Pointer native_idAndRecoverCoin( ffi.Pointer serializedCoin, int serializedCoinLength, @@ -84,13 +98,15 @@ external ffi.Pointer native_idAndRecoverCoin( ); @ffi.Native< - ffi.Pointer Function( - ffi.Pointer, - ffi.Int, - ffi.Pointer, - ffi.Pointer, - ffi.Int, - ffi.Int)>(symbol: 'idAndRecoverCoinByFullViewKey') + ffi.Pointer Function( + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Pointer, + ffi.Int, + ffi.Int, + ) +>(symbol: 'idAndRecoverCoinByFullViewKey') external ffi.Pointer native_idAndRecoverCoinByFullViewKey( ffi.Pointer serializedCoin, int serializedCoinLength, @@ -104,12 +120,14 @@ external ffi.Pointer native_idAndRecoverCoinByFullViewKey( /// /// createSparkMintRecipients: https://github.com/firoorg/sparkmobile/blob/8bf17cd3deba6c3b0d10e89282e02936d7e71cdd/src/spark.cpp#L43 @ffi.Native< - ffi.Pointer Function( - ffi.Pointer, - ffi.Int, - ffi.Pointer, - ffi.Int, - ffi.Int)>(symbol: 'cCreateSparkMintRecipients') + ffi.Pointer Function( + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Int, + ffi.Int, + ) +>(symbol: 'cCreateSparkMintRecipients') external ffi.Pointer native_cCreateSparkMintRecipients( ffi.Pointer outputs, int outputsLength, @@ -122,25 +140,27 @@ external ffi.Pointer native_cCreateSparkMintRecipients( /// /// createSparkSpendTransaction: https://github.com/firoorg/sparkmobile/blob/23099b0d9010a970ad75b9cfe05d568d634088f3/src/spark.cpp#L190 @ffi.Native< - ffi.Pointer Function( - ffi.Pointer, - ffi.Int, - ffi.Pointer, - ffi.Int, - ffi.Pointer, - ffi.Int, - ffi.Pointer, - ffi.Int, - ffi.Pointer, - ffi.Int, - ffi.Pointer, - ffi.Int, - ffi.Pointer, - ffi.Int, - ffi.Int, - ffi.Pointer)>(symbol: 'cCreateSparkSpendTransaction') + ffi.Pointer Function( + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Int, + ffi.Int, + ffi.Pointer, + ) +>(symbol: 'cCreateSparkSpendTransaction') external ffi.Pointer - native_cCreateSparkSpendTransaction( +native_cCreateSparkSpendTransaction( ffi.Pointer keyData, int index, ffi.Pointer recipients, @@ -160,49 +180,54 @@ external ffi.Pointer ); @ffi.Native< - ffi.Pointer Function( - ffi.Pointer, ffi.Int)>(symbol: 'serializeMintContext') + ffi.Pointer Function( + ffi.Pointer, + ffi.Int, + ) +>(symbol: 'serializeMintContext') external ffi.Pointer native_serializeMintContext( ffi.Pointer inputs, int inputsLength, ); @ffi.Native< - ffi.Pointer Function( - ffi.Pointer, ffi.Int)>(symbol: 'isValidSparkAddress') + ffi.Pointer Function(ffi.Pointer, ffi.Int) +>(symbol: 'isValidSparkAddress') external ffi.Pointer native_isValidSparkAddress( ffi.Pointer addressCStr, int isTestNet, ); @ffi.Native< - ffi.Pointer Function( - ffi.Pointer, ffi.Int)>(symbol: 'hashTags') + ffi.Pointer Function(ffi.Pointer, ffi.Int) +>(symbol: 'hashTags') external ffi.Pointer native_hashTags( ffi.Pointer tags, int tagCount, ); @ffi.Native< - ffi.Pointer Function( - ffi.Pointer, ffi.Pointer)>(symbol: 'hashTag') + ffi.Pointer Function(ffi.Pointer, ffi.Pointer) +>(symbol: 'hashTag') external ffi.Pointer native_hashTag( ffi.Pointer x, ffi.Pointer y, ); @ffi.Native< - ffi.Pointer Function( - ffi.Pointer, - ffi.Int, - ffi.Int64, - ffi.Int, - ffi.Pointer, - ffi.Int, - ffi.Int, - ffi.Int, - ffi.Int, - ffi.Int)>(symbol: 'estimateSparkFee') + ffi.Pointer Function( + ffi.Pointer, + ffi.Int, + ffi.Int64, + ffi.Int, + ffi.Pointer, + ffi.Int, + ffi.Int, + ffi.Int, + ffi.Int, + ffi.Int, + ) +>(symbol: 'estimateSparkFee') external ffi.Pointer native_estimateSparkFee( ffi.Pointer keyData, int index, @@ -217,18 +242,20 @@ external ffi.Pointer native_estimateSparkFee( ); @ffi.Native< - ffi.Pointer Function( - ffi.Int, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Int, - ffi.Pointer, - ffi.Int, - ffi.Int, - ffi.Int, - ffi.Int, - ffi.Int)>(symbol: 'createSparkNameScript') + ffi.Pointer Function( + ffi.Int, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Int, + ffi.Int, + ffi.Int, + ffi.Int, + ffi.Int, + ) +>(symbol: 'createSparkNameScript') external ffi.Pointer native_createSparkNameScript( int sparkNameValidityBlocks, ffi.Pointer name, @@ -244,12 +271,14 @@ external ffi.Pointer native_createSparkNameScript( ); @ffi.Native< - ffi.Pointer Function( - ffi.Pointer, - ffi.Int, - ffi.Pointer, - ffi.Int, - ffi.Int)>(symbol: 'createSparkAddressOwnershipProof') + ffi.Pointer Function( + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Int, + ffi.Int, + ) +>(symbol: 'createSparkAddressOwnershipProof') external ffi.Pointer native_createSparkAddressOwnershipProof( ffi.Pointer message, @@ -260,18 +289,18 @@ native_createSparkAddressOwnershipProof( ); @ffi.Native< - ffi.Pointer Function( - ffi.Pointer, - ffi.Int)>(symbol: 'cGetSparkNameCommitment') + ffi.Pointer Function( + ffi.Pointer, + ffi.Int, + ) +>(symbol: 'cGetSparkNameCommitment') external ffi.Pointer native_cGetSparkNameCommitment( ffi.Pointer serializedSparkNameData, int serializedSparkNameDataLength, ); @ffi.Native)>() -external void native_free( - ffi.Pointer ptr, -); +external void native_free(ffi.Pointer ptr); /// FFI-friendly wrapper for a spark::Coin. /// @@ -648,3 +677,22 @@ final class SparkAddressOwnershipProofResult extends ffi.Struct { external ffi.Pointer error; } + +@ffi.Native< + ffi.Int Function( + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Pointer, + ffi.Int, + ffi.Int, + ) +>(symbol: 'verifySparkAddressOwnershipProof') +external int native_verifySparkAddressOwnershipProof( + ffi.Pointer message, + int messageLength, + ffi.Pointer encodedAddress, + ffi.Pointer proofData, + int proofLength, + int isTestNet, +); diff --git a/src/flutter_libsparkmobile.cpp b/src/flutter_libsparkmobile.cpp index 6cf6b94..0117a7f 100644 --- a/src/flutter_libsparkmobile.cpp +++ b/src/flutter_libsparkmobile.cpp @@ -114,6 +114,52 @@ SparkAddressOwnershipProofResult* createSparkAddressOwnershipProof( return result; } +FFI_PLUGIN_EXPORT +int verifySparkAddressOwnershipProof( + const unsigned char* message, + int messageLength, + const char* encodedAddress, + const unsigned char* proofData, + int proofLength, + int isTestNet +) { + try { + spark::OwnershipProof proof; + if (messageLength < 0 || (messageLength > 0 && !message) || + !encodedAddress || !proofData || + proofLength != static_cast(proof.memoryRequired())) { + return 0; + } + + spark::Address address; + if (address.decode(encodedAddress) != (isTestNet + ? spark::ADDRESS_NETWORK_TESTNET : spark::ADDRESS_NETWORK_MAINNET)) { + return 0; + } + + std::vector bytes(proofData, proofData + proofLength); + CDataStream stream(bytes, SER_NETWORK, PROTOCOL_VERSION); + stream >> proof; + // Reject alternate encodings as well as truncated or trailing data. + CDataStream canonical(SER_NETWORK, PROTOCOL_VERSION); + canonical << proof; + if (bytes != std::vector(canonical.begin(), canonical.end())) { + return 0; + } + + const std::string messageString = messageLength == 0 + ? std::string() + : std::string(reinterpret_cast(message), messageLength); + CHashWriter messageHash(SER_GETHASH, 0); + messageHash << std::string("Zcoin Signed Message:\n") << messageString; + Scalar messageScalar; + messageScalar.SetHex(messageHash.GetHash().GetHex()); + return address.verify_own(messageScalar, proof) ? 1 : 0; + } catch (const std::exception&) { + return 0; + } +} + /* * FFI-friendly wrapper for spark:identifyCoin. * diff --git a/src/flutter_libsparkmobile.h b/src/flutter_libsparkmobile.h index 826383d..c7e20b9 100644 --- a/src/flutter_libsparkmobile.h +++ b/src/flutter_libsparkmobile.h @@ -157,6 +157,16 @@ struct SparkAddressOwnershipProofResult* createSparkAddressOwnershipProof( int diversifier ); +FFI_PLUGIN_EXPORT +int verifySparkAddressOwnershipProof( + const unsigned char* message, + int messageLength, + const char* encodedAddress, + const unsigned char* proofData, + int proofLength, + int isTestNet +); + FFI_PLUGIN_EXPORT struct SparkNameCommitmentResult* cGetSparkNameCommitment( const unsigned char* serializedSparkNameData, diff --git a/test/flutter_libsparkmobile_test.dart b/test/flutter_libsparkmobile_test.dart index cca5fb9..1b8ce50 100644 --- a/test/flutter_libsparkmobile_test.dart +++ b/test/flutter_libsparkmobile_test.dart @@ -128,6 +128,84 @@ void main() { expect(RegExp(r'^[0-9a-f]+$').hasMatch(proof), isTrue); }); + test( + 'ownership proofs bind the exact message, address and network', + () async { + const key = + 'cb02b05c71a69080b083484f1cdf407677fac00ced6438df16925e2a29b4eebf'; + for (final isTestNet in [false, true]) { + final address = await LibSpark.getAddress( + privateKey: key.to32BytesFromHex(), + index: 1, + diversifier: 0, + isTestNet: isTestNet, + ); + final otherAddress = await LibSpark.getAddress( + privateKey: key.to32BytesFromHex(), + index: 1, + diversifier: 1, + isTestNet: isTestNet, + ); + expect( + LibSpark.verifySparkAddressOwnershipProof( + message: 'challenge', + address: address, + proof: '00' * 2097152, + isTestNet: isTestNet, + ), + isFalse, + ); + for (final message in [ + '', + ' challenge\n', + '\u03bb\u0000message', + 'a' * 1025, + ]) { + final proof = LibSpark.createSparkAddressOwnershipProof( + message: message, + privateKeyHex: key, + spendKeyIndex: 1, + diversifier: 0, + ); + bool verify({ + String? text, + String? addr, + String? sig, + bool? testnet, + }) => LibSpark.verifySparkAddressOwnershipProof( + message: text ?? message, + address: addr ?? address, + proof: sig ?? proof, + isTestNet: testnet ?? isTestNet, + ); + expect(verify(), isTrue); + expect(verify(sig: proof.toUpperCase()), isTrue); + expect(verify(text: '${message}x'), isFalse); + if (message.trim() != message) { + expect(verify(text: message.trim()), isFalse); + } + expect(verify(addr: otherAddress), isFalse); + expect(verify(testnet: !isTestNet), isFalse); + expect(verify(addr: '$address\u0000ignored'), isFalse); + expect(verify(addr: 'not-an-address'), isFalse); + for (final invalidProof in [ + '', + 'zz', + 'a', + 'g${proof.substring(1)}', + '$proof\n', + '${proof}00', + proof.substring(2), + '00' * 130, + '${proof.substring(0, 64)}02${proof.substring(66)}', + ]) { + expect(verify(sig: invalidProof), isFalse); + } + } + } + }, + ); + test('mnemonic to address test', () async { // Generate key data from the mnemonic. //