Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions lib/flutter_libsparkmobile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<UnsignedChar>()
: messageBytes.unsignedCharPointer();
final privateKeyPtr = privateKeyBytes.unsignedCharPointer();

late Pointer<SparkAddressOwnershipProofResult> 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<Utf8>().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<String> hashTags({required List<String> base64Tags}) {
DateTime? start;
int? id;
Expand Down
25 changes: 25 additions & 0 deletions lib/src/flutter_libsparkmobile_bindings_generated.dart
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,22 @@ external ffi.Pointer<SparkNameScript> native_createSparkNameScript(
int withoutProof,
);

@ffi.Native<
ffi.Pointer<SparkAddressOwnershipProofResult> Function(
ffi.Pointer<ffi.UnsignedChar>,
ffi.Int,
ffi.Pointer<ffi.UnsignedChar>,
ffi.Int,
ffi.Int)>(symbol: 'createSparkAddressOwnershipProof')
external ffi.Pointer<SparkAddressOwnershipProofResult>
native_createSparkAddressOwnershipProof(
ffi.Pointer<ffi.UnsignedChar> message,
int messageLength,
ffi.Pointer<ffi.UnsignedChar> spendKeyData,
int spendKeyIndex,
int diversifier,
);

@ffi.Native<
ffi.Pointer<SparkNameCommitmentResult> Function(
ffi.Pointer<ffi.UnsignedChar>,
Expand Down Expand Up @@ -623,3 +639,12 @@ final class SparkNameScript extends ffi.Struct {
@ffi.Int()
external int size;
}

final class SparkAddressOwnershipProofResult extends ffi.Struct {
external ffi.Pointer<ffi.UnsignedChar> proof;

@ffi.Int()
external int proofLength;

external ffi.Pointer<ffi.Char> error;
}
64 changes: 64 additions & 0 deletions src/flutter_libsparkmobile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<SparkAddressOwnershipProofResult*>(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<const char*>(message),
static_cast<size_t>(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<uint64_t>(diversifier));

spark::OwnershipProof proof;
address.prove_own(messageScalar, spendKey, incomingViewKey, proof);

CDataStream proofStream(SER_NETWORK, PROTOCOL_VERSION);
proofStream << proof;

result->proofLength = static_cast<int>(proofStream.size());
result->proof = static_cast<unsigned char*>(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<char*>(malloc(strlen(e.what()) + 1));
if (result->error) {
strcpy(result->error, e.what());
}
}

return result;
}

/*
* FFI-friendly wrapper for spark:identifyCoin.
*
Expand Down
9 changes: 9 additions & 0 deletions src/flutter_libsparkmobile.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@ struct SparkNameScript {
int size;
};

struct SparkAddressOwnershipProofResult {
unsigned char* proof;
int proofLength;
char* error;
};

//#ifdef __cplusplus
//}
//#endif
Expand Down
54 changes: 54 additions & 0 deletions test/flutter_libsparkmobile_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down