use crate::{
consensus::{emission::Emission, ConsensusConstants},
transactions::{
tari_amount::{uT, MicroTari},
transaction::{
KernelBuilder,
KernelFeatures,
OutputFeatures,
Transaction,
TransactionBuilder,
UnblindedOutput,
},
transaction_protocol::{build_challenge, RewindData, TransactionMetadata},
types::{BlindingFactor, CryptoFactories, PrivateKey, PublicKey, Signature},
},
};
use tari_crypto::{commitment::HomomorphicCommitmentFactory, keys::PublicKey as PK};
use thiserror::Error;
#[derive(Debug, Clone, Error, PartialEq)]
pub enum CoinbaseBuildError {
#[error("The block height for this coinbase transaction wasn't provided")]
MissingBlockHeight,
#[error("The value for the coinbase transaction is missing")]
MissingFees,
#[error("The private nonce for this coinbase transaction wasn't provided")]
MissingNonce,
#[error("The spend key for this coinbase transaction wasn't provided")]
MissingSpendKey,
#[error("An error occurred building the final transaction: `{0}`")]
BuildError(String),
#[error("Some inconsistent data was given to the builder. This transaction is not valid")]
InvalidTransaction,
}
pub struct CoinbaseBuilder {
factories: CryptoFactories,
block_height: Option<u64>,
fees: Option<MicroTari>,
spend_key: Option<PrivateKey>,
private_nonce: Option<PrivateKey>,
rewind_data: Option<RewindData>,
}
impl CoinbaseBuilder {
pub fn new(factories: CryptoFactories) -> Self {
CoinbaseBuilder {
factories,
block_height: None,
fees: None,
spend_key: None,
private_nonce: None,
rewind_data: None,
}
}
pub fn with_block_height(mut self, height: u64) -> Self {
self.block_height = Some(height);
self
}
pub fn with_fees(mut self, value: MicroTari) -> Self {
self.fees = Some(value);
self
}
pub fn with_spend_key(mut self, key: PrivateKey) -> Self {
self.spend_key = Some(key);
self
}
pub fn with_nonce(mut self, nonce: PrivateKey) -> Self {
self.private_nonce = Some(nonce);
self
}
pub fn with_rewind_data(mut self, rewind_data: RewindData) -> Self {
self.rewind_data = Some(rewind_data);
self
}
pub fn build(
self,
constants: &ConsensusConstants,
emission_schedule: &dyn Emission,
) -> Result<(Transaction, UnblindedOutput), CoinbaseBuildError>
{
let height = self
.block_height
.ok_or_else(|| CoinbaseBuildError::MissingBlockHeight)?;
let reward = emission_schedule.block_reward(height);
self.build_with_reward(constants, reward)
}
#[allow(clippy::erasing_op)] pub fn build_with_reward(
self,
constants: &ConsensusConstants,
block_reward: MicroTari,
) -> Result<(Transaction, UnblindedOutput), CoinbaseBuildError>
{
let height = self
.block_height
.ok_or_else(|| CoinbaseBuildError::MissingBlockHeight)?;
let total_reward = block_reward + self.fees.ok_or_else(|| CoinbaseBuildError::MissingFees)?;
let nonce = self.private_nonce.ok_or_else(|| CoinbaseBuildError::MissingNonce)?;
let public_nonce = PublicKey::from_secret_key(&nonce);
let key = self.spend_key.ok_or_else(|| CoinbaseBuildError::MissingSpendKey)?;
let output_features = OutputFeatures::create_coinbase(height + constants.coinbase_lock_height());
let excess = self.factories.commitment.commit_value(&key, 0);
let kernel_features = KernelFeatures::create_coinbase();
let metadata = TransactionMetadata::default();
let challenge = build_challenge(&public_nonce, &metadata);
let sig = Signature::sign(key.clone(), nonce, &challenge)
.map_err(|_| CoinbaseBuildError::BuildError("Challenge could not be represented as a scalar".into()))?;
let unblinded_output = UnblindedOutput::new(total_reward, key, Some(output_features));
let output = if let Some(rewind_data) = self.rewind_data.as_ref() {
unblinded_output
.as_rewindable_transaction_output(&self.factories, rewind_data)
.map_err(|e| CoinbaseBuildError::BuildError(e.to_string()))?
} else {
unblinded_output
.as_transaction_output(&self.factories)
.map_err(|e| CoinbaseBuildError::BuildError(e.to_string()))?
};
let kernel = KernelBuilder::new()
.with_fee(0 * uT)
.with_features(kernel_features)
.with_lock_height(0)
.with_excess(&excess)
.with_signature(&sig)
.build()
.map_err(|e| CoinbaseBuildError::BuildError(e.to_string()))?;
let mut builder = TransactionBuilder::new();
builder
.add_output(output)
.add_offset(BlindingFactor::default())
.with_reward(total_reward)
.with_kernel(kernel);
let tx = builder
.build(&self.factories)
.map_err(|e| CoinbaseBuildError::BuildError(e.to_string()))?;
Ok((tx, unblinded_output))
}
}
#[cfg(test)]
mod test {
use crate::{
consensus::{ConsensusManager, ConsensusManagerBuilder, Network},
transactions::{
coinbase_builder::CoinbaseBuildError,
helpers::TestParams,
tari_amount::uT,
transaction::{KernelFeatures, OutputFeatures, OutputFlags, TransactionError, UnblindedOutput},
transaction_protocol::RewindData,
types::{BlindingFactor, CryptoFactories, PrivateKey},
CoinbaseBuilder,
},
};
use rand::rngs::OsRng;
use tari_crypto::{commitment::HomomorphicCommitmentFactory, keys::SecretKey as SecretKeyTrait};
fn get_builder() -> (CoinbaseBuilder, ConsensusManager, CryptoFactories) {
let network = Network::LocalNet;
let rules = ConsensusManagerBuilder::new(network).build();
let factories = CryptoFactories::default();
(CoinbaseBuilder::new(factories.clone()), rules, factories)
}
#[test]
fn missing_height() {
let (builder, rules, _) = get_builder();
assert_eq!(
builder
.build(rules.consensus_constants(0), rules.emission_schedule())
.unwrap_err(),
CoinbaseBuildError::MissingBlockHeight
);
}
#[test]
fn missing_fees() {
let (builder, rules, _) = get_builder();
let builder = builder.with_block_height(42);
assert_eq!(
builder
.build(rules.consensus_constants(42), rules.emission_schedule())
.unwrap_err(),
CoinbaseBuildError::MissingFees
);
}
#[test]
fn missing_spend_key() {
let p = TestParams::new();
let (builder, rules, _) = get_builder();
let builder = builder.with_block_height(42).with_fees(0 * uT).with_nonce(p.nonce);
assert_eq!(
builder
.build(rules.consensus_constants(42), rules.emission_schedule())
.unwrap_err(),
CoinbaseBuildError::MissingSpendKey
);
}
#[test]
fn valid_coinbase() {
let p = TestParams::new();
let (builder, rules, factories) = get_builder();
let builder = builder
.with_block_height(42)
.with_fees(145 * uT)
.with_nonce(p.nonce.clone())
.with_spend_key(p.spend_key.clone());
let (tx, unblinded_output) = builder
.build(rules.consensus_constants(42), rules.emission_schedule())
.unwrap();
let utxo = &tx.body.outputs()[0];
let block_reward = rules.emission_schedule().block_reward(42) + 145 * uT;
let unblinded_test = UnblindedOutput::new(block_reward, p.spend_key.clone(), Some(utxo.features.clone()));
assert_eq!(unblinded_output, unblinded_test);
assert!(factories
.commitment
.open_value(&p.spend_key, block_reward.into(), utxo.commitment()));
assert!(utxo.verify_range_proof(&factories.range_proof).unwrap());
assert!(utxo.features.flags.contains(OutputFlags::COINBASE_OUTPUT));
assert_eq!(
tx.body.check_coinbase_output(
block_reward,
rules.consensus_constants(0).coinbase_lock_height(),
&factories,
42
),
Ok(())
);
}
#[test]
fn valid_coinbase_with_rewindable_output() {
let rewind_key = PrivateKey::random(&mut OsRng);
let rewind_blinding_key = PrivateKey::random(&mut OsRng);
let proof_message = b"alice__12345678910111";
let rewind_data = RewindData {
rewind_key: rewind_key.clone(),
rewind_blinding_key: rewind_blinding_key.clone(),
proof_message: proof_message.clone(),
};
let p = TestParams::new();
let (builder, rules, factories) = get_builder();
let builder = builder
.with_block_height(42)
.with_fees(145 * uT)
.with_nonce(p.nonce.clone())
.with_spend_key(p.spend_key.clone())
.with_rewind_data(rewind_data.clone());
let (tx, _unblinded_output) = builder
.build(rules.consensus_constants(42), rules.emission_schedule())
.unwrap();
let block_reward = rules.emission_schedule().block_reward(42) + 145 * uT;
let rewind_result = tx.body.outputs()[0]
.full_rewind_range_proof(&factories.range_proof, &rewind_key, &rewind_blinding_key)
.unwrap();
assert_eq!(rewind_result.committed_value, block_reward);
assert_eq!(&rewind_result.proof_message, proof_message);
assert_eq!(rewind_result.blinding_factor, p.spend_key);
}
#[test]
fn invalid_coinbase_maturity() {
let p = TestParams::new();
let (builder, rules, factories) = get_builder();
let block_reward = rules.emission_schedule().block_reward(42) + 145 * uT;
let builder = builder
.with_block_height(42)
.with_fees(145 * uT)
.with_nonce(p.nonce.clone())
.with_spend_key(p.spend_key.clone());
let (mut tx, _) = builder
.build(rules.consensus_constants(42), rules.emission_schedule())
.unwrap();
tx.body.outputs_mut()[0].features.maturity = 1;
assert_eq!(
tx.body.check_coinbase_output(
block_reward,
rules.consensus_constants(0).coinbase_lock_height(),
&factories,
42
),
Err(TransactionError::InvalidCoinbaseMaturity)
);
}
#[test]
fn invalid_coinbase_value() {
let p = TestParams::new();
let (builder, rules, factories) = get_builder();
let missing_fee = rules.emission_schedule().block_reward(4200000) + (2 * uT);
let builder = builder
.with_block_height(42)
.with_fees(1 * uT)
.with_nonce(p.nonce.clone())
.with_spend_key(p.spend_key.clone());
let (mut tx, _) = builder
.build(rules.consensus_constants(0), rules.emission_schedule())
.unwrap();
let block_reward = rules.emission_schedule().block_reward(42) + missing_fee;
let builder = CoinbaseBuilder::new(factories.clone());
let builder = builder
.with_block_height(4200000)
.with_fees(1 * uT)
.with_nonce(p.nonce.clone())
.with_spend_key(p.spend_key.clone());
let (tx2, _) = builder
.build(rules.consensus_constants(0), rules.emission_schedule())
.unwrap();
let mut coinbase2 = tx2.body.outputs()[0].clone();
let mut coinbase_kernel2 = tx2.body.kernels()[0].clone();
coinbase2.features = OutputFeatures::default();
coinbase_kernel2.features = KernelFeatures::empty();
tx.body.add_output(coinbase2);
tx.body.add_kernel(coinbase_kernel2);
assert_eq!(
tx.body.check_coinbase_output(
block_reward,
rules.consensus_constants(0).coinbase_lock_height(),
&factories,
42
),
Err(TransactionError::InvalidCoinbase)
);
assert_eq!(
tx.body
.validate_internal_consistency(&BlindingFactor::default(), block_reward, &factories),
Ok(())
);
let builder = CoinbaseBuilder::new(factories.clone());
let builder = builder
.with_block_height(42)
.with_fees(missing_fee)
.with_nonce(p.nonce.clone())
.with_spend_key(p.spend_key.clone());
let (tx3, _) = builder
.build(rules.consensus_constants(0), rules.emission_schedule())
.unwrap();
assert!(tx3
.body
.check_coinbase_output(
block_reward,
rules.consensus_constants(0).coinbase_lock_height(),
&factories,
42
)
.is_ok());
}
#[test]
fn invalid_coinbase_amount() {
let p = TestParams::new();
let (builder, rules, factories) = get_builder();
let missing_fee = rules.emission_schedule().block_reward(4200000) + (2 * uT);
let builder = builder
.with_block_height(42)
.with_fees(1 * uT)
.with_nonce(p.nonce.clone())
.with_spend_key(p.spend_key.clone());
let (mut tx, _) = builder
.build(rules.consensus_constants(0), rules.emission_schedule())
.unwrap();
let block_reward = rules.emission_schedule().block_reward(42) + missing_fee;
let builder = CoinbaseBuilder::new(factories.clone());
let builder = builder
.with_block_height(4200000)
.with_fees(1 * uT)
.with_nonce(p.nonce.clone())
.with_spend_key(p.spend_key.clone());
let (tx2, _) = builder
.build(rules.consensus_constants(0), rules.emission_schedule())
.unwrap();
let mut tx_kernel_test = tx.clone();
let coinbase2 = tx2.body.outputs()[0].clone();
let mut coinbase_kernel2 = tx2.body.kernels()[0].clone();
coinbase_kernel2.features = KernelFeatures::empty();
tx.body.add_output(coinbase2);
tx.body.add_kernel(coinbase_kernel2);
let mut coinbase2 = tx2.body.outputs()[0].clone();
coinbase2.features = OutputFeatures::default();
let coinbase_kernel2 = tx2.body.kernels()[0].clone();
tx_kernel_test.body.add_output(coinbase2);
tx_kernel_test.body.add_kernel(coinbase_kernel2);
assert_eq!(
tx.body.check_coinbase_output(
block_reward,
rules.consensus_constants(0).coinbase_lock_height(),
&factories,
42
),
Err(TransactionError::MoreThanOneCoinbase)
);
assert_eq!(
tx_kernel_test.body.check_coinbase_output(
block_reward,
rules.consensus_constants(0).coinbase_lock_height(),
&factories,
42
),
Err(TransactionError::MoreThanOneCoinbase)
);
assert_eq!(
tx.body
.validate_internal_consistency(&BlindingFactor::default(), block_reward, &factories),
Ok(())
);
}
}