Skip to content

Commit

Permalink
feat: allow zero fees for pre-mine (#6595)
Browse files Browse the repository at this point in the history
Description
---
- Allowed zero fees for pre-mine immediate spend
- Added inputs to genesis block addition for pre-mine immediate spend
- Allow inputs in genesis block

Motivation and Context
---
Inputs of immediate spend transactions need to be considered in the
genesis block.

How Has This Been Tested?
---
System-level testing [**TBD**] to generate a new genesis block, then
unit test `xxx_genesis_sanity_check` must pass.

What process can a PR reviewer use to test or verify this change?
---
Code review

<!-- Checklist -->
<!-- 1. Is the title of your PR in the form that would make nice release
notes? The title, excluding the conventional commit
tag, will be included exactly as is in the CHANGELOG, so please think
about it carefully. -->


Breaking Changes
---

- [x] None
- [ ] Requires data directory on base node to be deleted
- [ ] Requires hard fork
- [ ] Other - Please specify

<!-- Does this include a breaking change? If so, include this line as a
footer -->
<!-- BREAKING CHANGE: Description what the user should do, e.g. delete a
database, resync the chain -->
hansieodendaal authored Oct 3, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 54eb6d7 commit 1d2e314
Showing 7 changed files with 66 additions and 52 deletions.
50 changes: 34 additions & 16 deletions applications/minotari_console_wallet/src/automation/commands.rs
Original file line number Diff line number Diff line change
@@ -1257,7 +1257,11 @@ pub async fn command_runner(

match encumber_aggregate_utxo(
transaction_service.clone(),
session_info.fee_per_gram,
if session_info.use_pre_mine_input_file {
MicroMinotari::zero()
} else {
session_info.fee_per_gram
},
embedded_output.commitment.clone(),
input_shares,
script_signature_public_nonces,
@@ -1630,6 +1634,7 @@ pub async fn command_runner(
}

// Create finalized spend transactions
let mut inputs = Vec::new();
let mut outputs = Vec::new();
let mut kernels = Vec::new();
for (indexed_info, leader_self) in party_info_per_index.iter().zip(leader_info.outputs_for_self.iter())
@@ -1659,22 +1664,17 @@ pub async fn command_runner(
break;
}

if args.print_to_console || args.save_to_file {
if session_info.use_pre_mine_input_file {
match transaction_service.get_any_transaction(leader_self.tx_id).await {
Ok(Some(WalletTransaction::Completed(tx))) => {
if args.save_to_file {
for output in tx.transaction.body.outputs() {
outputs.push(output.clone());
}
for kernel in tx.transaction.body.kernels() {
kernels.push(kernel.clone());
}
for input in tx.transaction.body.inputs() {
inputs.push(input.clone());
}
for output in tx.transaction.body.outputs() {
outputs.push(output.clone());
}
if args.print_to_console {
let tx_console = serde_json::to_string(&tx.transaction).unwrap_or_else(|_| {
format!("Transaction to json conversion error! ('{}')", leader_self.tx_id)
});
println!("Tx_Id: {}, Tx: {}", leader_self.tx_id, tx_console);
for kernel in tx.transaction.body.kernels() {
kernels.push(kernel.clone());
}
},
Ok(_) => {
@@ -1692,7 +1692,7 @@ pub async fn command_runner(
}
}

if args.save_to_file {
if session_info.use_pre_mine_input_file {
let file_name = get_pre_mine_addition_file_name();
let out_dir_path = out_dir(&args.session_id)?;
let out_file = out_dir_path.join(&file_name);
@@ -1705,6 +1705,24 @@ pub async fn command_runner(
};

let mut error = false;
for input in inputs {
let input_s = match serde_json::to_string(&input) {
Ok(val) => val,
Err(e) => {
eprintln!("\nError: Could not serialize UTXO ({})\n", e);
error = true;
break;
},
};
if let Err(e) = file_stream.write_all(format!("{}\n", input_s).as_bytes()) {
eprintln!("\nError: Could not write UTXO to file ({})\n", e);
error = true;
break;
}
}
if error {
break;
}
for output in outputs {
let utxo_s = match serde_json::to_string(&output) {
Ok(val) => val,
@@ -2459,7 +2477,7 @@ fn read_genesis_file_outputs(
}
file
} else {
return Err("Missing pre-mine file!".to_string());
return Err("Missing pre-mine file! Need '--pre-mine-file-path <path_to_file>.'".to_string());
};

let file = File::open(file_path.clone())
6 changes: 2 additions & 4 deletions applications/minotari_console_wallet/src/cli.rs
Original file line number Diff line number Diff line change
@@ -281,10 +281,6 @@ pub struct PreMineSpendAggregateTransactionArgs {
pub session_id: String,
#[clap(long)]
pub input_file_names: Vec<String>,
#[clap(long)]
pub save_to_file: bool,
#[clap(long)]
pub print_to_console: bool,
}

#[derive(Debug, Args, Clone)]
@@ -295,6 +291,8 @@ pub struct PreMineSpendBackupUtxoArgs {
pub output_index: usize,
#[clap(long)]
pub recipient_address: TariAddress,
#[clap(long)]
pub pre_mine_file_path: Option<PathBuf>,
}

#[derive(Debug, Args, Clone)]
24 changes: 21 additions & 3 deletions base_layer/core/src/blocks/genesis_block.rs
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ use crate::{
proof_of_work::{AccumulatedDifficulty, Difficulty, PowAlgorithm, PowData, ProofOfWork},
transactions::{
aggregated_body::AggregateBody,
transaction_components::{TransactionKernel, TransactionOutput},
transaction_components::{TransactionInput, TransactionKernel, TransactionOutput},
},
OutputSmt,
};
@@ -54,9 +54,12 @@ pub fn get_genesis_block(network: Network) -> ChainBlock {

fn add_pre_mine_utxos_to_genesis_block(file: &str, block: &mut Block) {
let mut utxos = Vec::new();
let mut inputs = Vec::new();
for line in file.lines() {
if let Ok(utxo) = serde_json::from_str::<TransactionOutput>(line) {
utxos.push(utxo);
} else if let Ok(input) = serde_json::from_str::<TransactionInput>(line) {
inputs.push(input);
} else if let Ok(kernel) = serde_json::from_str::<TransactionKernel>(line) {
block.body.add_kernel(kernel);
block.header.kernel_mmr_size += 1;
@@ -66,6 +69,7 @@ fn add_pre_mine_utxos_to_genesis_block(file: &str, block: &mut Block) {
}
block.header.output_smt_size += utxos.len() as u64;
block.body.add_outputs(utxos);
block.body.add_inputs(inputs);
block.body.sort();
}

@@ -647,14 +651,28 @@ mod test {
// Check that the pre_mine UTXOs balance (the pre_mine_value consensus constant is set correctly and pre_mine
// kernel is correct)

let utxo_sum = block.block().body.outputs().iter().map(|o| &o.commitment).sum();
let input_sum = block
.block()
.body
.inputs()
.iter()
.map(|o| o.commitment().unwrap())
.sum::<Commitment>();
let utxo_sum = block
.block()
.body
.outputs()
.iter()
.map(|o| &o.commitment)
.sum::<Commitment>();
let total_utxo_sum = &utxo_sum - &input_sum;
let kernel_sum = block.block().body.kernels().iter().map(|k| &k.excess).sum();

let db = create_new_blockchain_with_network(network);

let lock = db.db_read_access().unwrap();
ChainBalanceValidator::new(ConsensusManager::builder(network).build().unwrap(), Default::default())
.validate(&*lock, 0, &utxo_sum, &kernel_sum, &Commitment::default())
.validate(&*lock, 0, &total_utxo_sum, &kernel_sum, &Commitment::default())
.unwrap();
}

9 changes: 0 additions & 9 deletions base_layer/core/src/transactions/fee.rs
Original file line number Diff line number Diff line change
@@ -20,17 +20,13 @@
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

use std::cmp::max;

use super::{tari_amount::MicroMinotari, weight::TransactionWeight};
use crate::transactions::aggregated_body::AggregateBody;

#[derive(Debug, Clone, Copy)]
pub struct Fee(TransactionWeight);

impl Fee {
pub(crate) const MINIMUM_TRANSACTION_FEE: MicroMinotari = MicroMinotari(101);

pub fn new(weight: TransactionWeight) -> Self {
Self(weight)
}
@@ -62,11 +58,6 @@ impl Fee {
Ok(MicroMinotari::from(weight) * fee_per_gram)
}

/// Normalizes the given fee returning a fee that is equal to or above the minimum fee
pub fn normalize(fee: MicroMinotari) -> MicroMinotari {
max(Self::MINIMUM_TRANSACTION_FEE, fee)
}

pub fn weighting(&self) -> &TransactionWeight {
&self.0
}
Original file line number Diff line number Diff line change
@@ -36,7 +36,6 @@ use crate::{
consensus::ConsensusConstants,
covenants::Covenant,
transactions::{
fee::Fee,
key_manager::{TariKeyId, TransactionKeyManagerInterface, TxoStage},
tari_amount::*,
transaction_components::{
@@ -720,11 +719,6 @@ impl SenderTransactionProtocol {
/// Performs sanity checks on the collected transaction pieces prior to building the final Transaction instance
fn validate(&self) -> Result<(), TPE> {
if let SenderState::Finalizing(info) = &self.state {
let fee = info.metadata.fee;
// The fee must be greater than MIN_FEE to prevent spam attacks
if fee < Fee::MINIMUM_TRANSACTION_FEE {
return Err(TPE::ValidationError("Fee is less than the minimum".into()));
}
// Prevent overflow attacks by imposing sane limits on some key parameters
if info.inputs.len() > MAX_TRANSACTION_INPUTS {
return Err(TPE::ValidationError("Too many inputs in transaction".into()));
Original file line number Diff line number Diff line change
@@ -520,10 +520,6 @@ where KM: TransactionKeyManagerInterface
target: LOG_TARGET,
"Build transaction with Fee: {}. Change: {}. Output: {:?}", total_fee, change, change_output,
);
// Some checks on the fee
if total_fee < Fee::MINIMUM_TRANSACTION_FEE {
return self.build_err("Fee is less than the minimum");
}

let change_output_pair = match change_output {
Some((output, sender_offset_key_id)) => {
@@ -843,12 +839,13 @@ mod test {
}

#[tokio::test]
async fn fee_too_low() {
async fn zero_fee_allowed() {
// Create some inputs
let key_manager = create_memory_db_key_manager().unwrap();
let p = TestParams::new(&key_manager).await;
let fee_per_gram = MicroMinotari(0);
let tx_fee = p.fee().calculate(
MicroMinotari(1),
fee_per_gram,
1,
1,
1,
@@ -874,7 +871,7 @@ mod test {
Covenant::default(),
TariAddress::default(),
)
.with_fee_per_gram(MicroMinotari(1))
.with_fee_per_gram(fee_per_gram)
.with_recipient_data(
script,
Default::default(),
@@ -884,8 +881,7 @@ mod test {
)
.await
.unwrap();
let err = builder.build().await.unwrap_err();
assert_eq!(err.message, "Fee is less than the minimum");
assert!(builder.build().await.is_ok(), "Zero fee should be allowed");
}

#[tokio::test]
9 changes: 4 additions & 5 deletions base_layer/wallet/src/output_manager_service/service.rs
Original file line number Diff line number Diff line change
@@ -937,15 +937,14 @@ where
.get_serialized_size()
.map_err(|e| OutputManagerError::ConversionError(e.to_string()))?,
);
let fee = fee_calc.calculate(fee_per_gram, 1, 1, num_outputs, default_features_and_scripts_size);
return Ok(Fee::normalize(fee));
return Ok(fee_calc.calculate(fee_per_gram, 1, 1, num_outputs, default_features_and_scripts_size));
},
Err(e) => Err(e),
}?;

debug!(target: LOG_TARGET, "{} utxos selected.", utxo_selection.utxos.len());

let fee = Fee::normalize(utxo_selection.as_final_fee());
let fee = utxo_selection.as_final_fee();

debug!(target: LOG_TARGET, "Fee calculated: {}", fee);
Ok(fee)
@@ -1280,7 +1279,7 @@ where
output_hash, tx_id
))
})?,
UseOutput::AsProvided(val) => val,
UseOutput::AsProvided(ref val) => val.clone(),
};
if output.commitment != expected_commitment {
return Err(OutputManagerError::ServiceError(format!(
@@ -1389,7 +1388,7 @@ where
let fee = self.get_fee_calc();
let fee = fee.calculate(fee_per_gram, 1, 1, 1, metadata_byte_size);
let amount = input.value - fee;
trace!(target: LOG_TARGET, "encumber_aggregate_utxo: created script");
trace!(target: LOG_TARGET, "encumber_aggregate_utxo: created script, with fee {}", fee);

// Create sender transaction protocol builder with recipient data and no change
let mut builder = SenderTransactionProtocol::builder(

0 comments on commit 1d2e314

Please sign in to comment.