From d56d202aeb00c6c26942317919e6adf4772cb689 Mon Sep 17 00:00:00 2001 From: Martin Habovstiak Date: Thu, 16 Mar 2023 20:02:37 +0100 Subject: [PATCH] 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. --- bitcoin/src/blockdata/transaction.rs | 72 ++++++++++++++++++++++++++++ bitcoin/src/consensus/encode.rs | 4 ++ bitcoin/src/internal_macros.rs | 14 ++++++ 3 files changed, 90 insertions(+) diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs index 5d3af10b..160d57dd 100644 --- a/bitcoin/src/blockdata/transaction.rs +++ b/bitcoin/src/blockdata/transaction.rs @@ -1247,6 +1247,11 @@ pub fn predict_weight(inputs: I, output_script_lens: O) -> Weight let script_size = script_len + VarInt(script_len as u64).len(); (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 output_size = 8 * output_count + output_scripts_size; let non_input_size = @@ -1265,6 +1270,42 @@ pub fn predict_weight(inputs: I, output_script_lens: O) -> Weight }; 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. /// @@ -1351,6 +1392,37 @@ impl InputWeightPrediction { 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)] diff --git a/bitcoin/src/consensus/encode.rs b/bitcoin/src/consensus/encode.rs index 89beca6d..e2131b4f 100644 --- a/bitcoin/src/consensus/encode.rs +++ b/bitcoin/src/consensus/encode.rs @@ -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'. impl VarInt { + crate::internal_macros::maybe_const_fn! { /// 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), /// and 9 otherwise. #[inline] @@ -385,6 +388,7 @@ impl VarInt { _ => { 9 } } } + } } impl Encodable for VarInt { diff --git a/bitcoin/src/internal_macros.rs b/bitcoin/src/internal_macros.rs index dc49d44a..a5a07645 100644 --- a/bitcoin/src/internal_macros.rs +++ b/bitcoin/src/internal_macros.rs @@ -45,6 +45,20 @@ macro_rules! 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. #[cfg(test)] pub(crate) use test_macros::*;