Support weight prediction in `const` context

Some smart contracts or simplified wallets statically know the sizes of
transactions or inputs. The possible approaches to handling them so far
were re-computing the values (and hoping the optimizer will const fold
them) or using a simple constant which may be harder to understand and
get right. It's much nicer to just use a `const` but our code didn't
support it until now.

This change adds methods that can compute the prediction in `const`
context for Rust versions >= 1.46.0 which allow use of loops (and
conditions but those could be workaround anyway).

As a side effect of this, the change also adds `const` to `VarInt::len`
in Rust 1.46+. While this one could be made unconditional using array
trick it's probably not worth it because of the planned MSRV bump.

Note: this commit is intentionally unformatted to make diff easier to
understand. Formatting will be done in future commit.
This commit is contained in:
Martin Habovstiak 2023-03-16 20:02:37 +01:00
parent 2982681d59
commit d56d202aeb
3 changed files with 90 additions and 0 deletions

View File

@ -1247,6 +1247,11 @@ pub fn predict_weight<I, O>(inputs: I, output_script_lens: O) -> Weight
let script_size = script_len + VarInt(script_len as u64).len(); let script_size = script_len + VarInt(script_len as u64).len();
(output_count + 1, total_scripts_size + script_size) (output_count + 1, total_scripts_size + script_size)
}); });
predict_weight_internal(input_count, partial_input_weight, inputs_with_witnesses, output_count, output_scripts_size)
}
crate::internal_macros::maybe_const_fn! {
fn predict_weight_internal(input_count: usize, partial_input_weight: usize, inputs_with_witnesses: usize, output_count: usize, output_scripts_size: usize) -> Weight {
let input_weight = partial_input_weight + input_count * 4 * (32 + 4 + 4); let input_weight = partial_input_weight + input_count * 4 * (32 + 4 + 4);
let output_size = 8 * output_count + output_scripts_size; let output_size = 8 * output_count + output_scripts_size;
let non_input_size = let non_input_size =
@ -1265,6 +1270,42 @@ pub fn predict_weight<I, O>(inputs: I, output_script_lens: O) -> Weight
}; };
Weight::from_wu(weight as u64) Weight::from_wu(weight as u64)
} }
}
/// Predicts the weight of a to-be-constructed transaction in const context.
///
/// *Important: only available in Rust 1.46+*
///
/// This is a `const` version of [`predict_weight`] which only allows slices due to current Rust
/// limitations around `const fn`. Because of these limitations it may be less efficient than
/// `predict_weight` and thus is intended to be only used in `const` context.
///
/// Please see the documentation of `predict_weight` to learn more about this function.
#[cfg(rust_v_1_46)]
pub const fn predict_weight_from_slices(inputs: &[InputWeightPrediction], output_script_lens: &[usize]) -> Weight {
let mut partial_input_weight = 0;
let mut inputs_with_witnesses = 0;
// for loops not supported in const fn
let mut i = 0;
while i < inputs.len() {
let prediction = inputs[i];
partial_input_weight += prediction.script_size * 4 + prediction.witness_size;
inputs_with_witnesses += (prediction.witness_size > 0) as usize;
i += 1;
}
let mut output_scripts_size = 0;
i = 0;
while i < output_script_lens.len() {
let script_len = output_script_lens[i];
output_scripts_size += script_len + VarInt(script_len as u64).len();
i += 1;
}
predict_weight_internal(inputs.len(), partial_input_weight, inputs_with_witnesses, output_script_lens.len(), output_scripts_size)
}
/// Weight prediction of an individual input. /// Weight prediction of an individual input.
/// ///
@ -1351,6 +1392,37 @@ impl InputWeightPrediction {
witness_size, witness_size,
} }
} }
/// Computes the prediction for a single input in `const` context.
///
/// *Important: only available in Rust 1.46+*
///
/// This is a `const` version of [`new`](Self::new) which only allows slices due to current Rust
/// limitations around `const fn`. Because of these limitations it may be less efficient than
/// `new` and thus is intended to be only used in `const` context.
#[cfg(rust_v_1_46)]
pub const fn from_slice(input_script_len: usize, witness_element_lengths: &[usize]) -> Self {
let mut i = 0;
let mut total_size = 0;
// for loops not supported in const fn
while i < witness_element_lengths.len() {
let elem_len = witness_element_lengths[i];
let elem_size = elem_len + VarInt(elem_len as u64).len();
total_size += elem_size;
i += 1;
}
let witness_size = if !witness_element_lengths.is_empty() {
total_size + VarInt(witness_element_lengths.len() as u64).len()
} else {
0
};
let script_size = input_script_len + VarInt(input_script_len as u64).len();
InputWeightPrediction {
script_size,
witness_size,
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -372,8 +372,11 @@ impl_int_encodable!(i64, read_i64, emit_i64);
#[allow(clippy::len_without_is_empty)] // VarInt has on concept of 'is_empty'. #[allow(clippy::len_without_is_empty)] // VarInt has on concept of 'is_empty'.
impl VarInt { impl VarInt {
crate::internal_macros::maybe_const_fn! {
/// Gets the length of this VarInt when encoded. /// Gets the length of this VarInt when encoded.
/// ///
/// *Important: this method is only `const` in Rust 1.46 or higher!*
///
/// Returns 1 for 0..=0xFC, 3 for 0xFD..=(2^16-1), 5 for 0x10000..=(2^32-1), /// Returns 1 for 0..=0xFC, 3 for 0xFD..=(2^16-1), 5 for 0x10000..=(2^32-1),
/// and 9 otherwise. /// and 9 otherwise.
#[inline] #[inline]
@ -385,6 +388,7 @@ impl VarInt {
_ => { 9 } _ => { 9 }
} }
} }
}
} }
impl Encodable for VarInt { impl Encodable for VarInt {

View File

@ -45,6 +45,20 @@ macro_rules! impl_consensus_encoding {
); );
} }
pub(crate) use impl_consensus_encoding; pub(crate) use impl_consensus_encoding;
/// Marks the function const in Rust 1.46.0
macro_rules! maybe_const_fn {
($(#[$attr:meta])* $vis:vis fn $name:ident($($args:tt)*) -> $ret:ty $body:block) => {
#[cfg(rust_v_1_46)]
$(#[$attr])*
$vis const fn $name($($args)*) -> $ret $body
#[cfg(not(rust_v_1_46))]
$(#[$attr])*
$vis fn $name($($args)*) -> $ret $body
}
}
pub(crate) use maybe_const_fn;
// We use test_macros module to keep things organised, re-export everything for ease of use. // We use test_macros module to keep things organised, re-export everything for ease of use.
#[cfg(test)] #[cfg(test)]
pub(crate) use test_macros::*; pub(crate) use test_macros::*;