From 573e01e42bf4b4d5c319854df95a77604f34b96c Mon Sep 17 00:00:00 2001 From: igor-aptos <110557261+igor-aptos@users.noreply.github.com> Date: Wed, 13 Nov 2024 17:39:28 -0800 Subject: [PATCH] [move-stdlib] Implement bcs::constant_serialized_size(): Option (#14984) It is sometimes useful to know if type has constants serialized size, and for example perform some optimization based on it. --- Cargo.lock | 1 + .../src/gas_schedule/move_stdlib.rs | 3 + .../sources/transaction_context_test.move | 9 +- aptos-move/framework/move-stdlib/Cargo.toml | 1 + aptos-move/framework/move-stdlib/doc/bcs.md | 36 +++++- .../framework/move-stdlib/sources/bcs.move | 19 +++ .../framework/move-stdlib/src/natives/bcs.rs | 116 +++++++++++++++++- .../move-stdlib/tests/bcs_tests.move | 40 ++++++ types/src/on_chain_config/aptos_features.rs | 1 + 9 files changed, 221 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47de75440da7c..a7c086423684c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2891,6 +2891,7 @@ dependencies = [ "aptos-gas-schedule", "aptos-native-interface", "aptos-types", + "bcs 0.1.4", "dir-diff", "file_diff", "move-cli", diff --git a/aptos-move/aptos-gas-schedule/src/gas_schedule/move_stdlib.rs b/aptos-move/aptos-gas-schedule/src/gas_schedule/move_stdlib.rs index ec6455e2ed17a..dd730ffc58c94 100644 --- a/aptos-move/aptos-gas-schedule/src/gas_schedule/move_stdlib.rs +++ b/aptos-move/aptos-gas-schedule/src/gas_schedule/move_stdlib.rs @@ -9,6 +9,7 @@ use crate::{ }; use aptos_gas_algebra::{ InternalGas, InternalGasPerAbstractValueUnit, InternalGasPerArg, InternalGasPerByte, + InternalGasPerTypeNode, }; crate::gas_schedule::macros::define_gas_parameters!( @@ -41,6 +42,8 @@ crate::gas_schedule::macros::define_gas_parameters!( [bcs_serialized_size_base: InternalGas, { RELEASE_V1_18.. => "bcs.serialized_size.base" }, 735], [bcs_serialized_size_per_byte_serialized: InternalGasPerByte, { RELEASE_V1_18.. => "bcs.serialized_size.per_byte_serialized" }, 36], [bcs_serialized_size_failure: InternalGas, { RELEASE_V1_18.. => "bcs.serialized_size.failure" }, 3676], + [bcs_constant_serialized_size_base: InternalGas, { RELEASE_V1_24.. => "bcs.constant_serialized_size.base" }, 735], + [bcs_constant_serialized_size_per_type_node: InternalGasPerTypeNode, { RELEASE_V1_24.. => "bcs.constant_serialized_size.per_type_node" }, 40], [cmp_compare_base: InternalGas, { RELEASE_V1_24.. => "cmp.compare.base" }, 367], [cmp_compare_per_abs_val_unit: InternalGasPerAbstractValueUnit, { RELEASE_V1_24.. => "cmp.compare.per_abs_val_unit"}, 14], diff --git a/aptos-move/e2e-move-tests/src/tests/transaction_context.data/pack/sources/transaction_context_test.move b/aptos-move/e2e-move-tests/src/tests/transaction_context.data/pack/sources/transaction_context_test.move index 0116d1035c9b0..de1ef952205a5 100644 --- a/aptos-move/e2e-move-tests/src/tests/transaction_context.data/pack/sources/transaction_context_test.move +++ b/aptos-move/e2e-move-tests/src/tests/transaction_context.data/pack/sources/transaction_context_test.move @@ -110,6 +110,10 @@ module admin::transaction_context_test { ), type_info::type_name()], 13 ); + + assert!(option::some(option::destroy_some(payload_opt)) == transaction_context::entry_function_payload(), 13); + } else { + assert!(option::none() == payload_opt, 14); } } @@ -128,7 +132,10 @@ module admin::transaction_context_test { store.function_name = transaction_context::function_name(entry); store.type_arg_names = transaction_context::type_arg_names(entry); store.args = transaction_context::args(entry); - } + }; + assert!(option::some(option::destroy_some(multisig_opt)) == transaction_context::multisig_payload(), 1); + } else { + assert!(option::none() == multisig_opt, 2); } } diff --git a/aptos-move/framework/move-stdlib/Cargo.toml b/aptos-move/framework/move-stdlib/Cargo.toml index 3fb0a928cd8d9..c14625d18dbd6 100644 --- a/aptos-move/framework/move-stdlib/Cargo.toml +++ b/aptos-move/framework/move-stdlib/Cargo.toml @@ -14,6 +14,7 @@ publish = false aptos-gas-schedule = { workspace = true } aptos-native-interface = { workspace = true } aptos-types = { workspace = true } +bcs = { workspace = true } move-core-types = { path = "../../../third_party/move/move-core/types" } move-vm-runtime = { path = "../../../third_party/move/move-vm/runtime" } move-vm-types = { path = "../../../third_party/move/move-vm/types" } diff --git a/aptos-move/framework/move-stdlib/doc/bcs.md b/aptos-move/framework/move-stdlib/doc/bcs.md index 9bdcf7f45f7d3..0a73d39902341 100644 --- a/aptos-move/framework/move-stdlib/doc/bcs.md +++ b/aptos-move/framework/move-stdlib/doc/bcs.md @@ -11,11 +11,13 @@ details on BCS. - [Function `to_bytes`](#0x1_bcs_to_bytes) - [Function `serialized_size`](#0x1_bcs_serialized_size) +- [Function `constant_serialized_size`](#0x1_bcs_constant_serialized_size) - [Specification](#@Specification_0) - [Function `serialized_size`](#@Specification_0_serialized_size) -
+
use 0x1::option;
+
@@ -65,6 +67,38 @@ Aborts with 0x1c5 error code if there is a failure when calculating + + + + +## Function `constant_serialized_size` + +If the type has known constant (always the same, independent of instance) serialized size +in BCS (Binary Canonical Serialization) format, returns it, otherwise returns None. +Aborts with 0x1c5 error code if there is a failure when calculating serialized size. + +Note: +For some types it might not be known they have constant size, and function might return None. +For example, signer appears to have constant size, but it's size might change. +If this function returned Some() for some type before - it is guaranteed to continue returning Some(). +On the other hand, if function has returned None for some type, +it might change in the future to return Some() instead, if size becomes "known". + + +
public(friend) fun constant_serialized_size<MoveValue>(): option::Option<u64>
+
+ + + +
+Implementation + + +
native public(friend) fun constant_serialized_size<MoveValue>(): Option<u64>;
+
+ + +
diff --git a/aptos-move/framework/move-stdlib/sources/bcs.move b/aptos-move/framework/move-stdlib/sources/bcs.move index 478cbed627f24..7721d69653f6e 100644 --- a/aptos-move/framework/move-stdlib/sources/bcs.move +++ b/aptos-move/framework/move-stdlib/sources/bcs.move @@ -3,6 +3,8 @@ /// published on-chain. See https://github.com/aptos-labs/bcs#binary-canonical-serialization-bcs for more /// details on BCS. module std::bcs { + use std::option::Option; + /// Returns the binary representation of `v` in BCS (Binary Canonical Serialization) format. /// Aborts with `0x1c5` error code if serialization fails. native public fun to_bytes(v: &MoveValue): vector; @@ -11,6 +13,23 @@ module std::bcs { /// Aborts with `0x1c5` error code if there is a failure when calculating serialized size. native public fun serialized_size(v: &MoveValue): u64; + // TODO - function `constant_serialized_size1 is `public(friend)` here for one release, + // and to be changed to `public` one release later. + #[test_only] + friend std::bcs_tests; + + /// If the type has known constant (always the same, independent of instance) serialized size + /// in BCS (Binary Canonical Serialization) format, returns it, otherwise returns None. + /// Aborts with `0x1c5` error code if there is a failure when calculating serialized size. + /// + /// Note: + /// For some types it might not be known they have constant size, and function might return None. + /// For example, signer appears to have constant size, but it's size might change. + /// If this function returned Some() for some type before - it is guaranteed to continue returning Some(). + /// On the other hand, if function has returned None for some type, + /// it might change in the future to return Some() instead, if size becomes "known". + native public(friend) fun constant_serialized_size(): Option; + // ============================== // Module Specification spec module {} // switch to module documentation context diff --git a/aptos-move/framework/move-stdlib/src/natives/bcs.rs b/aptos-move/framework/move-stdlib/src/natives/bcs.rs index 283e890c1cdf7..1ba7a7f17ca3f 100644 --- a/aptos-move/framework/move-stdlib/src/natives/bcs.rs +++ b/aptos-move/framework/move-stdlib/src/natives/bcs.rs @@ -11,18 +11,26 @@ use aptos_native_interface::{ SafeNativeResult, }; use move_core_types::{ - gas_algebra::NumBytes, vm_status::sub_status::NFE_BCS_SERIALIZATION_FAILURE, + account_address::AccountAddress, + gas_algebra::{NumBytes, NumTypeNodes}, + u256, + value::{MoveStructLayout, MoveTypeLayout}, + vm_status::{sub_status::NFE_BCS_SERIALIZATION_FAILURE, StatusCode}, }; use move_vm_runtime::native_functions::NativeFunction; use move_vm_types::{ loaded_data::runtime_types::Type, - natives::function::PartialVMResult, + natives::function::{PartialVMError, PartialVMResult}, value_serde::serialized_size_allowing_delayed_values, - values::{values_impl::Reference, Value}, + values::{values_impl::Reference, Struct, Value}, }; use smallvec::{smallvec, SmallVec}; use std::collections::VecDeque; +pub fn create_option_u64(value: Option) -> Value { + Value::struct_(Struct::pack(vec![Value::vector_u64(value)])) +} + /*************************************************************************************************** * native fun to_bytes * @@ -126,6 +134,107 @@ fn serialized_size_impl( serialized_size_allowing_delayed_values(&value, &ty_layout) } +fn native_constant_serialized_size( + context: &mut SafeNativeContext, + mut ty_args: Vec, + _args: VecDeque, +) -> SafeNativeResult> { + debug_assert!(ty_args.len() == 1); + + context.charge(BCS_CONSTANT_SERIALIZED_SIZE_BASE)?; + + let ty = ty_args.pop().unwrap(); + let ty_layout = context.type_to_type_layout(&ty)?; + + let (visited_count, serialized_size_result) = constant_serialized_size(&ty_layout); + context + .charge(BCS_CONSTANT_SERIALIZED_SIZE_PER_TYPE_NODE * NumTypeNodes::new(visited_count))?; + + let result = match serialized_size_result { + Ok(value) => create_option_u64(value.map(|v| v as u64)), + Err(_) => { + context.charge(BCS_SERIALIZED_SIZE_FAILURE)?; + + // Re-use the same abort code as bcs::to_bytes. + return Err(SafeNativeError::Abort { + abort_code: NFE_BCS_SERIALIZATION_FAILURE, + }); + }, + }; + + Ok(smallvec![result]) +} + +/// If given type has a constant serialized size (irrespective of the instance), it returns the serialized +/// size in bytes any value would have. +/// Otherwise it returns None. +/// First element of the returned tuple represents number of visited nodes, used to charge gas. +fn constant_serialized_size(ty_layout: &MoveTypeLayout) -> (u64, PartialVMResult>) { + let mut visited_count = 1; + let bcs_size_result = match ty_layout { + MoveTypeLayout::Bool => bcs::serialized_size(&false).map(Some), + MoveTypeLayout::U8 => bcs::serialized_size(&0u8).map(Some), + MoveTypeLayout::U16 => bcs::serialized_size(&0u16).map(Some), + MoveTypeLayout::U32 => bcs::serialized_size(&0u32).map(Some), + MoveTypeLayout::U64 => bcs::serialized_size(&0u64).map(Some), + MoveTypeLayout::U128 => bcs::serialized_size(&0u128).map(Some), + MoveTypeLayout::U256 => bcs::serialized_size(&u256::U256::zero()).map(Some), + MoveTypeLayout::Address => bcs::serialized_size(&AccountAddress::ZERO).map(Some), + // signer's size is VM implementation detail, and can change at will. + MoveTypeLayout::Signer => Ok(None), + // vectors have no constant size + MoveTypeLayout::Vector(_) => Ok(None), + // enums have no constant size + MoveTypeLayout::Struct( + MoveStructLayout::RuntimeVariants(_) | MoveStructLayout::WithVariants(_), + ) => Ok(None), + MoveTypeLayout::Struct(MoveStructLayout::Runtime(fields)) => { + let mut total = Some(0); + for field in fields { + let (cur_visited_count, cur) = constant_serialized_size(field); + visited_count += cur_visited_count; + match cur { + Err(e) => return (visited_count, Err(e)), + Ok(Some(cur_value)) => total = total.map(|v| v + cur_value), + Ok(None) => { + total = None; + break; + }, + } + } + Ok(total) + }, + MoveTypeLayout::Struct(MoveStructLayout::WithFields(_)) + | MoveTypeLayout::Struct(MoveStructLayout::WithTypes { .. }) => { + return ( + visited_count, + Err( + PartialVMError::new(StatusCode::VALUE_SERIALIZATION_ERROR).with_message( + "Only runtime types expected, but found WithFields/WithTypes".to_string(), + ), + ), + ) + }, + MoveTypeLayout::Native(_, inner) => { + let (cur_visited_count, cur) = constant_serialized_size(inner); + visited_count += cur_visited_count; + match cur { + Err(e) => return (visited_count, Err(e)), + Ok(v) => Ok(v), + } + }, + }; + ( + visited_count, + bcs_size_result.map_err(|e| { + PartialVMError::new(StatusCode::VALUE_SERIALIZATION_ERROR).with_message(format!( + "failed to compute serialized size of a value: {:?}", + e + )) + }), + ) +} + /*************************************************************************************************** * module **************************************************************************************************/ @@ -135,6 +244,7 @@ pub fn make_all( let funcs = [ ("to_bytes", native_to_bytes as RawSafeNative), ("serialized_size", native_serialized_size), + ("constant_serialized_size", native_constant_serialized_size), ]; builder.make_named_natives(funcs) diff --git a/aptos-move/framework/move-stdlib/tests/bcs_tests.move b/aptos-move/framework/move-stdlib/tests/bcs_tests.move index 367f504044694..7fb6b0b57b0a9 100644 --- a/aptos-move/framework/move-stdlib/tests/bcs_tests.move +++ b/aptos-move/framework/move-stdlib/tests/bcs_tests.move @@ -2,6 +2,8 @@ module std::bcs_tests { use std::bcs; use std::vector; + use std::option; + use std::signer; struct Box has copy, drop, store { x: T } struct Box3 has copy, drop, store { x: Box> } @@ -20,6 +22,8 @@ module std::bcs_tests { let expected_size = vector::length(&actual_bytes); let actual_size = bcs::serialized_size(&true); assert!(actual_size == expected_size, 1); + + assert!(option::some(actual_size) == bcs::constant_serialized_size(), 2); } #[test] @@ -31,6 +35,8 @@ module std::bcs_tests { let expected_size = vector::length(&actual_bytes); let actual_size = bcs::serialized_size(&1u8); assert!(actual_size == expected_size, 1); + + assert!(option::some(actual_size) == bcs::constant_serialized_size(), 2); } #[test] @@ -42,6 +48,8 @@ module std::bcs_tests { let expected_size = vector::length(&actual_bytes); let actual_size = bcs::serialized_size(&1); assert!(actual_size == expected_size, 1); + + assert!(option::some(actual_size) == bcs::constant_serialized_size(), 2); } #[test] @@ -53,6 +61,8 @@ module std::bcs_tests { let expected_size = vector::length(&actual_bytes); let actual_size = bcs::serialized_size(&1u128); assert!(actual_size == expected_size, 1); + + assert!(option::some(actual_size) == bcs::constant_serialized_size(), 2); } #[test] @@ -66,6 +76,23 @@ module std::bcs_tests { let expected_size = vector::length(&actual_bytes); let actual_size = bcs::serialized_size(&v); assert!(actual_size == expected_size, 1); + + assert!(option::none() == bcs::constant_serialized_size>(), 2); + } + + #[test(creator = @0xcafe)] + fun bcs_address(creator: &signer) { + let v = signer::address_of(creator); + + let expected_bytes = x"000000000000000000000000000000000000000000000000000000000000CAFE"; + let actual_bytes = bcs::to_bytes(&v); + assert!(actual_bytes == expected_bytes, 0); + + let expected_size = vector::length(&actual_bytes); + let actual_size = bcs::serialized_size(&v); + assert!(actual_size == expected_size, 1); + + assert!(option::some(actual_size) == bcs::constant_serialized_size
(), 2); } fun box3(x: T): Box3 { @@ -101,5 +128,18 @@ module std::bcs_tests { let actual_size = bcs::serialized_size(&box); assert!(actual_size == expected_size, 0); + + assert!(option::some(actual_size) == bcs::constant_serialized_size>(), 1); + assert!(option::none() == bcs::constant_serialized_size>>(), 2); + assert!(option::none() == bcs::constant_serialized_size>>(), 3); } + + // enum Singleton { + // V1(u64), + // } + + // fun encode_enum() { + // assert!(option::none() == bcs::constant_serialized_size()); + // assert!(option::none() == bcs::constant_serialized_size>()); + // } } diff --git a/types/src/on_chain_config/aptos_features.rs b/types/src/on_chain_config/aptos_features.rs index 7ee0d4d2dbbd5..f167354ba56c4 100644 --- a/types/src/on_chain_config/aptos_features.rs +++ b/types/src/on_chain_config/aptos_features.rs @@ -97,6 +97,7 @@ pub enum FeatureFlag { TRANSACTION_SIMULATION_ENHANCEMENT = 78, COLLECTION_OWNER = 79, /// covers mem::swap and vector::move_range + /// AIP-105 (https://github.com/aptos-foundation/AIPs/blob/main/aips/aip-105.md) NATIVE_MEMORY_OPERATIONS = 80, ENABLE_LOADER_V2 = 81, }