diff --git a/doc/src/build/sui-json.md b/doc/src/build/sui-json.md index 50b46fad02ad4..1f2d4378fe148 100644 --- a/doc/src/build/sui-json.md +++ b/doc/src/build/sui-json.md @@ -26,7 +26,7 @@ This table shows the restrictions placed on JSON types to make them SuiJSON comp U64 -(U128 not supported yet) +(U128 is encoded as String) @@ -41,6 +41,10 @@ Address ObjectID TypeTag + +Identifier + +Unsigned Integer (128 bit max) @@ -54,7 +58,7 @@ TypeTag Array - Must be homogenous and of SuiJSON type + Must be homogenous JSON and of SuiJSON type Vector @@ -88,17 +92,19 @@ TypeTag ## Type coercion reasoning Due to the loosely typed nature of JSON/SuiJSON and the strongly typed nature of Move types, we sometimes need to overload SuiJSON types to represent multiple Move types. \ -For example `SuiJSON::Number` can represent both _U8_ and _U64_. This means we have to coerce and sometimes convert types. +For example `SuiJSON::Number` can represent both *U8* and *U64*. This means we have to coerce and sometimes convert types. Which type we coerce depends on the expected Move type. For example, if the Move function expects a U8, we must have received a `SuiJSON::Number` with a value less than 256. More importantly, we have no way to easily express Move addresses in JSON, so we encode them as hex strings prefixed by `0x`. +Additionally, Move supports U128 but JSON doesn't. As a result we allow encoding numbers as strings. + ## Type coercion rules - @@ -118,9 +124,20 @@ Which type we coerce depends on the expected Move type. For example, if the Move - - - - - - - + + - - + + + + + + + @@ -186,9 +262,12 @@ Which type we coerce depends on the expected Move type. For example, if the Move [[3,600],[],[0,7,4]]: nested U64 vector - @@ -199,9 +278,9 @@ Which type we coerce depends on the expected Move type. For example, if the Move U8 vectors represented as UTF-8 (and ASCII) strings. -
Move Type SuiJSON + SuiJSON Representations Valid Examples
U8 Unsigned number < 256 + +Three formats are supported + +* Unsigned number < 256. +* Decimal string with value < 256. +* One byte hex string prefixed with `0x`. 7 + + + 7 + + "70" + + "0x43" -5: negative not allowed @@ -130,34 +147,57 @@ Which type we coerce depends on the expected Move type. For example, if the Move 300: U8 must be less than 256 +" 9": Spaces not allowed in string + +"9A": Hex num must be prefixed with `0x` + +"0x09CD": Too large for U8 +
U64 Unsigned number < U64::MAX + +Similarly to U8, three formats are supported + +* Unsigned number < U64::MAX. +* Decimal string with value < U64::MAX. +* Up to 8 byte hex string prefixed with `0x`. + 12345 + Extrapolate above examples 184467440737095516159: must be less than U64::MAX + Extrapolate above examples
U128 Not supported yet - N/A + + +Two formats are supported + +* Decimal string with value < U128::MAX. +* Up to 16 byte hex string prefixed with `0x`. + + "74794734937420002470" + + "0x2B1A39A1514E1D8A7CE" + + 34: Although this is a valid u128 number, it must be encoded as a string
Address 20 byte hex string prefixed with 0x 0x2B1A39A1514E1D8A7CE45919CFEB4FEE70B4E011 + "0x2B1A39A1514E1D8A7CE45919CFEB4FEE70B4E011" 0x2B1A39: string too short @@ -171,11 +211,47 @@ Which type we coerce depends on the expected Move type. For example, if the Move 16 byte hex string prefixed with 0x 0x2B1A39A1514E1D8A7CE45919CFEB4FEE + "0x2B1A39A1514E1D8A7CE45919CFEB4FEE" Similar to above
Identifier + Typically used for module and function names. Encoded as one of the following: + + 1. A String whose first character is a letter and the remaining characters are letters, digits or underscore. + + 2. A String whose first character is an underscore, and there is at least one further letter, digit or underscore + + + "function", + + "_function", + + "some_name", + + "\___\_some_name", + + "Another" + + + "_": missing trailing underscore, digit or letter, + + "8name": cannot start with digit, + + ".function": cannot start with period, + + " ": cannot be empty space, + + "func name": cannot have spaces + +
Vector<Move Type> [1,2,3,false]: not homogenous + [1,2,3,false]: not homogenous JSON [1,2,null,4]: invalid elements + +[1,2,"7"]: although we allow encoding numbers as strings meaning this array can evaluate to [1,2,7], the array is still ambiguous so it fails the homogeneity check. +
√®ˆbo72 √∂†∆˚–œ∑π2ie: UTF-8 + "√®ˆbo72 √∂†∆˚–œ∑π2ie": UTF-8 - abcdE738-2 _=?: ASCII + "abcdE738-2 _=?": ASCII diff --git a/sui/src/rest_server.rs b/sui/src/rest_server.rs index 8ece53532c27a..2e6d3a84d7f01 100644 --- a/sui/src/rest_server.rs +++ b/sui/src/rest_server.rs @@ -42,7 +42,7 @@ use sui_types::move_package::resolve_and_type_check; use sui_types::object::Object as SuiObject; use sui_types::object::ObjectRead; -const REST_SERVER_PORT: u16 = 5000; +const REST_SERVER_PORT: u16 = 5001; const REST_SERVER_ADDR_IPV4: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); #[path = "unit_tests/rest_server_tests.rs"] diff --git a/sui/src/sui_json.rs b/sui/src/sui_json.rs index 7bdb90fc092ef..8510cbc18fb22 100644 --- a/sui/src/sui_json.rs +++ b/sui/src/sui_json.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use anyhow::anyhow; -use move_core_types::identifier::Identifier; +use move_core_types::{account_address::AccountAddress, identifier::Identifier}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; @@ -15,7 +15,6 @@ use sui_types::{ // Alias the type names for clarity use move_binary_format::normalized::{Function as MoveFunction, Type as NormalizedMoveType}; use serde_json::Value as JsonValue; -use serde_value::Value as SerdeValue; const HEX_PREFIX: &str = "0x"; @@ -23,6 +22,17 @@ const HEX_PREFIX: &str = "0x"; #[path = "unit_tests/sui_json.rs"] mod base_types_tests; +#[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize)] +pub enum IntermediateValue { + Bool(bool), + U8(u8), + U64(u64), + U128(u128), + Address(SuiAddress), + ObjectID(ObjectID), + Vector(Vec), +} + #[derive(Eq, PartialEq, Debug, Clone, Deserialize, Serialize, JsonSchema)] pub struct SuiJsonValue(JsonValue); impl SuiJsonValue { @@ -49,35 +59,33 @@ impl SuiJsonValue { } pub fn to_bcs_bytes(&self, typ: &NormalizedMoveType) -> Result, anyhow::Error> { - let serde_val = Self::to_serde_value(&self.0, typ)?; + let intermediate_val = Self::to_intermediate_value(&self.0, typ)?; fn inner_serialize( - ser_val: SerdeValue, + inter_val: IntermediateValue, ty: &NormalizedMoveType, ) -> Result, anyhow::Error> { - let ret = match ty { - NormalizedMoveType::Address => bcs::to_bytes(&ser_val)?[1..].to_vec(), - NormalizedMoveType::Vector(t) => { + let ser = match (inter_val.clone(), ty) { + (IntermediateValue::Bool(b), NormalizedMoveType::Bool) => bcs::to_bytes(&b)?, + (IntermediateValue::U8(n), NormalizedMoveType::U8) => bcs::to_bytes(&n)?, + (IntermediateValue::U64(n), NormalizedMoveType::U64) => bcs::to_bytes(&n)?, + (IntermediateValue::U128(n), NormalizedMoveType::U128) => bcs::to_bytes(&n)?, + + (IntermediateValue::Address(a), NormalizedMoveType::Address) => { + bcs::to_bytes(&AccountAddress::from(a))? + } + + // Not currently used + // (IntermediateValue::ObjectID(a), NormalizedMoveType::Address) => { + // bcs::to_bytes(&AccountAddress::from(a))? + // } + (IntermediateValue::Vector(v), NormalizedMoveType::Vector(move_type)) => { let mut inner_ser = vec![]; - // This must be an array. Checked in previous step - - let arr_len = match ser_val { - SerdeValue::Seq(s) => { - let l = s.len(); - for i in s { - // Serialize each - inner_ser.append(&mut inner_serialize(i, t)?); - } - l - } - SerdeValue::Bytes(b) => { - let l = b.len(); - - inner_ser.extend(b); - l - } - _ => return Err(anyhow!("Unable to serialize {:?} as vector", ser_val)), - }; + let arr_len = v.len(); + for i in v { + // Serialize each + inner_ser.append(&mut inner_serialize(i, move_type)?); + } // The data is already serialized, so ideally we just append // First serialize the types like they u8s // We use this to create the ULEB128 length prefix @@ -87,38 +95,55 @@ impl SuiJsonValue { ser_container.truncate(ser_container.len() - arr_len); // Append the actual data data ser_container.append(&mut inner_ser); + ser_container } - - _ => bcs::to_bytes(&ser_val)?, + _ => { + return Err(anyhow!( + "Unable to serialize {:?}. Expected {}", + inter_val, + ty + )) + } }; - Ok(ret) + Ok(ser) } - - inner_serialize(serde_val, typ) + inner_serialize(intermediate_val, typ) } pub fn to_json_value(&self) -> JsonValue { self.0.clone() } - fn to_serde_value( + fn to_intermediate_value( val: &JsonValue, typ: &NormalizedMoveType, - ) -> Result { + ) -> Result { let new_serde_value = match (val, typ.clone()) { // Bool to Bool is simple - (JsonValue::Bool(b), NormalizedMoveType::Bool) => SerdeValue::Bool(*b), + (JsonValue::Bool(b), NormalizedMoveType::Bool) => IntermediateValue::Bool(*b), - // In constructor, we have already checked that the number is unsigned int of at most U64 + // In constructor, we have already checked that the JSON number is unsigned int of at most U64 // Hence it is okay to unwrap() numbers (JsonValue::Number(n), NormalizedMoveType::U8) => { - SerdeValue::U8(u8::try_from(n.as_u64().unwrap())?) + IntermediateValue::U8(u8::try_from(n.as_u64().unwrap())?) + } + (JsonValue::Number(n), NormalizedMoveType::U64) => { + IntermediateValue::U64(n.as_u64().unwrap()) } - (JsonValue::Number(n), NormalizedMoveType::U64) => SerdeValue::U64(n.as_u64().unwrap()), - // U128 Not allowed for now - (_, NormalizedMoveType::U128) => unimplemented!("U128 not supported yet."), + // u8, u64, u128 can be encoded as String + (JsonValue::String(s), NormalizedMoveType::U8) => { + IntermediateValue::U8(u8::try_from(convert_string_to_u128(s.as_str())?)?) + } + (JsonValue::String(s), NormalizedMoveType::U64) => { + IntermediateValue::U64(u64::try_from(convert_string_to_u128(s.as_str())?)?) + } + (JsonValue::String(s), NormalizedMoveType::U128) => { + IntermediateValue::U128(convert_string_to_u128(s.as_str())?) + } + + // U256 Not allowed for now // We can encode U8 Vector as string in 2 ways // 1. If it starts with 0x, we treat it as hex strings, where each pair is a byte @@ -136,16 +161,16 @@ impl SuiJsonValue { // Else raw bytes s.as_bytes().to_vec() }; - SerdeValue::Bytes(vec) + IntermediateValue::Vector(vec.iter().map(|q| IntermediateValue::U8(*q)).collect()) } // We have already checked that the array is homogeneous in the constructor (JsonValue::Array(a), NormalizedMoveType::Vector(t)) => { - // Recursively build a SerdeValue array - SerdeValue::Seq( + // Recursively build an IntermediateValue array + IntermediateValue::Vector( a.iter() - .map(|i| Self::to_serde_value(i, &t)) - .collect::, _>>()?, + .map(|i| Self::to_intermediate_value(i, &t)) + .collect::, _>>()?, ) } @@ -155,7 +180,7 @@ impl SuiJsonValue { return Err(anyhow!("Address hex string must start with 0x.",)); } let r: SuiAddress = decode_bytes_hex(s.trim_start_matches(HEX_PREFIX))?; - SerdeValue::Bytes(r.to_vec()) + IntermediateValue::Address(r) } _ => return Err(anyhow!("Unexpected arg {} for expected type {}", val, typ)), }; @@ -251,7 +276,9 @@ fn check_and_serialize_pure_args( // Check that the args are what we expect or can be converted // Then return the serialized bcs value match curr.to_bcs_bytes(expected_pure_arg_type) { - Ok(a) => pure_args_serialized.push(a), + Ok(a) => { + pure_args_serialized.push(a.clone()); + } Err(e) => return Err(anyhow!("Unable to parse arg at pos: {}, err: {:?}", idx, e)), } } @@ -345,3 +372,20 @@ pub fn resolve_move_function_args( Ok((obj_args, pure_args_serialized)) } + +fn convert_string_to_u128(s: &str) -> Result { + // Try as normal number + if let Ok(v) = s.parse::() { + return Ok(v); + } + + // Check prefix + // For now only Hex supported + // TODO: add support for bin and octal? + + let s = s.trim().to_lowercase(); + if !s.starts_with(HEX_PREFIX) { + return Err(anyhow!("Unable to convert {s} to unsigned int.",)); + } + u128::from_str_radix(s.trim_start_matches(HEX_PREFIX), 16).map_err(|e| e.into()) +} diff --git a/sui/src/unit_tests/sui_json.rs b/sui/src/unit_tests/sui_json.rs index 5d018146a6254..ac5fadb35245f 100644 --- a/sui/src/unit_tests/sui_json.rs +++ b/sui/src/unit_tests/sui_json.rs @@ -19,6 +19,9 @@ fn test_json_is_homogenous() { let checks = vec![ (json!([1, 2, 3, true, 5, 6, 7]), false), (json!([1, 2, 3, 4, 5, 6, 7]), true), + // Although we can encode numbers as strings, we do not allow mixing primitive + // numbers and string encoded numbers + (json!([1, 2, "4", 4, 5, 6, 7]), false), (json!([1, 2, 3, 4, "", 6, 7]), false), (json!([]), true), (json!([[], 2, 3, 5, 6, 7]), false), @@ -50,7 +53,7 @@ fn test_json_is_valid_sui_json() { (json!([1, 2, 3, true, 5, 6, 7]), false), // Homogenous (json!([1, 2, 3, 4, 5, 6, 7]), true), - // String not allowed + // String allowed (json!("a string"), true), // Float not allowed (json!(1.3), false), @@ -85,6 +88,7 @@ fn test_basic_args_linter_pure_args() { let good_utf8_str = "enbeuf√12∫∆∂3456789hdπ˚ffwfof libgude ˚ø˙߃çß +_))@+"; let good_hex_val = "0x1234ABCD"; let bad_hex_val = "0x1234AB CD"; + let u128_val = u64::MAX as u128 + 0xff; let checks = vec![ // Expected Bool match @@ -105,6 +109,65 @@ fn test_basic_args_linter_pure_args() { Type::U8, Some(bcs::to_bytes(&9u8).unwrap()), ), + // U8 value encoded as str + ( + Value::from("89"), + Type::U8, + Some(bcs::to_bytes(&89u8).unwrap()), + ), + // U8 value encoded as str promoted to U64 + ( + Value::from("89"), + Type::U64, + Some(bcs::to_bytes(&89u64).unwrap()), + ), + // U64 value encoded as str + ( + Value::from("890"), + Type::U64, + Some(bcs::to_bytes(&890u64).unwrap()), + ), + // U128 value encoded as str + ( + Value::from(format!("{}", u128_val)), + Type::U128, + Some(bcs::to_bytes(&u128_val).unwrap()), + ), + // U8 value encoded as hex str + ( + Value::from("0x12"), + Type::U8, + Some(bcs::to_bytes(&0x12u8).unwrap()), + ), + // U8 value encoded as hex str promoted to U64 + ( + Value::from("0x12"), + Type::U64, + Some(bcs::to_bytes(&0x12u64).unwrap()), + ), + // U64 value encoded as hex str + ( + Value::from("0x890"), + Type::U64, + Some(bcs::to_bytes(&0x890u64).unwrap()), + ), + // U128 value encoded as hex str + ( + Value::from(format!("0x{:02x}", u128_val)), + Type::U128, + Some(bcs::to_bytes(&u128_val).unwrap()), + ), + // Space not allowed + (Value::from(" 9"), Type::U8, None), + // Hex must start with 0x + (Value::from("AB"), Type::U8, None), + // Too large + (Value::from("123456789"), Type::U8, None), + // Too large + (Value::from("123456789123456789123456789123456789"), Type::U64, None), + // Too large + (Value::from("123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789123456789"), Type::U128, None), + // U64 value greater than 255 cannot be used as U8 (Value::from(900u64), Type::U8, None), // floats cannot be used as U8