Skip to content

Commit 71536a9

Browse files
authored
Merge pull request #1916 from mintlayer/fix/wallet-fee-calculation
Fix/wallet fee calculation
2 parents 55a3841 + be73675 commit 71536a9

File tree

11 files changed

+742
-277
lines changed

11 files changed

+742
-277
lines changed

common/src/size_estimation/mod.rs

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use std::{
1919
};
2020

2121
use crypto::key::{PrivateKey, PublicKey, Signature};
22-
use serialization::Encode;
22+
use serialization::{CompactLen, Encode};
2323

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

4547
/// Return the encoded size of an input signature.
@@ -192,15 +194,46 @@ pub fn input_signature_size_from_destination(
192194
}
193195
}
194196

195-
/// Return the encoded size for a SignedTransaction with specified outputs and empty inputs and
196-
/// signatures
197-
pub fn tx_size_with_outputs(outputs: &[TxOutput]) -> usize {
198-
let tx = SignedTransaction::new(
199-
Transaction::new(1, vec![], outputs.into()).expect("should not fail"),
200-
vec![],
201-
)
202-
.expect("should not fail");
203-
serialization::Encode::encoded_size(&tx)
197+
/// Return the encoded size for a SignedTransaction also accounting for the compact encoding of the
198+
/// vectors for the specified number of inputs and outputs
199+
pub fn tx_size_with_num_inputs_and_outputs(
200+
num_outputs: usize,
201+
num_inputs: usize,
202+
) -> Result<usize, SizeEstimationError> {
203+
lazy_static::lazy_static! {
204+
static ref EMPTY_SIGNED_TX_SIZE: usize = {
205+
let tx = SignedTransaction::new(
206+
Transaction::new(1, vec![], vec![]).expect("should not fail"),
207+
vec![],
208+
)
209+
.expect("should not fail");
210+
serialization::Encode::encoded_size(&tx)
211+
};
212+
}
213+
lazy_static::lazy_static! {
214+
static ref ZERO_COMPACT_SIZE: usize = {
215+
serialization::Compact::<u32>::compact_len(&0)
216+
};
217+
}
218+
219+
let input_compact_size_diff = serialization::Compact::<u32>::compact_len(
220+
&(num_inputs
221+
.try_into()
222+
.map_err(|_| SizeEstimationError::TooManyElements(num_inputs))?),
223+
) - *ZERO_COMPACT_SIZE;
224+
225+
let output_compact_size_diff = serialization::Compact::<u32>::compact_len(
226+
&(num_outputs
227+
.try_into()
228+
.map_err(|_| SizeEstimationError::TooManyElements(num_inputs))?),
229+
) - *ZERO_COMPACT_SIZE;
230+
231+
// 2 for number of inputs and number of input signatures
232+
Ok(*EMPTY_SIGNED_TX_SIZE + output_compact_size_diff + (input_compact_size_diff * 2))
233+
}
234+
235+
pub fn outputs_encoded_size(outputs: &[TxOutput]) -> usize {
236+
outputs.iter().map(serialization::Encode::encoded_size).sum()
204237
}
205238

206239
fn get_tx_output_destination(txo: &TxOutput) -> Option<&Destination> {
@@ -219,3 +252,6 @@ fn get_tx_output_destination(txo: &TxOutput) -> Option<&Destination> {
219252
| TxOutput::CreateOrder(_) => None,
220253
}
221254
}
255+
256+
#[cfg(test)]
257+
mod tests;

common/src/size_estimation/tests.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) 2025 RBB S.r.l
2+
3+
// SPDX-License-Identifier: MIT
4+
// Licensed under the MIT License;
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
use randomness::Rng;
17+
use rstest::rstest;
18+
use test_utils::random::{make_seedable_rng, Seed};
19+
20+
use crate::chain::{
21+
signature::{
22+
inputsig::{standard_signature::StandardInputSignature, InputWitness},
23+
sighash::sighashtype::SigHashType,
24+
},
25+
OutPointSourceId, SignedTransaction, Transaction, TxInput,
26+
};
27+
use crate::primitives::{Amount, Id};
28+
29+
#[rstest]
30+
#[trace]
31+
#[case(Seed::from_entropy())]
32+
fn estimate_tx_size(
33+
#[case] seed: Seed,
34+
#[values(1..64, 64..0x4000, 0x4000..0x4001)] inputs_range: std::ops::Range<u32>,
35+
#[values(1..64, 64..0x4000, 0x4000..0x4001)] outputs_range: std::ops::Range<u32>,
36+
) {
37+
use crypto::key::{KeyKind, PrivateKey};
38+
use serialization::Encode;
39+
40+
use crate::{
41+
chain::{
42+
output_value::OutputValue,
43+
signature::inputsig::authorize_pubkey_spend::AuthorizedPublicKeySpend, Destination,
44+
TxOutput,
45+
},
46+
size_estimation::tx_size_with_num_inputs_and_outputs,
47+
};
48+
49+
let mut rng = make_seedable_rng(seed);
50+
51+
let num_inputs = rng.gen_range(inputs_range);
52+
let inputs = (0..num_inputs)
53+
.map(|_| {
54+
TxInput::from_utxo(
55+
OutPointSourceId::Transaction(Id::random_using(&mut rng)),
56+
rng.gen_range(0..100),
57+
)
58+
})
59+
.collect();
60+
61+
let num_outputs = rng.gen_range(outputs_range);
62+
let outputs = (0..num_outputs)
63+
.map(|_| {
64+
let destination = Destination::PublicKey(
65+
crypto::key::PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr).1,
66+
);
67+
68+
TxOutput::Transfer(
69+
OutputValue::Coin(Amount::from_atoms(rng.gen_range(1..10000))),
70+
destination,
71+
)
72+
})
73+
.collect();
74+
75+
let tx = Transaction::new(0, inputs, outputs).unwrap();
76+
let signatures = (0..num_inputs)
77+
.map(|_| {
78+
let private_key =
79+
PrivateKey::new_from_rng(&mut rng, crypto::key::KeyKind::Secp256k1Schnorr).0;
80+
let signature = private_key.sign_message(&[0; 32], &mut rng).unwrap();
81+
let raw_signature = AuthorizedPublicKeySpend::new(signature).encode();
82+
let standard = StandardInputSignature::new(SigHashType::all(), raw_signature);
83+
InputWitness::Standard(standard)
84+
})
85+
.collect();
86+
let tx = SignedTransaction::new(tx, signatures).unwrap();
87+
88+
let estimated_tx_size =
89+
tx_size_with_num_inputs_and_outputs(num_outputs as usize, num_inputs as usize).unwrap()
90+
+ tx.inputs().iter().map(Encode::encoded_size).sum::<usize>()
91+
+ tx.signatures().iter().map(Encode::encoded_size).sum::<usize>()
92+
+ tx.outputs().iter().map(Encode::encoded_size).sum::<usize>();
93+
94+
let expected_tx_size = Encode::encoded_size(&tx);
95+
96+
assert_eq!(estimated_tx_size, expected_tx_size);
97+
}

serialization/core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
1818
// Re-export SCALE traits
1919
pub use parity_scale_codec::{
20-
Codec, Decode, DecodeAll, Encode, EncodeLike, Input, Output, WrapperTypeDecode,
20+
Codec, CompactLen, Decode, DecodeAll, Encode, EncodeLike, Input, Output, WrapperTypeDecode,
2121
WrapperTypeEncode,
2222
};
2323

0 commit comments

Comments
 (0)