Skip to content

Commit

Permalink
[move-bytecode-template] All new and shiny (MystenLabs#16583)
Browse files Browse the repository at this point in the history
## Description 

- reimplements methods to work on Uint8Array instead of "hex string"
- better wasm-typing for return values
- adds `update_contants` method to patch consts by (value + type) pair

## Test Plan 

Features tests.

---
If your changes are not user-facing and do not break anything, you can
skip the following section. Otherwise, please briefly describe what has
changed under the Release Notes section.

### Type of Change (Check all that apply)

- [ ] protocol change
- [ ] user-visible impact
- [ ] breaking change for a client SDKs
- [ ] breaking change for FNs (FN binary must upgrade)
- [ ] breaking change for validators or node operators (must upgrade
binaries)
- [ ] breaking change for on-chain data layout
- [ ] necessitate either a data wipe or data migration

### Release notes
  • Loading branch information
damirka authored Mar 11, 2024
1 parent b33e05c commit 8cf578a
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 86 deletions.
5 changes: 3 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

133 changes: 126 additions & 7 deletions sdk/move-bytecode-template/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,143 @@
# Move Binary Format
# Move Bytecode Template

This crate builds a WASM binary for the `move-language/move-binary-format` allowing bytecode serialization and deserialization in various environments. The main environment this package targets is "web".
Move Bytecode Template allows updating a pre-compiled bytecode, so that a standard template could be customized and used to publish new modules on Sui directly in the browser. Hence, removing the need for a backend to compile new modules.

This crate builds a WASM binary for the `move-language/move-binary-format` allowing bytecode serialization and deserialization in various environments. The main target for this package is "web".

## Applications

This package is a perfect fit for the following applications:

- Publishing new Coins
- Publishing TransferPolicies
- Initializing any base type with a custom sub-type

## Example of a Template Module

The following code is a close-copy of the `Coin` example from the [Move by Example](https://examples.sui.io/samples/coin.html) book.

```move
module 0x0::template {
use std::option;
use sui::coin;
use sui::transfer;
use sui::tx_context::{Self, TxContext};
/// The OTW for the Coin
struct TEMPLATE has drop {}
const DECIMALS: u8 = 0;
const SYMBOL: vector<u8> = b"TMPL";
const NAME: vector<u8> = b"Template Coin";
const DESCRIPTION: vector<u8> = b"Template Coin Description";
/// Init the Coin
fun init(witness: TEMPLATE, ctx: &mut TxContext) {
let (treasury, metadata) = coin::create_currency(
witness, DECIMALS, SYMBOL, NAME, DESCRIPTION, option::none(), ctx
);
transfer::public_transfer(treasury, tx_context::sender(ctx));
transfer::public_transfer(metadata, tx_context::sender(ctx));
}
}
```

To update the identifiers, you can use the `update_identifiers` function.

```ts
import { fromHEX, update_identifiers } from '@mysten/move-bytecode-template';

let bytecode = /* ... */;
let updated = update_identifiers(bytecode, {
"TEMPLATE": "MY_MODULE",
"template": "my_module"
});

console.assert(updated != bytecode, 'identifiers were not updated!');
```

To update constants in the bytecode, you can use the `update_constants` function. For each constant you need to supply new value as BCS bytes, existing value as BCS, and the type of the constant (as a string: `U8`, `U16` ... `U256`, `Address`, `Vector(U8)` and so on).

```ts
import * as template from '@mysten/move-bytecode-template';
import { bcs } from '@mysten/bcs';

// please, manually scan the existing values, this operation is very sensitive
console.log(template.get_constants(bytecode));

let updated;

// Update DECIMALS
updated = update_constants(
bytecode,
bcs.u8().serialize(3).toBytes(), // new value
bcs.u8().serialize(6).toBytes(), // current value
'U8', // type of the constant
);

// Update SYMBOL
updated = update_constants(
updated,
bcs.vector(bcs.string()).serialize('MYC').toBytes(), // new value
bcs.vector(bcs.string()).serialize('TMPL').toBytes(), // current value
'Vector(U8)', // type of the constant
);

// Update NAME
updated = update_constants(
updated,
bcs.vector(bcs.string()).serialize('My Coin').toBytes(), // new value
bcs.vector(bcs.string()).serialize('Template Coin').toBytes(), // current value
'Vector(U8)', // type of the constant
);
```

## Usage in Web applications

The package consists of code and a wasm binary. While the former can be imported directly, the latter should be made available in static / public assets as a Web application. Initialization needs to be performed via a URL, and once completed, other functions become available.

```ts
import init, initSync, * as wasm from '@mysten/move-binary-format';
import init, initSync, * as template from '@mysten/move-bytecode-template';

await init('...path to /move_binary_format_bg.wasm');
await init('path/to/move_binary_format_bg.wasm');
// alternatively initSync(...);

let version = wasm.version();
let json = wasm.deserialize('a11ceb0b06....');
let bytes = wasm.serialize(json);
let version = template.version();
let json = template.deserialize(fromHEX('a11ceb0b06....'));
let bytes = template.serialize(json);

console.assert(json == bytes, '(de)serialization failed!');
```

## Using with Vite

To use this package with Vite, you need to import the source file and the wasm binary.

```ts
import init, * as template from "@mysten/move-bytecode-template";
import url from '@mysten/move-bytecode-template/move_bytecode_template_bg.wasm?url';
```

Later, you can initialize the package with the URL.

```ts
await init(url);
```

Lastly, once the package is initialized, you can use the functions as described in the previous section.

```ts
const templateBytecode = fromHEX("a11ceb0b06....");

template.deserialize(templateBytecode);
template.version();
template.update_identifiers(templateBytecode, {
"TEMPLATE": "MY_MODULE",
"template": "my_module"
});
```

## Build locally

To build the binary, you need to have Rust installed and then the `wasm-pack`. The installation script [can be found here](https://rustwasm.github.io/wasm-pack/).
Expand Down
1 change: 1 addition & 0 deletions sdk/move-bytecode-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"author": "Mysten Labs <build@mystenlabs.com>",
"license": "Apache-2.0",
"devDependencies": {
"@mysten/bcs": "workspace:*",
"@mysten/build-scripts": "workspace:*",
"typescript": "^5.3.3",
"vitest": "^0.33.0",
Expand Down
136 changes: 113 additions & 23 deletions sdk/move-bytecode-template/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::collections::HashMap;
use move_binary_format::{file_format::StructFieldInformation, CompiledModule};
use move_core_types::identifier::Identifier;
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen::to_value;
use serde_wasm_bindgen::{from_value, to_value};
use wasm_bindgen::{prelude::*, JsValue};

const VERSION: &str = env!("CARGO_PKG_VERSION");
Expand All @@ -18,21 +18,35 @@ pub fn version() -> String {
}

#[wasm_bindgen]
/// Deserialize the bytecode into a JSON string.
pub fn deserialize(binary: String) -> Result<JsValue, JsErr> {
let bytes = hex::decode(binary)?;
let compiled_module = CompiledModule::deserialize_with_defaults(&bytes[..])?;
let serialized = serde_json::to_string(&compiled_module)?;
Ok(to_value(&serialized)?)
/// Deserialize the `Uint8Array`` bytecode into a JSON object.
///
/// ```javascript
/// import * as template from '@mysten/move-binary-template';
///
/// const json = template.deserialize( binary );
/// console.log( json, json.identifiers );
/// ```
pub fn deserialize(binary: &[u8]) -> Result<JsValue, JsErr> {
let compiled_module = CompiledModule::deserialize_with_defaults(binary)?;
Ok(to_value(&compiled_module)?)
}

#[wasm_bindgen]
/// Perform an operation on a bytecode string - deserialize, patch the identifiers
/// and serialize back to a bytecode string.
pub fn update_identifiers(binary: String, map: JsValue) -> Result<JsValue, JsErr> {
let bytes = hex::decode(binary)?;
/// Update the identifiers in the module bytecode, given a map of old -> new identifiers.
/// Returns the updated bytecode.
///
/// ```javascript
/// import * as template from '@mysten/move-binary-template';
///
/// const updated = template.update_identifiers( binary, {
/// 'TEMPLATE': 'NEW_VALUE',
/// 'template': 'new_value',
/// 'Name': 'NewName'
/// });
/// ```
pub fn update_identifiers(binary: &[u8], map: JsValue) -> Result<Box<[u8]>, JsErr> {
let updates: HashMap<String, String> = serde_wasm_bindgen::from_value(map)?;
let mut compiled_module = CompiledModule::deserialize_with_defaults(&bytes[..])?;
let mut compiled_module = CompiledModule::deserialize_with_defaults(binary)?;

// First update the identifiers.
for ident in compiled_module.identifiers.iter_mut() {
Expand Down Expand Up @@ -91,21 +105,97 @@ pub fn update_identifiers(binary: String, map: JsValue) -> Result<JsValue, JsErr
message: err.to_string(),
})?;

Ok(to_value(&hex::encode(binary))?)
Ok(binary.into())
}

#[wasm_bindgen]
/// Serialize the JSON module into a HEX string.
pub fn serialize(json_module: String) -> Result<JsValue, JsErr> {
let compiled_module: CompiledModule = serde_json::from_str(json_module.as_str())?;
/// Updates a constant in the constant pool. Because constants don't have names,
/// the only way to identify them is by their type and value.
///
/// The value of a constant is BCS-encoded and the type is a string representation
/// of the `SignatureToken` enum. String identifier for `SignatureToken` is a
/// capitalized version of the type: U8, Address, Vector(Bool), Vector(U8), etc.
///
/// ```javascript
/// import * as template from '@mysten/move-binary-template';
/// import { bcs } from '@mysten/bcs';
///
/// let binary = template.update_constants(
/// binary, // Uint8Array
/// bcs.u64().serialize(0).toBytes(), // new value
/// bcs.u64().serialize(100000).toBytes(), // old value
/// 'U64' // type
/// );
/// ```
pub fn update_constants(
binary: &[u8],
new_value: &[u8],
expected_value: &[u8],
expected_type: String,
) -> Result<Box<[u8]>, JsErr> {
let mut compiled_module = CompiledModule::deserialize_with_defaults(&binary)?;

compiled_module.constant_pool.iter_mut().for_each(|handle| {
if handle.data == expected_value && expected_type == format!("{:?}", handle.type_) {
handle.data = new_value.to_vec();
};
});

let mut binary = Vec::new();
compiled_module
.serialize(&mut binary)
.map_err(|err| JsErr {
display: format!("{}", err),
message: err.to_string(),
})?;
Ok(to_value(&hex::encode(binary))?)

Ok(binary.into())
}

#[wasm_bindgen]
#[derive(Serialize, Deserialize)]
/// A transformed constant from the constant pool.
pub struct Constant {
type_: String,
value_bcs: Box<[u8]>,
}

#[wasm_bindgen]
/// Convenience method to analyze the constant pool; returns all constants in order
/// with their type and BCS value.
///
/// ```javascript
/// import * as template from '@mysten/move-binary-template';
///
/// let consts = template.get_constants(binary);
/// ```
pub fn get_constants(binary: &[u8]) -> Result<JsValue, JsErr> {
let compiled_module = CompiledModule::deserialize_with_defaults(&binary)?;
let constants: Vec<Constant> = compiled_module
.constant_pool
.into_iter()
.map(|constant| Constant {
type_: format!("{:?}", constant.type_),
value_bcs: constant.data.into(),
})
.collect();

Ok(to_value(&constants)?)
}

#[wasm_bindgen]
/// Serialize the JSON module into a `Uint8Array` (bytecode).
pub fn serialize(json_module: JsValue) -> Result<Box<[u8]>, JsErr> {
let compiled_module: CompiledModule = from_value(json_module)?;
let mut binary = Vec::new();
compiled_module
.serialize(&mut binary)
.map_err(|err| JsErr {
display: format!("{}", err),
message: err.to_string(),
})?;

Ok(binary.into())
}

#[derive(Serialize, Deserialize)]
Expand All @@ -117,6 +207,12 @@ pub struct JsErr {
display: String,
}

impl Into<JsValue> for JsErr {
fn into(self) -> JsValue {
to_value(&self).unwrap()
}
}

impl<T: std::error::Error> From<T> for JsErr {
fn from(err: T) -> Self {
JsErr {
Expand All @@ -125,9 +221,3 @@ impl<T: std::error::Error> From<T> for JsErr {
}
}
}

impl From<JsErr> for JsValue {
fn from(err: JsErr) -> Self {
to_value(&err).unwrap()
}
}
16 changes: 0 additions & 16 deletions sdk/move-bytecode-template/tests/serialization.test.ts

This file was deleted.

Loading

0 comments on commit 8cf578a

Please sign in to comment.