contracts: Don't fail fast if the Weight limit of a cross contract call is too big (#3243)

When doing a cross contract call you can supply an optional Weight limit
for that call. If one doesn't specify the limit (setting it to 0) the
sub call will have all the remaining gas available. If one does specify
the limit we subtract that amount eagerly from the Weight meter and fail
fast if not enough `Weight` is available.

This is quite annoying because setting a fixed limit will set the
`gas_required` in the gas estimation according to the specified limit.
Even if in that dry-run the actual call didn't consume that whole
amount. It effectively discards the more precise measurement it should
have from the dry-run.

This PR changes the behaviour so that the supplied limit is an actual
limit: We do the cross contract call even if the limit is higher than
the remaining `Weight`. We then fail and roll back in the cub call in
case there is not enough weight.

This makes the weight estimation in the dry-run no longer dependent on
the weight limit supplied when doing a cross contract call.

---------

Co-authored-by: PG Herveou <pgherveou@gmail.com>
This commit is contained in:
Alexander Theißen
2024-02-08 15:05:00 +01:00
committed by GitHub
parent 9cd02a07c9
commit 28463a12f0
5 changed files with 155 additions and 105 deletions
+8 -26
View File
@@ -104,9 +104,7 @@ impl<T: Config> GasMeter<T> {
/// # Note
///
/// Passing `0` as amount is interpreted as "all remaining gas".
pub fn nested(&mut self, amount: Weight) -> Result<Self, DispatchError> {
// NOTE that it is ok to allocate all available gas since it still ensured
// by `charge` that it doesn't reach zero.
pub fn nested(&mut self, amount: Weight) -> Self {
let amount = Weight::from_parts(
if amount.ref_time().is_zero() {
self.gas_left().ref_time()
@@ -118,33 +116,17 @@ impl<T: Config> GasMeter<T> {
} else {
amount.proof_size()
},
);
self.gas_left = self.gas_left.checked_sub(&amount).ok_or_else(|| <Error<T>>::OutOfGas)?;
Ok(GasMeter::new(amount))
)
.min(self.gas_left);
self.gas_left -= amount;
GasMeter::new(amount)
}
/// Absorb the remaining gas of a nested meter after we are done using it.
pub fn absorb_nested(&mut self, nested: Self) {
if self.gas_left.ref_time().is_zero() {
// All of the remaining gas was inherited by the nested gas meter. When absorbing
// we can therefore safely inherit the lowest gas that the nested gas meter experienced
// as long as it is lower than the lowest gas that was experienced by the parent.
// We cannot call `self.gas_left_lowest()` here because in the state that this
// code is run the parent gas meter has `0` gas left.
*self.gas_left_lowest.ref_time_mut() =
nested.gas_left_lowest().ref_time().min(self.gas_left_lowest.ref_time());
} else {
// The nested gas meter was created with a fixed amount that did not consume all of the
// parents (self) gas. The lowest gas that self will experience is when the nested
// gas was pre charged with the fixed amount.
*self.gas_left_lowest.ref_time_mut() = self.gas_left_lowest().ref_time();
}
if self.gas_left.proof_size().is_zero() {
*self.gas_left_lowest.proof_size_mut() =
nested.gas_left_lowest().proof_size().min(self.gas_left_lowest.proof_size());
} else {
*self.gas_left_lowest.proof_size_mut() = self.gas_left_lowest().proof_size();
}
self.gas_left_lowest = (self.gas_left + nested.gas_limit)
.saturating_sub(nested.gas_required())
.min(self.gas_left_lowest);
self.gas_left += nested.gas_left;
}