diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/key_derivation.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/key_derivation.dart index 53ec2f37d9b..017301be517 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/key_derivation.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/keychain/key_derivation.dart @@ -7,6 +7,9 @@ final class KeyDerivation { /// Derives an [Ed25519KeyPair] from a [seedPhrase] and [path]. /// /// Example [path]: m/0'/2147483647' + /// + // TODO(dtscalac): this takes around 2.5s to execute, optimize it + // or move to a JS web worker. Future deriveKeyPair({ required SeedPhrase seedPhrase, required String path, diff --git a/catalyst_voices/packages/catalyst_voices_services/lib/src/registration/registration_transaction_builder.dart b/catalyst_voices/packages/catalyst_voices_services/lib/src/registration/registration_transaction_builder.dart index 63850d0bcd5..e551c64204c 100644 --- a/catalyst_voices/packages/catalyst_voices_services/lib/src/registration/registration_transaction_builder.dart +++ b/catalyst_voices/packages/catalyst_voices_services/lib/src/registration/registration_transaction_builder.dart @@ -60,7 +60,7 @@ final class RegistrationTransactionBuilder { return _buildUnsignedRbacTx( auxiliaryData: AuxiliaryData.fromCbor( - x509Envelope.toCbor(serializer: (e) => e.toCbor()), + await x509Envelope.toCbor(serializer: (e) => e.toCbor()), ), ); } diff --git a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/example/lib/sign_and_submit_rbac_tx.dart b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/example/lib/sign_and_submit_rbac_tx.dart index ac34dc1ab64..6a32f9453b6 100644 --- a/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/example/lib/sign_and_submit_rbac_tx.dart +++ b/catalyst_voices_packages/catalyst_cardano/catalyst_cardano/example/lib/sign_and_submit_rbac_tx.dart @@ -34,7 +34,7 @@ Future _signAndSubmitRbacTx({ ); final auxiliaryData = AuxiliaryData.fromCbor( - x509Envelope.toCbor(serializer: (e) => e.toCbor()), + await x509Envelope.toCbor(serializer: (e) => e.toCbor()), ); final unsignedTx = _buildUnsignedRbacTx( @@ -122,7 +122,9 @@ Future> _buildMetadataEnvelope({ print('unsigned x509 envelope:'); print( - hex.encode(cbor.encode(x509Envelope.toCbor(serializer: (e) => e.toCbor()))), + hex.encode( + cbor.encode(await x509Envelope.toCbor(serializer: (e) => e.toCbor())), + ), ); final signedX509Envelope = await x509Envelope.sign( @@ -133,7 +135,9 @@ Future> _buildMetadataEnvelope({ print('signed x509 envelope:'); print( hex.encode( - cbor.encode(signedX509Envelope.toCbor(serializer: (e) => e.toCbor())), + cbor.encode( + await signedX509Envelope.toCbor(serializer: (e) => e.toCbor()), + ), ), ); diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/rbac/x509_metadata_envelope.dart b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/rbac/x509_metadata_envelope.dart index d9275fdb8c1..22c7ca0ed4a 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/rbac/x509_metadata_envelope.dart +++ b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/rbac/x509_metadata_envelope.dart @@ -129,16 +129,16 @@ final class X509MetadataEnvelope extends Equatable { /// /// The [deserializer] in most cases is going /// to be [RegistrationData.fromCbor]. - factory X509MetadataEnvelope.fromCbor( + static Future> fromCbor( CborValue value, { required ChunkedDataDeserializer deserializer, - }) { + }) async { final metadata = value as CborMap; final envelope = metadata[const CborSmallInt(509)]! as CborMap; final purpose = envelope[const CborSmallInt(0)]! as CborBytes; final txInputsHash = envelope[const CborSmallInt(1)]!; final previousTransactionId = envelope[const CborSmallInt(2)]; - final chunkedData = _deserializeChunkedData(envelope); + final chunkedData = await _deserializeChunkedData(envelope); final validationSignature = envelope[const CborSmallInt(99)]!; return X509MetadataEnvelope( @@ -155,10 +155,12 @@ final class X509MetadataEnvelope extends Equatable { /// Serializes the type as cbor. /// /// The [serializer] in most cases is going to be [RegistrationData.toCbor]. - CborValue toCbor({required ChunkedDataSerializer serializer}) { + Future toCbor({ + required ChunkedDataSerializer serializer, + }) async { final chunkedData = this.chunkedData; final metadata = chunkedData != null - ? _serializeChunkedData(serializer(chunkedData)) + ? await _serializeChunkedData(serializer(chunkedData)) : null; return CborMap({ @@ -182,7 +184,7 @@ final class X509MetadataEnvelope extends Equatable { required Ed25519PrivateKey privateKey, required ChunkedDataSerializer serializer, }) async { - final bytes = cbor.encode(toCbor(serializer: serializer)); + final bytes = cbor.encode(await toCbor(serializer: serializer)); final signature = await privateKey.sign(bytes); return withValidationSignature(signature); } @@ -197,7 +199,7 @@ final class X509MetadataEnvelope extends Equatable { required ChunkedDataSerializer serializer, }) async { final envelope = withValidationSignature(Ed25519Signature.seeded(0)); - final bytes = cbor.encode(envelope.toCbor(serializer: serializer)); + final bytes = cbor.encode(await envelope.toCbor(serializer: serializer)); return signature.verify(bytes, publicKey: publicKey); } @@ -215,7 +217,7 @@ final class X509MetadataEnvelope extends Equatable { ); } - static CborValue? _deserializeChunkedData(CborMap map) { + static Future _deserializeChunkedData(CborMap map) async { final rawCbor = map[const CborSmallInt(10)] as CborList?; if (rawCbor != null) { final bytes = _unchunkCborBytes(rawCbor); @@ -226,7 +228,7 @@ final class X509MetadataEnvelope extends Equatable { if (brotliCbor != null) { final bytes = _unchunkCborBytes(brotliCbor); final uncompressedBytes = - CatalystCompression.instance.brotli.decompress(bytes); + await CatalystCompression.instance.brotli.decompress(bytes); return cbor.decode(uncompressedBytes); } @@ -234,19 +236,19 @@ final class X509MetadataEnvelope extends Equatable { if (zstdCbor != null) { final bytes = _unchunkCborBytes(zstdCbor); final uncompressedBytes = - CatalystCompression.instance.zstd.decompress(bytes); + await CatalystCompression.instance.zstd.decompress(bytes); return cbor.decode(uncompressedBytes); } return null; } - static MapEntry _serializeChunkedData( + static Future> _serializeChunkedData( CborValue value, - ) { + ) async { final rawBytes = cbor.encode(value); - final brotliBytes = _compressBrotli(rawBytes); - final zstdBytes = _compressZstd(rawBytes); + final brotliBytes = await _compressBrotli(rawBytes); + final zstdBytes = await _compressZstd(rawBytes); final bytesByKey = { 10: rawBytes, @@ -266,17 +268,17 @@ final class X509MetadataEnvelope extends Equatable { ); } - static List? _compressBrotli(List bytes) { + static Future?> _compressBrotli(List bytes) async { try { - return CatalystCompression.instance.brotli.compress(bytes); + return await CatalystCompression.instance.brotli.compress(bytes); } on CompressionNotSupportedException { return null; } } - static List? _compressZstd(List bytes) { + static Future?> _compressZstd(List bytes) async { try { - return CatalystCompression.instance.zstd.compress(bytes); + return await CatalystCompression.instance.zstd.compress(bytes); } on CompressionNotSupportedException { return null; } diff --git a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/signature.dart b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/signature.dart index bee9f452d85..a29064f0a73 100644 --- a/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/signature.dart +++ b/catalyst_voices_packages/catalyst_cardano_serialization/lib/src/signature.dart @@ -116,6 +116,9 @@ extension type Ed25519PrivateKey._(List bytes) { String toHex() => hex.encode(bytes); /// Signs the [message] with the private key and returns the signature. + // + // TODO(dtscalac): it takes 200-300ms to execute, optimize it + // or move to a JS web worker Future sign(List message) async { final algorithm = Ed25519(); final keyPair = await algorithm.newKeyPairFromSeed(bytes); diff --git a/catalyst_voices_packages/catalyst_compression/catalyst_compression/README.md b/catalyst_voices_packages/catalyst_compression/catalyst_compression/README.md index 68965ff97a8..d5c0683c0f4 100644 --- a/catalyst_voices_packages/catalyst_compression/catalyst_compression/README.md +++ b/catalyst_voices_packages/catalyst_compression/catalyst_compression/README.md @@ -85,13 +85,13 @@ E61E8EE7D77E9F7F9804E03EBC31B458 ''' .replaceAll('\n', ''); -void main() { +Future main() async { final rawBytes = hex.decode(derCertHex); // brotli final brotli = CatalystCompression.instance.brotli; - final brotliCompressed = brotli.compress(rawBytes); - final brotliDecompressed = brotli.decompress(brotliCompressed); + final brotliCompressed = await brotli.compress(rawBytes); + final brotliDecompressed = await brotli.decompress(brotliCompressed); assert( listEquals(rawBytes, brotliDecompressed), @@ -100,8 +100,8 @@ void main() { // zstd final zstd = CatalystCompression.instance.zstd; - final zstdCompressed = zstd.compress(rawBytes); - final zstdDecompressed = zstd.decompress(zstdCompressed); + final zstdCompressed = await zstd.compress(rawBytes); + final zstdDecompressed = await zstd.decompress(zstdCompressed); assert( listEquals(rawBytes, zstdDecompressed), diff --git a/catalyst_voices_packages/catalyst_compression/catalyst_compression/example/main.dart b/catalyst_voices_packages/catalyst_compression/catalyst_compression/example/main.dart index 22697d19e98..2a102c3732b 100644 --- a/catalyst_voices_packages/catalyst_compression/catalyst_compression/example/main.dart +++ b/catalyst_voices_packages/catalyst_compression/catalyst_compression/example/main.dart @@ -42,13 +42,13 @@ E61E8EE7D77E9F7F9804E03EBC31B458 ''' .replaceAll('\n', ''); -void main() { +Future main() async { final rawBytes = hex.decode(derCertHex); // brotli final brotli = CatalystCompression.instance.brotli; - final brotliCompressed = brotli.compress(rawBytes); - final brotliDecompressed = brotli.decompress(brotliCompressed); + final brotliCompressed = await brotli.compress(rawBytes); + final brotliDecompressed = await brotli.decompress(brotliCompressed); assert( listEquals(rawBytes, brotliDecompressed), @@ -57,8 +57,8 @@ void main() { // zstd final zstd = CatalystCompression.instance.zstd; - final zstdCompressed = zstd.compress(rawBytes); - final zstdDecompressed = zstd.decompress(zstdCompressed); + final zstdCompressed = await zstd.compress(rawBytes); + final zstdDecompressed = await zstd.decompress(zstdCompressed); assert( listEquals(rawBytes, zstdDecompressed), diff --git a/catalyst_voices_packages/catalyst_compression/catalyst_compression_platform_interface/lib/src/catalyst_compressor.dart b/catalyst_voices_packages/catalyst_compression/catalyst_compression_platform_interface/lib/src/catalyst_compressor.dart index c64ff341dec..34908e27f89 100644 --- a/catalyst_voices_packages/catalyst_compression/catalyst_compression_platform_interface/lib/src/catalyst_compressor.dart +++ b/catalyst_voices_packages/catalyst_compression/catalyst_compression_platform_interface/lib/src/catalyst_compressor.dart @@ -7,13 +7,13 @@ abstract class CatalystCompressor { /// /// Compressing and then decompressing the [bytes] /// should yield the original [bytes]. - List compress(List bytes); + Future> compress(List bytes); /// Returns the list of decompressed [bytes]. /// /// Compressing and then decompressing the [bytes] /// should yield the original [bytes]. - List decompress(List bytes); + Future> decompress(List bytes); } /// Exception thrown when [CatalystCompressor.compress] can't compress diff --git a/catalyst_voices_packages/catalyst_compression/catalyst_compression_web/assets/js/catalyst_compression.js b/catalyst_voices_packages/catalyst_compression/catalyst_compression_web/assets/js/catalyst_compression.js index 065791c5e01..b4749753718 100644 --- a/catalyst_voices_packages/catalyst_compression/catalyst_compression_web/assets/js/catalyst_compression.js +++ b/catalyst_voices_packages/catalyst_compression/catalyst_compression_web/assets/js/catalyst_compression.js @@ -1,73 +1,84 @@ -const brotli = await import("https://unpkg.com/brotli-wasm@3.0.0/index.web.js?module").then(m => m.default); -const zstd = await import("https://unpkg.com/@oneidentity/zstd-js@1.0.3/wasm/index.js?module"); +// Initialize a web worker for compression works. +// This is a persistent worker, will last for life of the app. +const compressionWorker = new Worker(new URL('./catalyst_compression_worker.js', import.meta.url)); -// Initializes the zstd module, must be called before it can be used. -await zstd.ZstdInit(); +let idCounter = 0; -/// Compresses hex bytes using brotli compression algorithm and returns compressed hex bytes. -function _brotliCompress(bytesHex) { - const bytes = _hexStringToUint8Array(bytesHex); - const compressedBytes = brotli.compress(bytes); - return _uint8ArrayToHexString(compressedBytes); -} +// A simple id generator function. +function generateId() { + const thisId = idCounter; + const nextId = idCounter + 1; -/// Decompresses hex bytes using brotli compression algorithm and returns decompressed hex bytes. -function _brotliDecompress(bytesHex) { - const bytes = _hexStringToUint8Array(bytesHex); - const decompressedBytes = brotli.decompress(bytes); - return _uint8ArrayToHexString(decompressedBytes); -} + idCounter = nextId >= Number.MAX_SAFE_INTEGER ? 0 : nextId; -/// Compresses hex bytes using zstd compression algorithm and returns compressed hex bytes. -function _zstdCompress(bytesHex) { - const bytes = _hexStringToUint8Array(bytesHex); - const compressedBytes = zstd.ZstdSimple.compress(bytes); - return _uint8ArrayToHexString(compressedBytes); + return thisId; } -/// Decompresses hex bytes using zstd compression algorithm and returns decompressed hex bytes. -function _zstdDecompress(bytesHex) { - const bytes = _hexStringToUint8Array(bytesHex); - const decompressedBytes = zstd.ZstdSimple.decompress(bytes); - return _uint8ArrayToHexString(decompressedBytes); -} +function registerWorkerEventHandler(worker, handleMessage, handleError) { + const wrappedHandleMessage = (event) => handleMessage(event, complete); + const wrappedHandleError = (error) => handleError(error, complete); -// Converts a hex string into a byte array. -function _hexStringToUint8Array(hexString) { - // Ensure the hex string length is even - if (hexString.length % 2 !== 0) { - throw new Error('Invalid hex string'); + function complete() { + worker.removeEventListener("message", wrappedHandleMessage); + worker.removeEventListener("error", wrappedHandleError); } - // Create a Uint8Array - const byteArray = new Uint8Array(hexString.length / 2); + worker.addEventListener("message", wrappedHandleMessage); + worker.addEventListener("error", wrappedHandleError); +} - // Parse the hex string into byte values - for (let i = 0; i < hexString.length; i += 2) { - byteArray[i / 2] = parseInt(hexString.substr(i, 2), 16); - } +// A function to create a compression function according to its name. +function runCompressionInWorker(fnName) { + return (data) => { + return new Promise((resolve, reject) => { + const id = generateId(); - return byteArray; -} + registerWorkerEventHandler( + compressionWorker, + (event, complete) => { + const { + id: responseId, + result, + error, + initialized + } = event.data; + + // skip the initializing completion event, + // and the id that is not itself. + if (initialized || responseId !== id) { + return; + } + + if (result) { + resolve(result); + } else { + reject(error || 'Unexpected error'); + } -// Converts a byte array into a hex string. -function _uint8ArrayToHexString(uint8Array) { - return Array.from(uint8Array) - .map(byte => byte.toString(16).padStart(2, '0')) - .join(''); -} + complete(); + }, + (error, complete) => { + reject(error); + complete(); + } + ); + + compressionWorker.postMessage({ id, action: fnName, bytesHex: data }); + }); + } +} // A namespace containing the JS functions that // can be executed from dart side const catalyst_compression = { - brotliCompress: _brotliCompress, - brotliDecompress: _brotliDecompress, - zstdCompress: _zstdCompress, - zstdDecompress: _zstdDecompress, -} + brotliCompress: runCompressionInWorker("brotliCompress"), + brotliDecompress: runCompressionInWorker("brotliDecompress"), + zstdCompress: runCompressionInWorker("zstdCompress"), + zstdDecompress: runCompressionInWorker("zstdDecompress"), +}; // Expose catalyst compression as globally accessible // so that we can call it via catalyst_compression.function_name() from // other scripts or dart without needing to care about module imports -window.catalyst_compression = catalyst_compression; \ No newline at end of file +window.catalyst_compression = catalyst_compression; diff --git a/catalyst_voices_packages/catalyst_compression/catalyst_compression_web/assets/js/catalyst_compression_worker.js b/catalyst_voices_packages/catalyst_compression/catalyst_compression_web/assets/js/catalyst_compression_worker.js new file mode 100644 index 00000000000..7de73de5f9d --- /dev/null +++ b/catalyst_voices_packages/catalyst_compression/catalyst_compression_web/assets/js/catalyst_compression_worker.js @@ -0,0 +1,93 @@ +// initial message handler from the main thread. +self.onmessage = (event) => { + const { id } = event.data; + self.postMessage({ id, error: 'Worker is not ready' }); +}; + +Promise.all([ + import('https://unpkg.com/brotli-wasm@3.0.0/index.web.js?module').then(m => m.default), + import('https://unpkg.com/@oneidentity/zstd-js@1.0.3/wasm/index.js?module') +]).then(async ([brotli, zstd]) => { + // Initializes the zstd module, must be called before it can be used. + await zstd.ZstdInit(); + + /// Compresses hex bytes using brotli compression algorithm and returns compressed hex bytes. + function _brotliCompress(bytesHex) { + const bytes = _hexStringToUint8Array(bytesHex); + const compressedBytes = brotli.compress(bytes); + return _uint8ArrayToHexString(compressedBytes); + } + + /// Decompresses hex bytes using brotli compression algorithm and returns decompressed hex bytes. + function _brotliDecompress(bytesHex) { + const bytes = _hexStringToUint8Array(bytesHex); + const decompressedBytes = brotli.decompress(bytes); + return _uint8ArrayToHexString(decompressedBytes); + } + + /// Compresses hex bytes using zstd compression algorithm and returns compressed hex bytes. + function _zstdCompress(bytesHex) { + const bytes = _hexStringToUint8Array(bytesHex); + const compressedBytes = zstd.ZstdSimple.compress(bytes); + return _uint8ArrayToHexString(compressedBytes); + } + + /// Decompresses hex bytes using zstd compression algorithm and returns decompressed hex bytes. + function _zstdDecompress(bytesHex) { + const bytes = _hexStringToUint8Array(bytesHex); + const decompressedBytes = zstd.ZstdSimple.decompress(bytes); + return _uint8ArrayToHexString(decompressedBytes); + } + + // Converts a hex string into a byte array. + function _hexStringToUint8Array(hexString) { + // Ensure the hex string length is even + if (hexString.length % 2 !== 0) { + throw new Error('Invalid hex string'); + } + + // Create a Uint8Array + const byteArray = new Uint8Array(hexString.length / 2); + + // Parse the hex string into byte values + for (let i = 0; i < hexString.length; i += 2) { + byteArray[i / 2] = parseInt(hexString.substr(i, 2), 16); + } + + return byteArray; + } + + // Converts a byte array into a hex string. + function _uint8ArrayToHexString(uint8Array) { + return Array.from(uint8Array) + .map(byte => byte.toString(16).padStart(2, '0')) + .join(''); + } + + const catalyst_compression = { + brotliCompress: _brotliCompress, + brotliDecompress: _brotliDecompress, + zstdCompress: _zstdCompress, + zstdDecompress: _zstdDecompress, + }; + + // replace the initial handler with the actual handler. + self.onmessage = (event) => { + const { id, action, bytesHex } = event.data; + if (catalyst_compression[action]) { + try { + const result = catalyst_compression[action](bytesHex); + self.postMessage({ id, result }); + } catch (error) { + self.postMessage({ id, error: error.message }); + } + } else { + self.postMessage({ id, error: 'Unknown action' }); + } + }; + + // send an event to notify the main thread that the worker is ready. + self.postMessage({ initialized: true }); +}).catch(error => { + self.postMessage({ error: `Failed to load modules: ${error.message}` }); +}) \ No newline at end of file diff --git a/catalyst_voices_packages/catalyst_compression/catalyst_compression_web/lib/src/interop/catalyst_compression_interop.dart b/catalyst_voices_packages/catalyst_compression/catalyst_compression_web/lib/src/interop/catalyst_compression_interop.dart index 0c5e8d84cf2..c912e779688 100644 --- a/catalyst_voices_packages/catalyst_compression/catalyst_compression_web/lib/src/interop/catalyst_compression_interop.dart +++ b/catalyst_voices_packages/catalyst_compression/catalyst_compression_web/lib/src/interop/catalyst_compression_interop.dart @@ -11,28 +11,28 @@ import 'package:convert/convert.dart'; /// The bytes are transferred as hex string due to /// raw bytes not being supported by the js_interop. @JS() -external JSString brotliCompress(JSString bytes); +external JSPromise brotliCompress(JSString bytes); /// Decompresses the [bytes] using brotli algorithm. /// /// The bytes are transferred as hex string due to /// raw bytes not being supported by the js_interop. @JS() -external JSString brotliDecompress(JSString bytes); +external JSPromise brotliDecompress(JSString bytes); /// Compresses the [bytes] using zstd algorithm. /// /// The bytes are transferred as hex string due to /// raw bytes not being supported by the js_interop. @JS() -external JSString zstdCompress(JSString bytes); +external JSPromise zstdCompress(JSString bytes); /// Decompresses the [bytes] using zstd algorithm. /// /// The bytes are transferred as hex string due to /// raw bytes not being supported by the js_interop. @JS() -external JSString zstdDecompress(JSString bytes); +external JSPromise zstdDecompress(JSString bytes); /// The JS implementation of brotli compressor. class JSBrotliCompressor implements CatalystCompressor { @@ -40,15 +40,15 @@ class JSBrotliCompressor implements CatalystCompressor { const JSBrotliCompressor(); @override - List compress(List bytes) { - final data = brotliCompress(hex.encode(bytes).toJS).toDart; - return hex.decode(data); + Future> compress(List bytes) async { + final data = await brotliCompress(hex.encode(bytes).toJS).toDart; + return hex.decode(data.toDart); } @override - List decompress(List bytes) { - final data = brotliDecompress(hex.encode(bytes).toJS).toDart; - return hex.decode(data); + Future> decompress(List bytes) async { + final data = await brotliDecompress(hex.encode(bytes).toJS).toDart; + return hex.decode(data.toDart); } } @@ -65,20 +65,20 @@ class JSZstdCompressor implements CatalystCompressor { const JSZstdCompressor(); @override - List compress(List bytes) { + Future> compress(List bytes) async { if (bytes.length < _minLength) { throw CompressionNotSupportedException( 'Bytes too short, actual: ${bytes.length}, required: $_minLength', ); } - final data = zstdCompress(hex.encode(bytes).toJS).toDart; - return hex.decode(data); + final data = await zstdCompress(hex.encode(bytes).toJS).toDart; + return hex.decode(data.toDart); } @override - List decompress(List bytes) { - final data = zstdDecompress(hex.encode(bytes).toJS).toDart; - return hex.decode(data); + Future> decompress(List bytes) async { + final data = await zstdDecompress(hex.encode(bytes).toJS).toDart; + return hex.decode(data.toDart); } }