Skip to content

Fix/wallet fee calculation #1916

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions common/src/size_estimation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use std::{
};

use crypto::key::{PrivateKey, PublicKey, Signature};
use serialization::Encode;
use serialization::{CompactLen, Encode};

use crate::chain::{
classic_multisig::ClassicMultisigChallenge,
Expand All @@ -40,6 +40,8 @@ use crate::chain::{
pub enum SizeEstimationError {
#[error("Unsupported input destination")]
UnsupportedInputDestination(Destination),
#[error("Attempted to estimate the size of a TX with too many inputs or outputs {0}")]
TooManyElements(usize),
}

/// Return the encoded size of an input signature.
Expand Down Expand Up @@ -192,15 +194,46 @@ pub fn input_signature_size_from_destination(
}
}

/// Return the encoded size for a SignedTransaction with specified outputs and empty inputs and
/// signatures
pub fn tx_size_with_outputs(outputs: &[TxOutput]) -> usize {
let tx = SignedTransaction::new(
Transaction::new(1, vec![], outputs.into()).expect("should not fail"),
vec![],
)
.expect("should not fail");
serialization::Encode::encoded_size(&tx)
/// Return the encoded size for a SignedTransaction also accounting for the compact encoding of the
/// vectors for the specified number of inputs and outputs
pub fn tx_size_with_num_inputs_and_outputs(
num_outputs: usize,
num_inputs: usize,
) -> Result<usize, SizeEstimationError> {
lazy_static::lazy_static! {
static ref EMPTY_SIGNED_TX_SIZE: usize = {
let tx = SignedTransaction::new(
Transaction::new(1, vec![], vec![]).expect("should not fail"),
vec![],
)
.expect("should not fail");
serialization::Encode::encoded_size(&tx)
};
}
lazy_static::lazy_static! {
static ref ZERO_COMPACT_SIZE: usize = {
serialization::Compact::<u32>::compact_len(&0)
};
}

let input_compact_size_diff = serialization::Compact::<u32>::compact_len(
&(num_inputs
.try_into()
.map_err(|_| SizeEstimationError::TooManyElements(num_inputs))?),
) - *ZERO_COMPACT_SIZE;

let output_compact_size_diff = serialization::Compact::<u32>::compact_len(
&(num_outputs
.try_into()
.map_err(|_| SizeEstimationError::TooManyElements(num_inputs))?),
) - *ZERO_COMPACT_SIZE;

// 2 for number of inputs and number of input signatures
Ok(*EMPTY_SIGNED_TX_SIZE + output_compact_size_diff + (input_compact_size_diff * 2))
}

pub fn outputs_encoded_size(outputs: &[TxOutput]) -> usize {
outputs.iter().map(serialization::Encode::encoded_size).sum()
}

fn get_tx_output_destination(txo: &TxOutput) -> Option<&Destination> {
Expand All @@ -219,3 +252,6 @@ fn get_tx_output_destination(txo: &TxOutput) -> Option<&Destination> {
| TxOutput::CreateOrder(_) => None,
}
}

#[cfg(test)]
mod tests;
97 changes: 97 additions & 0 deletions common/src/size_estimation/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) 2025 RBB S.r.l
// [email protected]
// SPDX-License-Identifier: MIT
// Licensed under the MIT License;
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use randomness::Rng;
use rstest::rstest;
use test_utils::random::{make_seedable_rng, Seed};

use crate::chain::{
signature::{
inputsig::{standard_signature::StandardInputSignature, InputWitness},
sighash::sighashtype::SigHashType,
},
OutPointSourceId, SignedTransaction, Transaction, TxInput,
};
use crate::primitives::{Amount, Id};

#[rstest]
#[trace]
#[case(Seed::from_entropy())]
fn estimate_tx_size(
#[case] seed: Seed,
#[values(1..64, 64..0x4000, 0x4000..0x4001)] inputs_range: std::ops::Range<u32>,
#[values(1..64, 64..0x4000, 0x4000..0x4001)] outputs_range: std::ops::Range<u32>,
) {
use crypto::key::{KeyKind, PrivateKey};
use serialization::Encode;

use crate::{
chain::{
output_value::OutputValue,
signature::inputsig::authorize_pubkey_spend::AuthorizedPublicKeySpend, Destination,
TxOutput,
},
size_estimation::tx_size_with_num_inputs_and_outputs,
};

let mut rng = make_seedable_rng(seed);

let num_inputs = rng.gen_range(inputs_range);
let inputs = (0..num_inputs)
.map(|_| {
TxInput::from_utxo(
OutPointSourceId::Transaction(Id::random_using(&mut rng)),
rng.gen_range(0..100),
)
})
.collect();

let num_outputs = rng.gen_range(outputs_range);
let outputs = (0..num_outputs)
.map(|_| {
let destination = Destination::PublicKey(
crypto::key::PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr).1,
);

TxOutput::Transfer(
OutputValue::Coin(Amount::from_atoms(rng.gen_range(1..10000))),
destination,
)
})
.collect();

let tx = Transaction::new(0, inputs, outputs).unwrap();
let signatures = (0..num_inputs)
.map(|_| {
let private_key =
PrivateKey::new_from_rng(&mut rng, crypto::key::KeyKind::Secp256k1Schnorr).0;
let signature = private_key.sign_message(&[0; 32], &mut rng).unwrap();
let raw_signature = AuthorizedPublicKeySpend::new(signature).encode();
let standard = StandardInputSignature::new(SigHashType::all(), raw_signature);
InputWitness::Standard(standard)
})
.collect();
let tx = SignedTransaction::new(tx, signatures).unwrap();

let estimated_tx_size =
tx_size_with_num_inputs_and_outputs(num_outputs as usize, num_inputs as usize).unwrap()
+ tx.inputs().iter().map(Encode::encoded_size).sum::<usize>()
+ tx.signatures().iter().map(Encode::encoded_size).sum::<usize>()
+ tx.outputs().iter().map(Encode::encoded_size).sum::<usize>();

let expected_tx_size = Encode::encoded_size(&tx);

assert_eq!(estimated_tx_size, expected_tx_size);
}
2 changes: 1 addition & 1 deletion serialization/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

// Re-export SCALE traits
pub use parity_scale_codec::{
Codec, Decode, DecodeAll, Encode, EncodeLike, Input, Output, WrapperTypeDecode,
Codec, CompactLen, Decode, DecodeAll, Encode, EncodeLike, Input, Output, WrapperTypeDecode,
WrapperTypeEncode,
};

Expand Down
Loading