Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a maxUtf8BytesSize parameter to databaseContent #1773

Merged
merged 5 commits into from
Dec 10, 2021
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
25 changes: 22 additions & 3 deletions bin/light-base/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -893,11 +893,18 @@ impl<TChain, TPlat: Platform> Client<TChain, TPlat> {
/// 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<Output = String> {
pub fn database_content(
&self,
chain_id: ChainId,
max_size: usize,
) -> impl Future<Output = String> {
let mut services = match self.public_api_chains.get(chain_id.0) {
Some(PublicApiChain::Ok { key, .. }) => {
// Clone the services initialization future.
Expand All @@ -918,11 +925,23 @@ impl<TChain, TPlat: Platform> Client<TChain, TPlat> {
// 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(|| "<unknown>".into())
.unwrap_or_else(|| "<unknown>".into());

// Cap the database length to the requested max length.
if database_content.len() > max_size {
let dummy_message = "<too-large>";
if dummy_message.len() >= max_size {
String::new()
} else {
dummy_message.to_owned()
}
} else {
database_content
}
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion bin/wasm-node/javascript/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.
*/
databaseContent(): Promise<string>;
databaseContent(maxUtf8BytesSize?: number): Promise<string>;

/**
* Disconnects from the blockchain.
Expand Down
11 changes: 9 additions & 2 deletions bin/wasm-node/javascript/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,19 +132,26 @@ 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) => {
resolve = res;
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: () => {
Expand Down
13 changes: 12 additions & 1 deletion bin/wasm-node/javascript/src/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,18 @@ 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. 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;
instance.exports.database_content(message.chainId, converted);

} else
throw new Error('unrecognized message type');
Expand Down
9 changes: 7 additions & 2 deletions bin/wasm-node/rust/src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions bin/wasm-node/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down