diff --git a/crates/sui-framework/packages/move-stdlib/sources/fixed_point32.move b/crates/sui-framework/packages/move-stdlib/sources/fixed_point32.move index 557400e813d40..9b1a2fe577010 100644 --- a/crates/sui-framework/packages/move-stdlib/sources/fixed_point32.move +++ b/crates/sui-framework/packages/move-stdlib/sources/fixed_point32.move @@ -3,7 +3,7 @@ /// Defines a fixed-point numeric type with a 32-bit integer part and /// a 32-bit fractional part. - +#[deprecated(note = b"Use `std::uq32_32` instead. If you need to convert from a `FixedPoint32` to a `UQ32_32`, you can use the `std::fixed_point32::get_raw_value` with `std::uq32_32::from_raw_value`.")] module std::fixed_point32; /// Define a fixed-point numeric type with 32 fractional bits. diff --git a/crates/sui-framework/packages/move-stdlib/sources/uq32_32.move b/crates/sui-framework/packages/move-stdlib/sources/uq32_32.move new file mode 100644 index 0000000000000..cad6381324090 --- /dev/null +++ b/crates/sui-framework/packages/move-stdlib/sources/uq32_32.move @@ -0,0 +1,160 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +/// Defines an unsigned, fixed-point numeric type with a 32-bit integer part and a 32-bit fractional +/// part. The notation `uq32_32` and `UQ32_32` is based on +/// [Q notation](https://en.wikipedia.org/wiki/Q_(number_format)). `q` indicates it a fixed-point +/// number. The `u` prefix indicates it is unsigned. The `32_32` suffix indicates the number of +/// bits, where the first number indicates the number of bits in the integer part, and the second +/// the number of bits in the fractional part--in this case 32 bits for each. +module std::uq32_32; + +#[error] +const EDenominator: vector = b"Quotient specified with a zero denominator"; + +#[error] +const EQuotientTooSmall: vector = + b"Quotient specified is too small, and is outside of the supported range"; + +#[error] +const EQuotientTooLarge: vector = + b"Quotient specified is too large, and is outside of the supported range"; + +#[error] +const EOverflow: vector = b"Overflow from an arithmetic operation"; + +#[error] +const EDivisionByZero: vector = b"Division by zero"; + +/// A fixed-point numeric type with 32 integer bits and 32 fractional bits, represented by an +/// underlying 64 bit value. This is a binary representation, so decimal values may not be exactly +/// representable, but it provides more than 9 decimal digits of precision both before and after the +/// decimal point (18 digits total). +public struct UQ32_32(u64) has copy, drop, store; + +/// Create a fixed-point value from a quotient specified by its numerator and denominator. +/// `from_quotient` and `from_int` should be preferred over using `from_raw`. +/// Unless the denominator is a power of two, fractions can not be represented accurately, +/// so be careful about rounding errors. +/// Aborts if the denominator is zero. +/// Aborts if the input is non-zero but so small that it will be represented as zero, e.g. smaller +/// than 2^{-32}. +/// Aborts if the input is too large, e.g. larger than or equal to 2^32. +public fun from_quotient(numerator: u64, denominator: u64): UQ32_32 { + assert!(denominator != 0, EDenominator); + + // Scale the numerator to have 64 fractional bits and the denominator to have 32 fractional + // bits, so that the quotient will have 32 fractional bits. + let scaled_numerator = numerator as u128 << 64; + let scaled_denominator = denominator as u128 << 32; + let quotient = scaled_numerator / scaled_denominator; + + // The quotient can only be zero if the numerator is also zero. + assert!(quotient != 0 || numerator == 0, EQuotientTooSmall); + + // Return the quotient as a fixed-point number. We first need to check whether the cast + // can succeed. + assert!(quotient <= std::u64::max_value!() as u128, EQuotientTooLarge); + UQ32_32(quotient as u64) +} + +/// Create a fixed-point value from an integer. +/// `from_int` and `from_quotient` should be preferred over using `from_raw`. +public fun from_int(integer: u32): UQ32_32 { + UQ32_32((integer as u64) << 32) +} + +/// Add two fixed-point numbers, `a + b`. +/// Aborts if the sum overflows. +public fun add(a: UQ32_32, b: UQ32_32): UQ32_32 { + let sum = a.0 as u128 + (b.0 as u128); + assert!(sum <= std::u64::max_value!() as u128, EOverflow); + UQ32_32(sum as u64) +} + +/// Subtract two fixed-point numbers, `a - b`. +/// Aborts if `a < b`. +public fun sub(a: UQ32_32, b: UQ32_32): UQ32_32 { + assert!(a.0 >= b.0, EOverflow); + UQ32_32(a.0 - b.0) +} + +/// Multiply two fixed-point numbers, truncating any fractional part of the product. +/// Aborts if the product overflows. +public fun mul(a: UQ32_32, b: UQ32_32): UQ32_32 { + UQ32_32(int_mul(a.0, b)) +} + +/// Divide two fixed-point numbers, truncating any fractional part of the quotient. +/// Aborts if the divisor is zero. +/// Aborts if the quotient overflows. +public fun div(a: UQ32_32, b: UQ32_32): UQ32_32 { + UQ32_32(int_div(a.0, b)) +} + +/// Convert a fixed-point number to an integer, truncating any fractional part. +public fun to_int(a: UQ32_32): u32 { + (a.0 >> 32) as u32 +} + +/// Multiply a `u64` integer by a fixed-point number, truncating any fractional part of the product. +/// Aborts if the product overflows. +public fun int_mul(val: u64, multiplier: UQ32_32): u64 { + // The product of two 64 bit values has 128 bits, so perform the + // multiplication with u128 types and keep the full 128 bit product + // to avoid losing accuracy. + let unscaled_product = val as u128 * (multiplier.0 as u128); + // The unscaled product has 32 fractional bits (from the multiplier) + // so rescale it by shifting away the low bits. + let product = unscaled_product >> 32; + // Check whether the value is too large. + assert!(product <= std::u64::max_value!() as u128, EOverflow); + product as u64 +} + +/// Divide a `u64` integer by a fixed-point number, truncating any fractional part of the quotient. +/// Aborts if the divisor is zero. +/// Aborts if the quotient overflows. +public fun int_div(val: u64, divisor: UQ32_32): u64 { + // Check for division by zero. + assert!(divisor.0 != 0, EDivisionByZero); + // First convert to 128 bits and then shift left to + // add 32 fractional zero bits to the dividend. + let scaled_value = val as u128 << 32; + let quotient = scaled_value / (divisor.0 as u128); + // Check whether the value is too large. + assert!(quotient <= std::u64::max_value!() as u128, EOverflow); + quotient as u64 +} + +/// Less than or equal to. Returns `true` if and only if `a <= a`. +public fun le(a: UQ32_32, b: UQ32_32): bool { + a.0 <= b.0 +} + +/// Less than. Returns `true` if and only if `a < b`. +public fun lt(a: UQ32_32, b: UQ32_32): bool { + a.0 < b.0 +} + +/// Greater than or equal to. Returns `true` if and only if `a >= b`. +public fun ge(a: UQ32_32, b: UQ32_32): bool { + a.0 >= b.0 +} + +/// Greater than. Returns `true` if and only if `a > b`. +public fun gt(a: UQ32_32, b: UQ32_32): bool { + a.0 > b.0 +} + +/// Accessor for the raw u64 value. Can be paired with `from_raw` to perform less common operations +/// on the raw values directly. +public fun to_raw(a: UQ32_32): u64 { + a.0 +} + +/// Accessor for the raw u64 value. Can be paired with `to_raw` to perform less common operations +/// on the raw values directly. +public fun from_raw(raw_value: u64): UQ32_32 { + UQ32_32(raw_value) +} diff --git a/crates/sui-framework/packages/move-stdlib/tests/fixedpoint32_tests.move b/crates/sui-framework/packages/move-stdlib/tests/fixedpoint32_tests.move index 28071e935742c..f3a42e898ed11 100644 --- a/crates/sui-framework/packages/move-stdlib/tests/fixedpoint32_tests.move +++ b/crates/sui-framework/packages/move-stdlib/tests/fixedpoint32_tests.move @@ -3,7 +3,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -#[test_only] +#[test_only, allow(deprecated_usage)] module std::fixed_point32_tests; use std::fixed_point32; diff --git a/crates/sui-framework/packages/move-stdlib/tests/uq32_32_tests.move b/crates/sui-framework/packages/move-stdlib/tests/uq32_32_tests.move new file mode 100644 index 0000000000000..78c1d9f543c33 --- /dev/null +++ b/crates/sui-framework/packages/move-stdlib/tests/uq32_32_tests.move @@ -0,0 +1,257 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[test_only] +module std::uq32_32_tests; + +use std::unit_test::assert_eq; +use std::uq32_32::{ + Self, + add, + sub, + mul, + div, + int_div, + int_mul, + from_int, + from_quotient, + from_raw, + to_raw, +}; + +#[test] +fun from_quotient_zero() { + let x = from_quotient(0, 1); + assert_eq!(x.to_raw(), 0); +} + +#[test] +fun from_quotient_max_numerator_denominator() { + // Test creating a 1.0 fraction from the maximum u64 value. + let f = from_quotient(std::u64::max_value!(), std::u64::max_value!()); + let one = f.to_raw(); + assert_eq!(one, 1 << 32); // 0x1.00000000 +} + +#[test] +#[expected_failure(abort_code = uq32_32::EDenominator)] +fun from_quotient_div_zero() { + // A denominator of zero should cause an arithmetic error. + from_quotient(2, 0); +} + +#[test] +#[expected_failure(abort_code = uq32_32::EQuotientTooLarge)] +fun from_quotient_ratio_too_large() { + // The maximum value is 2^32 - 1. Check that anything larger aborts + // with an overflow. + from_quotient(1 << 32, 1); // 2^32 +} + +#[test] +#[expected_failure(abort_code = uq32_32::EQuotientTooSmall)] +fun from_quotient_ratio_too_small() { + // The minimum non-zero value is 2^-32. Check that anything smaller + // aborts. + from_quotient(1, (1 << 32) + 1); // 1/(2^32 + 1) +} + +#[test] +fun test_from_int() { + assert_eq!(from_int(0).to_raw(), 0); + assert_eq!(from_int(1).to_raw(), 0x1_0000_0000); + assert_eq!(from_int(std::u32::max_value!()).to_raw(), std::u32::max_value!() as u64 << 32); +} + +#[test] +fun test_add() { + let a = from_quotient(3, 4); + assert!(a.add(from_int(0)) == a); + + let c = a.add(from_int(1)); + assert!(from_quotient(7, 4) == c); + + let b = from_quotient(1, 4); + let c = a.add(b); + assert!(from_int(1) == c); +} + +#[test] +#[expected_failure(abort_code = uq32_32::EOverflow)] +fun test_add_overflow() { + let a = from_int(1 << 31); + let b = from_int(1 << 31); + let _ = a.add(b); +} + +#[test] +fun test_sub() { + let a = from_int(5); + assert_eq!(a.sub(from_int(0)), a); + + let b = from_int(4); + let c = a.sub(b); + assert_eq!(from_int(1), c); +} + +#[test] +#[expected_failure(abort_code = uq32_32::EOverflow)] +fun test_sub_underflow() { + let a = from_int(3); + let b = from_int(5); + a.sub(b); +} + +#[test] +fun test_mul() { + let a = from_quotient(3, 4); + assert!(a.mul(from_int(0)) == from_int(0)); + assert!(a.mul(from_int(1)) == a); + + let b = from_quotient(3, 2); + let c = a.mul(b); + let expected = from_quotient(9, 8); + assert_eq!(c, expected); +} + +#[test] +#[expected_failure(abort_code = uq32_32::EOverflow)] +fun test_mul_overflow() { + let a = from_int(1 << 16); + let b = from_int(1 << 16); + let _ = a.mul(b); +} + +#[test] +fun test_div() { + let a = from_quotient(3, 4); + assert!(a.div(from_int(1)) == a); + + let b = from_int(8); + let c = a.div(b); + let expected = from_quotient(3, 32); + assert_eq!(c, expected); +} + +#[test] +#[expected_failure(abort_code = uq32_32::EDivisionByZero)] +fun test_div_by_zero() { + let a = from_int(7); + let b = from_int(0); + let _ = a.div(b); +} + +#[test] +#[expected_failure(abort_code = uq32_32::EOverflow)] +fun test_div_overflow() { + let a = from_int(1 << 31); + let b = from_quotient(1, 2); + let _ = a.div(b); +} + +#[test] +fun exact_int_div() { + let f = from_quotient(3, 4); // 0.75 + let twelve = int_div(9, f); // 9 / 0.75 + assert_eq!(twelve, 12); +} + +#[test] +#[expected_failure(abort_code = uq32_32::EDivisionByZero)] +fun int_div_by_zero() { + let f = from_raw(0); // 0 + // Dividing by zero should cause an arithmetic error. + int_div(1, f); +} + +#[test] +#[expected_failure(abort_code = uq32_32::EOverflow)] +fun int_div_overflow_small_divisor() { + let f = from_raw(1); // 0x0.00000001 + // Divide 2^32 by the minimum fractional value. This should overflow. + int_div(1 << 32, f); +} + +#[test] +#[expected_failure(abort_code = uq32_32::EOverflow)] +fun int_div_overflow_large_numerator() { + let f = from_quotient(1, 2); // 0.5 + // Divide the maximum u64 value by 0.5. This should overflow. + int_div(std::u64::max_value!(), f); +} + +#[test] +fun exact_int_mul() { + let f = from_quotient(3, 4); // 0.75 + let nine = int_mul(12, f); // 12 * 0.75 + assert_eq!(nine, 9); +} + +#[test] +fun int_mul_truncates() { + let f = from_quotient(1, 3); // 0.333... + let not_three = int_mul(9, copy f); // 9 * 0.333... + // multiply_u64 does NOT round -- it truncates -- so values that + // are not perfectly representable in binary may be off by one. + assert_eq!(not_three, 2); + + // Try again with a fraction slightly larger than 1/3. + let f = from_raw(f.to_raw() + 1); + let three = int_mul(9, f); + assert_eq!(three, 3); +} + +#[test] +#[expected_failure(abort_code = uq32_32::EOverflow)] +fun int_mul_overflow_small_multiplier() { + let f = from_quotient(3, 2); // 1.5 + // Multiply the maximum u64 value by 1.5. This should overflow. + int_mul(std::u64::max_value!(), f); +} + +#[test] +#[expected_failure(abort_code = uq32_32::EOverflow)] +fun int_mul_overflow_large_multiplier() { + let f = from_raw(std::u64::max_value!()); + // Multiply 2^32 + 1 by the maximum fixed-point value. This should overflow. + int_mul((1 << 32) + 1, f); +} + +#[test] +fun test_comparison() { + let a = from_quotient(5, 2); + let b = from_quotient(5, 3); + let c = from_quotient(5, 2); + + assert!(b.le(a)); + assert!(b.lt(a)); + assert!(c.le(a)); + assert_eq!(c, a); + assert!(a.ge(b)); + assert!(a.gt(b)); + assert!(from_int(0).le(a)); +} + +#[random_test] +fun test_raw(raw: u64) { + assert_eq!(from_raw(raw).to_raw(), raw); +} + +#[random_test] +fun test_int_roundtrip(c: u32) { + assert_eq!(from_int(c).to_int(), c); +} + +#[random_test] +fun test_mul_rand(n: u16, d: u16, c: u16) { + if (d == 0) return; + let q = from_quotient(n as u64, d as u64); + assert_eq!(int_mul(c as u64, q), q.mul(from_int(c as u32)).to_int() as u64); +} + +#[random_test] +fun test_div_rand(n: u16, d: u16, c: u16) { + if (d == 0) return; + let q = from_quotient(n as u64, d as u64); + assert_eq!(int_div(c as u64, q), from_int(c as u32).div(q).to_int() as u64); +} diff --git a/crates/sui-framework/packages_compiled/move-stdlib b/crates/sui-framework/packages_compiled/move-stdlib index 8526133aab6a4..191c7bd74e552 100644 Binary files a/crates/sui-framework/packages_compiled/move-stdlib and b/crates/sui-framework/packages_compiled/move-stdlib differ diff --git a/crates/sui-framework/published_api.txt b/crates/sui-framework/published_api.txt index e6d0acfbb736b..4d9f1f6f33262 100644 --- a/crates/sui-framework/published_api.txt +++ b/crates/sui-framework/published_api.txt @@ -4282,6 +4282,54 @@ sha2_256 sha3_256 public fun 0x1::hash +UQ32_32 + public struct + 0x1::uq32_32 +from_quotient + public fun + 0x1::uq32_32 +from_int + public fun + 0x1::uq32_32 +add + public fun + 0x1::uq32_32 +sub + public fun + 0x1::uq32_32 +mul + public fun + 0x1::uq32_32 +div + public fun + 0x1::uq32_32 +to_int + public fun + 0x1::uq32_32 +int_mul + public fun + 0x1::uq32_32 +int_div + public fun + 0x1::uq32_32 +le + public fun + 0x1::uq32_32 +lt + public fun + 0x1::uq32_32 +ge + public fun + 0x1::uq32_32 +gt + public fun + 0x1::uq32_32 +to_raw + public fun + 0x1::uq32_32 +from_raw + public fun + 0x1::uq32_32 empty public fun 0x1::vector diff --git a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap index 05eaa6318c556..0293be0f3f153 100644 --- a/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap +++ b/crates/sui-swarm-config/tests/snapshots/snapshot_tests__populated_genesis_snapshot_matches-2.snap @@ -240,13 +240,13 @@ validators: next_epoch_worker_address: ~ extra_fields: id: - id: "0xcc91d06982135046b4cfa9bbd4f7f206cdaf3f9fb27c782a7137b3bef08892ed" + id: "0x5e216be58ccd6edc40d76fd21795437e3c135e1f4bcd3cf3ed07995d25456060" size: 0 voting_power: 10000 - operation_cap_id: "0xfc07728a8857acbf12a2d5684de4f98920fad0952f784426eaafdda79b94ee20" + operation_cap_id: "0x1b99479eef6dbadd1755f24adcdb074855f39e29941b53908a379c7e7091af81" gas_price: 1000 staking_pool: - id: "0xdb3034b1953243443f3cbc912d09de43d8e836803c018ba14245d7fa0a4c383d" + id: "0xb38eb98b4e99b71bcab647fbabf992c3cc19129c9528b767c6c303a986fa2c42" activation_epoch: 0 deactivation_epoch: ~ sui_balance: 20000000000000000 @@ -254,14 +254,14 @@ validators: value: 0 pool_token_balance: 20000000000000000 exchange_rates: - id: "0x8d37fa87257f45904f0ef1424ead1cc8e1ea23cf00ff16ce9a63f2b77df6231f" + id: "0x4dc309edc1409b3f194ac1758aef9372ce2eb471ced67d80e5b0f4bba96d28d5" size: 1 pending_stake: 0 pending_total_sui_withdraw: 0 pending_pool_token_withdraw: 0 extra_fields: id: - id: "0x9f7ae3a49a73bbfdf1721536325cb5699b3cdb3174e42e461bb4eed6cb45aeb0" + id: "0xe4f5ca1ff5bd26453325097073da8f2ee7bacf92155c291c62ae119c1cb5f832" size: 0 commission_rate: 200 next_epoch_stake: 20000000000000000 @@ -269,27 +269,27 @@ validators: next_epoch_commission_rate: 200 extra_fields: id: - id: "0xcf38a9cd46d8931bb0e06800c7dd818023842c6eed78c74f4d3663e3b256e9fe" + id: "0x2324e3ec47e27aa137997a532567f5afa2a9645a921ac016aea5a3eaaab9ec68" size: 0 pending_active_validators: contents: - id: "0x5757ed8fb231e16f163db4f3a31aa660fccf4189e5c8fe535588abfbabde730f" + id: "0xa07bbd43dc12c089a98ce90dec1e59dd1634de032a7506e063e2ebc6b8790567" size: 0 pending_removals: [] staking_pool_mappings: - id: "0x34c34655c2c9a2f2b708915189cf536c6f0e129655ef50d7157f461d74487741" + id: "0x617c029b1c4c382d85289275adffe619f9bd6d0d54eae1a6bd20854ae86dba51" size: 1 inactive_validators: - id: "0x15c6d3a2bd25a1232b8114376e0421dbdd5acf92f50bbd091588e7a43d5276da" + id: "0x8738f13db0421d917e79ee20e0b50f810922b60d44873fa9f5f4b07287d94a0e" size: 0 validator_candidates: - id: "0xb2c748bd9e0c7cc676667f7ea2909b6edc224d8dc1d1797626b926409a5f04ef" + id: "0x419362971d83e7403641c492fae230e83eed8e09662b83e6758808839c0fe0d9" size: 0 at_risk_validators: contents: [] extra_fields: id: - id: "0xfcd0b94a9048c129e2e0e690bc3afe2a9e35377d64821c33076e7c14969b9143" + id: "0x624540ee67bc94279d5a4eab2b2eca6bbf10d9b6dfe05deb1ba9d4f7530a494c" size: 0 storage_fund: total_object_storage_rebates: @@ -306,7 +306,7 @@ parameters: validator_low_stake_grace_period: 7 extra_fields: id: - id: "0x34e56a96336b73d4983cfdb95c4392f7214fc632973e1529f63ee7caf6ed0458" + id: "0x9ae4daddf54c4d97b85626f3e4441c7093a7c632ac1bbc3b521d71160ebbca65" size: 0 reference_gas_price: 1000 validator_report_records: @@ -320,7 +320,7 @@ stake_subsidy: stake_subsidy_decrease_rate: 1000 extra_fields: id: - id: "0xbfd2b299dab597fcf4e004a252274bd8262e18521b63168c49eca2b1839dc207" + id: "0x065bccd62b599e64be7189489a86c0c5758ab648a090d7e3ef765650d5018dda" size: 0 safe_mode: false safe_mode_storage_rewards: @@ -332,5 +332,5 @@ safe_mode_non_refundable_storage_fee: 0 epoch_start_timestamp_ms: 10 extra_fields: id: - id: "0x7f447c7cb0761fe5d2350b00849a94f887b78b2b3a5afd75f97092be04e1bf27" + id: "0x602bc63f6783de4a51993457c225e56158f59fbd07474b385a3a3ce45bfde7c6" size: 0