From 87ffdaa63cff7e37ae33ac8bb369ad467b0baf06 Mon Sep 17 00:00:00 2001 From: Pierre Krieger Date: Fri, 10 Dec 2021 13:10:31 +0100 Subject: [PATCH 1/5] Add a `maxUtf8BytesSize` parameter to `databaseContent` --- bin/light-base/src/lib.rs | 25 ++++++++++++++++++++++--- bin/wasm-node/javascript/demo/demo.js | 1 + bin/wasm-node/javascript/src/index.d.ts | 7 ++++++- bin/wasm-node/javascript/src/index.js | 11 +++++++++-- bin/wasm-node/javascript/src/worker.js | 12 +++++++++++- bin/wasm-node/rust/src/bindings.rs | 9 +++++++-- bin/wasm-node/rust/src/lib.rs | 5 +++-- 7 files changed, 59 insertions(+), 11 deletions(-) diff --git a/bin/light-base/src/lib.rs b/bin/light-base/src/lib.rs index a575a0d5ec..0dead217aa 100644 --- a/bin/light-base/src/lib.rs +++ b/bin/light-base/src/lib.rs @@ -893,11 +893,18 @@ impl Client { /// If the database content can't be obtained because not enough information is known about /// the chain, a dummy value is intentionally returned. /// + /// `max_size` can be passed force the output of the function to be smaller than the given + /// value. + /// /// # Panic /// /// Panics if the [`ChainId`] is invalid. /// - pub fn database_content(&self, chain_id: ChainId) -> impl Future { + pub fn database_content( + &self, + chain_id: ChainId, + max_size: usize, + ) -> impl Future { let mut services = match self.public_api_chains.get(chain_id.0) { Some(PublicApiChain::Ok { key, .. }) => { // Clone the services initialization future. @@ -918,11 +925,23 @@ impl Client { // Finally getting the database. // If the database can't be obtained, we just return a dummy value that will intentionally // fail to decode if passed back. - services + let database_content = services .sync_service .serialize_chain_information() .await - .unwrap_or_else(|| "".into()) + .unwrap_or_else(|| "".into()); + + // Cap the database length to the requested max length. + if database_content.len() > max_size { + let dummy_message = ""; + if dummy_message.len() >= max_size { + String::new() + } else { + dummy_message.to_owned() + } + } else { + database_content + } } } } diff --git a/bin/wasm-node/javascript/demo/demo.js b/bin/wasm-node/javascript/demo/demo.js index 31a04c51ce..dcf55ad999 100644 --- a/bin/wasm-node/javascript/demo/demo.js +++ b/bin/wasm-node/javascript/demo/demo.js @@ -60,6 +60,7 @@ const client = smoldot.start({ // WebSocket connection has been established. client .addChain({ chainSpec: westend }) + .then((chain) => chain.databaseContent()) .catch((error) => { console.error("Error while adding chain: " + error); process.exit(1); diff --git a/bin/wasm-node/javascript/src/index.d.ts b/bin/wasm-node/javascript/src/index.d.ts index 5fbe1b4c38..fcb6190207 100644 --- a/bin/wasm-node/javascript/src/index.d.ts +++ b/bin/wasm-node/javascript/src/index.d.ts @@ -118,10 +118,15 @@ export interface Chain { * * The content of the string is opaque and shouldn't be decoded. * + * A parameter can be passed to indicate the maximum length of the database content (in number + * of bytes this string would occupy in the UTF-8 encoding). Smoldot will never go above this + * limit, and the higher this limit is the more information it can include. Not passing any + * value means "unbounded". + * * @throws {AlreadyDestroyedError} If the chain has been removed or the client has been terminated. * @throws {CrashError} If the background client has crashed. */ - databaseContent(): Promise; + databaseContent(maxUtf8BytesSize?: number): Promise; /** * Disconnects from the blockchain. diff --git a/bin/wasm-node/javascript/src/index.js b/bin/wasm-node/javascript/src/index.js index 67afc223c9..cb22c23894 100644 --- a/bin/wasm-node/javascript/src/index.js +++ b/bin/wasm-node/javascript/src/index.js @@ -132,11 +132,12 @@ export function start(config) { throw new JsonRpcDisabledError(); worker.postMessage({ ty: 'request', request, chainId }); }, - databaseContent: () => { + databaseContent: (maxUtf8BytesSize) => { if (workerError) return Promise.reject(workerError); if (chainId === null) return Promise.reject(new AlreadyDestroyedError()); + let resolve; let reject; const promise = new Promise((res, rej) => { @@ -144,7 +145,13 @@ export function start(config) { reject = rej; }); chainsDatabaseContentPromises.get(chainId).push({ resolve, reject }); - worker.postMessage({ ty: 'databaseContent', chainId }); + + const twoPower32 = (1 << 30) * 4; // `1 << 31` and `1 << 32` in JavaScript don't give the value that you expect. + const maxSize = maxUtf8BytesSize || (twoPower32 - 1); + const cappedMaxSize = (maxSize >= twoPower32) ? (twoPower32 - 1) : maxSize; + + worker.postMessage({ ty: 'databaseContent', chainId, maxUtf8BytesSize: cappedMaxSize }); + return promise; }, remove: () => { diff --git a/bin/wasm-node/javascript/src/worker.js b/bin/wasm-node/javascript/src/worker.js index 3e08451e0e..87d1ea534a 100644 --- a/bin/wasm-node/javascript/src/worker.js +++ b/bin/wasm-node/javascript/src/worker.js @@ -89,7 +89,17 @@ const injectMessage = (instance, message) => { compat.postMessage({ kind: 'chainRemoved' }); } else if (message.ty == 'databaseContent') { - instance.exports.database_content(message.chainId); + // The value of `maxUtf8BytesSize` is guaranteed (by `index.js`) to always fit in 32 bits, in + // other words, that `maxUtf8BytesSize < (1 << 32)`. + // We need to perform a conversion in such a way that the the bits of the output of + // `ToInt32(converted)`, when interpreted as u32, is equal to `maxUtf8BytesSize`. + // See ToInt32 here: https://tc39.es/ecma262/#sec-toint32 + // Note that the code below has been tested against example values. Please be very careful + // if you decide to touch it. + const twoPower31 = (1 << 30) * 2; // `1 << 31` in JavaScript doesn't give the value that you expect. + const converted = (message.maxUtf8BytesSize >= twoPower31) ? + (message.maxUtf8BytesSize - (twoPower31 * 2)) : message.maxUtf8BytesSize; + instance.exports.database_content(message.chainId, converted); } else throw new Error('unrecognized message type'); diff --git a/bin/wasm-node/rust/src/bindings.rs b/bin/wasm-node/rust/src/bindings.rs index 767c8a4fb9..f95c4a6ff3 100644 --- a/bin/wasm-node/rust/src/bindings.rs +++ b/bin/wasm-node/rust/src/bindings.rs @@ -357,11 +357,16 @@ pub extern "C" fn json_rpc_send(text_ptr: u32, text_len: u32, chain_id: u32) { /// Calling this function multiple times will lead to multiple calls to [`database_content_ready`], /// with potentially different values. /// +/// The `max_size` parameter contains the maximum length, in bytes, of the value that will be +/// provided back. Please be aware that passing a `u32` accross the FFI boundary can be tricky. +/// From the Wasm perspective, the parameter of this function is actually a `i32̀ that is then +/// reinterpreted as a `u32̀ . +/// /// [`database_content_ready`] will not be called if you remove the chain with [`remove_chain`] /// while the operation is in progress. #[no_mangle] -pub extern "C" fn database_content(chain_id: u32) { - super::database_content(chain_id) +pub extern "C" fn database_content(chain_id: u32, max_size: u32) { + super::database_content(chain_id, max_size) } /// Must be called in response to [`start_timer`] after the given duration has passed. diff --git a/bin/wasm-node/rust/src/lib.rs b/bin/wasm-node/rust/src/lib.rs index 79f11d5fc7..c1feb5e422 100644 --- a/bin/wasm-node/rust/src/lib.rs +++ b/bin/wasm-node/rust/src/lib.rs @@ -327,14 +327,15 @@ fn json_rpc_send(ptr: u32, len: u32, chain_id: u32) { } } -fn database_content(chain_id: u32) { +fn database_content(chain_id: u32, max_size: u32) { let client_chain_id = smoldot_light_base::ChainId::from(chain_id); let mut client_lock = CLIENT.lock().unwrap(); let (client, tasks_spawner) = client_lock.as_mut().unwrap(); let task = { - let future = client.database_content(client_chain_id); + let max_size = usize::try_from(max_size).unwrap(); + let future = client.database_content(client_chain_id, max_size); async move { let content = future.await; unsafe { From 4a821f61d3467ad44fbab0ef8ef1b8dbf0f66e98 Mon Sep 17 00:00:00 2001 From: Pierre Krieger Date: Fri, 10 Dec 2021 13:12:07 +0100 Subject: [PATCH 2/5] Add note about tests --- bin/wasm-node/javascript/src/worker.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/wasm-node/javascript/src/worker.js b/bin/wasm-node/javascript/src/worker.js index 87d1ea534a..d1a7d554fa 100644 --- a/bin/wasm-node/javascript/src/worker.js +++ b/bin/wasm-node/javascript/src/worker.js @@ -95,7 +95,8 @@ const injectMessage = (instance, message) => { // `ToInt32(converted)`, when interpreted as u32, is equal to `maxUtf8BytesSize`. // See ToInt32 here: https://tc39.es/ecma262/#sec-toint32 // Note that the code below has been tested against example values. Please be very careful - // if you decide to touch it. + // if you decide to touch it. Ideally it would be unit-tested, but since it concerns the FFI + // layer between JS and Rust, writing unit tests would be extremely complicated. const twoPower31 = (1 << 30) * 2; // `1 << 31` in JavaScript doesn't give the value that you expect. const converted = (message.maxUtf8BytesSize >= twoPower31) ? (message.maxUtf8BytesSize - (twoPower31 * 2)) : message.maxUtf8BytesSize; From 5cbe47f0f3aa6b7259fdecca5638f0e7e2b95c0c Mon Sep 17 00:00:00 2001 From: Pierre Krieger Date: Fri, 10 Dec 2021 13:12:52 +0100 Subject: [PATCH 3/5] Fix unicode characters accidentally introduced --- bin/wasm-node/rust/src/bindings.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/wasm-node/rust/src/bindings.rs b/bin/wasm-node/rust/src/bindings.rs index f95c4a6ff3..6325f0eeb5 100644 --- a/bin/wasm-node/rust/src/bindings.rs +++ b/bin/wasm-node/rust/src/bindings.rs @@ -359,8 +359,8 @@ pub extern "C" fn json_rpc_send(text_ptr: u32, text_len: u32, chain_id: u32) { /// /// The `max_size` parameter contains the maximum length, in bytes, of the value that will be /// provided back. Please be aware that passing a `u32` accross the FFI boundary can be tricky. -/// From the Wasm perspective, the parameter of this function is actually a `i32̀ that is then -/// reinterpreted as a `u32̀ . +/// From the Wasm perspective, the parameter of this function is actually a `i32` that is then +/// reinterpreted as a `u32`. /// /// [`database_content_ready`] will not be called if you remove the chain with [`remove_chain`] /// while the operation is in progress. From 1a74038eadec3dd02edb38d845de2d3b0d3d0793 Mon Sep 17 00:00:00 2001 From: Pierre Krieger Date: Fri, 10 Dec 2021 13:23:29 +0100 Subject: [PATCH 4/5] Revert changes to demo.js --- bin/wasm-node/javascript/demo/demo.js | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/wasm-node/javascript/demo/demo.js b/bin/wasm-node/javascript/demo/demo.js index dcf55ad999..31a04c51ce 100644 --- a/bin/wasm-node/javascript/demo/demo.js +++ b/bin/wasm-node/javascript/demo/demo.js @@ -60,7 +60,6 @@ const client = smoldot.start({ // WebSocket connection has been established. client .addChain({ chainSpec: westend }) - .then((chain) => chain.databaseContent()) .catch((error) => { console.error("Error while adding chain: " + error); process.exit(1); From 9784b920fad0cbccb5bb61714f1c84635f81007b Mon Sep 17 00:00:00 2001 From: Pierre Krieger Date: Fri, 10 Dec 2021 13:26:34 +0100 Subject: [PATCH 5/5] Doc improvement --- bin/wasm-node/javascript/src/index.d.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/wasm-node/javascript/src/index.d.ts b/bin/wasm-node/javascript/src/index.d.ts index fcb6190207..209a41df6c 100644 --- a/bin/wasm-node/javascript/src/index.d.ts +++ b/bin/wasm-node/javascript/src/index.d.ts @@ -118,10 +118,10 @@ export interface Chain { * * The content of the string is opaque and shouldn't be decoded. * - * A parameter can be passed to indicate the maximum length of the database content (in number - * of bytes this string would occupy in the UTF-8 encoding). Smoldot will never go above this - * limit, and the higher this limit is the more information it can include. Not passing any - * value means "unbounded". + * A parameter can be passed to indicate the maximum length of the returned value (in number + * of bytes this string would occupy in the UTF-8 encoding). The higher this limit is the more + * information can be included. This parameter is optional, and not passing any value means + * "unbounded". * * @throws {AlreadyDestroyedError} If the chain has been removed or the client has been terminated. * @throws {CrashError} If the background client has crashed.