From c64ab6d15220c132cbe2737a24800113e8715207 Mon Sep 17 00:00:00 2001 From: tommytrg Date: Mon, 9 Oct 2023 18:29:38 +0200 Subject: [PATCH] feat: add StakeTransaction (split commit) --- data_structures/src/chain/mod.rs | 11 +- data_structures/src/error.rs | 27 +++- data_structures/src/superblock.rs | 3 + data_structures/src/transaction.rs | 98 +++++++++++++ data_structures/src/transaction_factory.rs | 5 +- data_structures/src/types.rs | 1 + node/src/actors/chain_manager/mining.rs | 5 + schemas/witnet/witnet.proto | 19 +++ validations/src/tests/mod.rs | 127 ++++++++++++++++- validations/src/validations.rs | 151 ++++++++++++++++++++- wallet/src/model.rs | 2 + wallet/src/repository/wallet/mod.rs | 1 + wallet/src/types.rs | 5 +- 13 files changed, 444 insertions(+), 11 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index 34910b8a5..56a1d2bd9 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -46,7 +46,8 @@ use crate::{ superblock::SuperBlockState, transaction::{ CommitTransaction, DRTransaction, DRTransactionBody, Memoized, MintTransaction, - RevealTransaction, TallyTransaction, Transaction, TxInclusionProof, VTTransaction, + RevealTransaction, StakeTransaction, TallyTransaction, Transaction, TxInclusionProof, + VTTransaction, }, transaction::{ MemoHash, MemoizedHashable, BETA, COMMIT_WEIGHT, OUTPUT_SIZE, REVEAL_WEIGHT, TALLY_WEIGHT, @@ -416,6 +417,8 @@ pub struct BlockTransactions { pub reveal_txns: Vec, /// A list of signed tally transactions pub tally_txns: Vec, + /// A list of signed stake transactions + pub stake_txns: Vec, } impl Block { @@ -444,6 +447,7 @@ impl Block { commit_txns: vec![], reveal_txns: vec![], tally_txns: vec![], + stake_txns: vec![], }; /// Function to calculate a merkle tree from a transaction vector @@ -468,6 +472,7 @@ impl Block { commit_hash_merkle_root: merkle_tree_root(&txns.commit_txns), reveal_hash_merkle_root: merkle_tree_root(&txns.reveal_txns), tally_hash_merkle_root: merkle_tree_root(&txns.tally_txns), + stake_hash_merkle_root: merkle_tree_root(&txns.stake_txns), }; Block::new( @@ -682,6 +687,8 @@ pub struct BlockMerkleRoots { pub reveal_hash_merkle_root: Hash, /// A 256-bit hash based on all of the tally transactions committed to this block pub tally_hash_merkle_root: Hash, + /// A 256-bit hash based on all of the stake transactions committed to this block + pub stake_hash_merkle_root: Hash, } /// Function to calculate a merkle tree from a transaction vector @@ -710,6 +717,7 @@ impl BlockMerkleRoots { commit_hash_merkle_root: merkle_tree_root(&txns.commit_txns), reveal_hash_merkle_root: merkle_tree_root(&txns.reveal_txns), tally_hash_merkle_root: merkle_tree_root(&txns.tally_txns), + stake_hash_merkle_root: merkle_tree_root(&txns.stake_txns), } } } @@ -2242,6 +2250,7 @@ impl TransactionsPool { // be impossible for nodes to broadcast these kinds of transactions. Transaction::Tally(_tt) => Err(TransactionError::NotValidTransaction), Transaction::Mint(_mt) => Err(TransactionError::NotValidTransaction), + Transaction::Stake(_mt) => !unimplemented!("contains Stake tx"), } } diff --git a/data_structures/src/error.rs b/data_structures/src/error.rs index 1189507ce..1909c6bc8 100644 --- a/data_structures/src/error.rs +++ b/data_structures/src/error.rs @@ -4,7 +4,8 @@ use failure::Fail; use std::num::ParseIntError; use crate::chain::{ - DataRequestOutput, Epoch, Hash, HashParseError, OutputPointer, PublicKeyHash, RADType, + DataRequestOutput, Epoch, Hash, HashParseError, OutputPointer, PublicKeyHash, + RADType, }; /// The error type for operations on a [`ChainInfo`](ChainInfo) @@ -278,6 +279,21 @@ pub enum TransactionError { max_weight: u32, dr_output: Box, }, + /// Stake weight limit exceeded + #[fail( + display = "Stake ({}) doesn't reach the minimum amount ({})", + min_stake, stake + )] + MinStakeNotReached { + min_stake: u64, + stake: u64, + }, + /// An stake output with zero value does not make sense + #[fail( + display = "Transaction {} has a zero value stake output", + tx_hash, + )] + ZeroValueStakeOutput{ tx_hash: Hash }, #[fail( display = "The reward-to-collateral ratio for this data request is {}, but must be equal or less than {}", reward_collateral_ratio, required_reward_collateral_ratio @@ -401,6 +417,15 @@ pub enum BlockError { weight, max_weight )] TotalDataRequestWeightLimitExceeded { weight: u32, max_weight: u32 }, + /// Stake weight limit exceeded + #[fail( + display = "Total weight of Stake Transactions in a block ({}) exceeds the limit ({})", + weight, max_weight + )] + TotalStakeWeightLimitExceeded { weight: u32, max_weight: u32 }, + /// Repeated operator Stake + #[fail(display = "A single operator is staking more than once: ({}) ", pkh)] + RepeatedStakeOperator { pkh: Hash }, /// Missing expected tallies #[fail( display = "{} expected tally transactions are missing in block candidate {}", diff --git a/data_structures/src/superblock.rs b/data_structures/src/superblock.rs index 8bbf2c0aa..17d459fb5 100644 --- a/data_structures/src/superblock.rs +++ b/data_structures/src/superblock.rs @@ -801,6 +801,7 @@ mod tests { commit_hash_merkle_root: default_hash, reveal_hash_merkle_root: default_hash, tally_hash_merkle_root: tally_merkle_root_1, + stake_hash_merkle_root: default_hash, }, proof: default_proof, bn256_public_key: None, @@ -850,6 +851,7 @@ mod tests { commit_hash_merkle_root: default_hash, reveal_hash_merkle_root: default_hash, tally_hash_merkle_root: tally_merkle_root_1, + stake_hash_merkle_root: default_hash, }, proof: default_proof.clone(), bn256_public_key: None, @@ -865,6 +867,7 @@ mod tests { commit_hash_merkle_root: default_hash, reveal_hash_merkle_root: default_hash, tally_hash_merkle_root: tally_merkle_root_2, + stake_hash_merkle_root: default_hash, }, proof: default_proof, bn256_public_key: None, diff --git a/data_structures/src/transaction.rs b/data_structures/src/transaction.rs index 4c98820e6..e49960d70 100644 --- a/data_structures/src/transaction.rs +++ b/data_structures/src/transaction.rs @@ -18,6 +18,7 @@ use crate::{ // https://github.com/witnet/WIPs/blob/master/wip-0007.md pub const INPUT_SIZE: u32 = 133; pub const OUTPUT_SIZE: u32 = 36; +pub const STAKE_OUTPUT_SIZE: u32 = 105; pub const COMMIT_WEIGHT: u32 = 400; pub const REVEAL_WEIGHT: u32 = 200; pub const TALLY_WEIGHT: u32 = 100; @@ -130,6 +131,7 @@ pub enum Transaction { Reveal(RevealTransaction), Tally(TallyTransaction), Mint(MintTransaction), + Stake(StakeTransaction), } impl From for Transaction { @@ -168,6 +170,12 @@ impl From for Transaction { } } +impl From for Transaction { + fn from(transaction: StakeTransaction) -> Self { + Self::Stake(transaction) + } +} + impl AsRef for Transaction { fn as_ref(&self) -> &Self { self @@ -683,6 +691,80 @@ impl MintTransaction { } } +#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash)] +#[protobuf_convert(pb = "witnet::StakeTransaction")] +pub struct StakeTransaction { + pub body: StakeTransactionBody, + pub signatures: Vec, +} +impl StakeTransaction { + // Creates a new stake transaction. + pub fn new(body: StakeTransactionBody, signatures: Vec) -> Self { + StakeTransaction { body, signatures } + } + + /// Returns the weight of a stake transaction. + /// This is the weight that will be used to calculate + /// how many transactions can fit inside one block + pub fn weight(&self) -> u32 { + self.body.weight() + } +} + +#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash)] +#[protobuf_convert(pb = "witnet::StakeTransactionBody")] +pub struct StakeTransactionBody { + pub inputs: Vec, + pub output: StakeOutput, + pub change: Option, + + #[protobuf_convert(skip)] + #[serde(skip)] + hash: MemoHash, +} + +impl StakeTransactionBody { + /// Creates a new stake transaction body. + pub fn new(inputs: Vec, output: StakeOutput, change: Option) -> Self { + StakeTransactionBody { + inputs, + output, + change, + hash: MemoHash::new(), + } + } + + /// Stake transaction weight. It is calculated as: + /// + /// ```text + /// ST_weight = N*INPUT_SIZE+M*OUTPUT_SIZE+STAKE_OUTPUT + /// + /// ``` + pub fn weight(&self) -> u32 { + let inputs_len = u32::try_from(self.inputs.len()).unwrap_or(u32::MAX); + let inputs_weight = inputs_len.saturating_mul(INPUT_SIZE); + let change_weight = if self.change.is_some() { OUTPUT_SIZE } else { 0 }; + + inputs_weight.saturating_add(change_weight).saturating_add(STAKE_OUTPUT_SIZE) + } +} + +#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash)] +#[protobuf_convert(pb = "witnet::StakeOutput")] +pub struct StakeOutput { + pub value: u64, + pub authorization: KeyedSignature, +} + +impl StakeOutput { + pub fn new(value: u64, authorization: KeyedSignature) -> Self { + StakeOutput { + value, + authorization, + } + } +} + impl MemoizedHashable for VTTransactionBody { fn hashable_bytes(&self) -> Vec { self.to_pb_bytes().unwrap() @@ -722,6 +804,15 @@ impl MemoizedHashable for RevealTransactionBody { &self.hash } } +impl MemoizedHashable for StakeTransactionBody { + fn hashable_bytes(&self) -> Vec { + self.to_pb_bytes().unwrap() + } + + fn memoized_hash(&self) -> &MemoHash { + &self.hash + } +} impl MemoizedHashable for TallyTransaction { fn hashable_bytes(&self) -> Vec { let Hash::SHA256(data_bytes) = self.data_poi_hash(); @@ -765,6 +856,12 @@ impl Hashable for RevealTransaction { } } +impl Hashable for StakeTransaction { + fn hash(&self) -> Hash { + self.body.hash() + } +} + impl Hashable for Transaction { fn hash(&self) -> Hash { match self { @@ -774,6 +871,7 @@ impl Hashable for Transaction { Transaction::Reveal(tx) => tx.hash(), Transaction::Tally(tx) => tx.hash(), Transaction::Mint(tx) => tx.hash(), + Transaction::Stake(tx) => tx.hash(), } } } diff --git a/data_structures/src/transaction_factory.rs b/data_structures/src/transaction_factory.rs index c121c6f9a..57687f3cb 100644 --- a/data_structures/src/transaction_factory.rs +++ b/data_structures/src/transaction_factory.rs @@ -9,7 +9,7 @@ use crate::{ }, error::TransactionError, fee::{AbsoluteFee, Fee}, - transaction::{DRTransactionBody, VTTransactionBody, INPUT_SIZE}, + transaction::{DRTransactionBody, StakeTransactionBody, VTTransactionBody, INPUT_SIZE}, utxo_pool::{ NodeUtxos, NodeUtxosRef, OwnUnspentOutputsPool, UnspentOutputsPool, UtxoDiff, UtxoSelectionStrategy, @@ -537,6 +537,9 @@ pub fn transaction_outputs_sum(outputs: &[ValueTransferOutput]) -> Result Result { + !unimplemented!() +} #[cfg(test)] mod tests { use std::{ diff --git a/data_structures/src/types.rs b/data_structures/src/types.rs index fa6b0cd1b..04370407e 100644 --- a/data_structures/src/types.rs +++ b/data_structures/src/types.rs @@ -75,6 +75,7 @@ impl fmt::Display for Command { Transaction::Reveal(_) => f.write_str("REVEAL_TRANSACTION")?, Transaction::Tally(_) => f.write_str("TALLY_TRANSACTION")?, Transaction::Mint(_) => f.write_str("MINT_TRANSACTION")?, + Transaction::Stake(_) => f.write_str("STAKE_TRANSACTION")?, } write!(f, ": {}", tx.hash()) } diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 231bdfb9c..e57632750 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -839,6 +839,8 @@ pub fn build_block( let mut value_transfer_txns = Vec::new(); let mut data_request_txns = Vec::new(); let mut tally_txns = Vec::new(); + // TODO: handle stake tx + let stake_txns = Vec::new(); let min_vt_weight = VTTransactionBody::new(vec![Input::default()], vec![ValueTransferOutput::default()]) @@ -1000,6 +1002,7 @@ pub fn build_block( let commit_hash_merkle_root = merkle_tree_root(&commit_txns); let reveal_hash_merkle_root = merkle_tree_root(&reveal_txns); let tally_hash_merkle_root = merkle_tree_root(&tally_txns); + let stake_hash_merkle_root = merkle_tree_root(&stake_txns); let merkle_roots = BlockMerkleRoots { mint_hash: mint.hash(), vt_hash_merkle_root, @@ -1007,6 +1010,7 @@ pub fn build_block( commit_hash_merkle_root, reveal_hash_merkle_root, tally_hash_merkle_root, + stake_hash_merkle_root, }; let block_header = BlockHeader { @@ -1024,6 +1028,7 @@ pub fn build_block( commit_txns, reveal_txns, tally_txns, + stake_txns, }; (block_header, txns) diff --git a/schemas/witnet/witnet.proto b/schemas/witnet/witnet.proto index 64b1b04e0..11883f872 100644 --- a/schemas/witnet/witnet.proto +++ b/schemas/witnet/witnet.proto @@ -59,6 +59,7 @@ message Block { Hash commit_hash_merkle_root = 4; Hash reveal_hash_merkle_root = 5; Hash tally_hash_merkle_root = 6; + Hash stake_hash_merkle_root = 7; } uint32 signals = 1; CheckpointBeacon beacon = 2; @@ -73,6 +74,7 @@ message Block { repeated CommitTransaction commit_txns = 4; repeated RevealTransaction reveal_txns = 5; repeated TallyTransaction tally_txns = 6; + repeated StakeTransaction stake_txns = 7; } BlockHeader block_header = 1; @@ -229,6 +231,22 @@ message MintTransaction { repeated ValueTransferOutput outputs = 2; } +message StakeOutput { + uint64 value = 1; + KeyedSignature authorization = 2; +} + +message StakeTransactionBody { + repeated Input inputs = 1; + StakeOutput output = 2; + ValueTransferOutput change = 3; +} + +message StakeTransaction { + StakeTransactionBody body = 1 ; + repeated KeyedSignature signatures = 2; +} + message Transaction { oneof kind { VTTransaction ValueTransfer = 1; @@ -237,6 +255,7 @@ message Transaction { RevealTransaction Reveal = 4; TallyTransaction Tally = 5; MintTransaction Mint = 6; + StakeTransaction Stake = 7; } } diff --git a/validations/src/tests/mod.rs b/validations/src/tests/mod.rs index b14bb19c3..9fd248f44 100644 --- a/validations/src/tests/mod.rs +++ b/validations/src/tests/mod.rs @@ -47,6 +47,9 @@ mod witnessing; static ONE_WIT: u64 = 1_000_000_000; const MAX_VT_WEIGHT: u32 = 20_000; const MAX_DR_WEIGHT: u32 = 80_000; +const MAX_STAKE_BLOCK_WEIGHT: u32 = 10_000_000; +const MIN_STAKE_NANOWITS: u64 = 10_000_000_000_000; + const REQUIRED_REWARD_COLLATERAL_RATIO: u64 = PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; const INITIAL_BLOCK_REWARD: u64 = 250 * 1_000_000_000; @@ -433,7 +436,7 @@ fn vtt_no_inputs_zero_output() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); - // Try to create a data request with no inputs + // Try to create a value transfer with no inputs let pkh = PublicKeyHash::default(); let vto0 = ValueTransferOutput { pkh, @@ -8448,6 +8451,114 @@ fn tally_error_encode_reveal_wip() { x.unwrap(); } +#[test] +fn st_no_inputs() { + let utxo_set = UnspentOutputsPool::default(); + let block_number = 0; + let utxo_diff = UtxoDiff::new(&utxo_set, block_number); + + // Try to create an stake tx with no inputs + let st_output = StakeOutput { + value: MIN_STAKE_NANOWITS + 1, + authorization: KeyedSignature::default(), + }; + // let vto0 = ValueTransferOutput { + // pkh, + // value: 1000, + // time_lock: 0, + // }; + let st_body = StakeTransactionBody::new( + Vec::new(), + st_output, + None, + ); + let st_tx = StakeTransaction::new(st_body, vec![]); + // let vt_body = VTTransactionBody::new(vec![], vec![vto0]); + // let vt_tx = VTTransaction::new(vt_body, vec![]); + let x = validate_stake_transaction( + &st_tx, + &utxo_diff, + Epoch::default(), + EpochConstants::default(), + &mut vec![], + ); + assert_eq!( + x.unwrap_err().downcast::().unwrap(), + TransactionError::NoInputs { + tx_hash: st_tx.hash(), + } + ); +} + +#[test] +fn st_one_input_but_no_signature() { + let mut signatures_to_verify = vec![]; + let utxo_set = UnspentOutputsPool::default(); + let block_number = 0; + let utxo_diff = UtxoDiff::new(&utxo_set, block_number); + let vti = Input::new( + "2222222222222222222222222222222222222222222222222222222222222222:0" + .parse() + .unwrap(), + ); + + // No signatures but 1 input + let stake_output = StakeOutput { + authorization: KeyedSignature::default(), + value: MIN_STAKE_NANOWITS + 1, + }; + + let stake_tx_body = StakeTransactionBody::new(vec![vti], stake_output, None); + let stake_tx = StakeTransaction::new(stake_tx_body, vec![]); + let x = validate_stake_transaction( + &stake_tx, + &utxo_diff, + Epoch::default(), + EpochConstants::default(), + &mut signatures_to_verify, + ); + assert_eq!( + x.unwrap_err().downcast::().unwrap(), + TransactionError::MismatchingSignaturesNumber { + signatures_n: 0, + inputs_n: 1, + } + ); +} + +#[test] +fn st_min_stake_not_reach() { + let mut signatures_to_verify = vec![]; + let utxo_set = UnspentOutputsPool::default(); + let block_number = 0; + let utxo_diff = UtxoDiff::new(&utxo_set, block_number); + let vti = Input::new( + "2222222222222222222222222222222222222222222222222222222222222222:0" + .parse() + .unwrap(), + ); + + // No signatures but 1 input + let stake_output = StakeOutput { + authorization: KeyedSignature::default(), + value: 1, + }; + + let stake_tx_body = StakeTransactionBody::new(vec![vti], stake_output, None); + let stake_tx = StakeTransaction::new(stake_tx_body, vec![]); + let x = validate_stake_transaction( + &stake_tx, + &utxo_diff, + Epoch::default(), + EpochConstants::default(), + &mut signatures_to_verify, + ); + assert_eq!( + x.unwrap_err().downcast::().unwrap(), + TransactionError::MinStakeNotReached { min_stake: MIN_STAKE_NANOWITS, stake: 1 } + ); +} + static LAST_VRF_INPUT: &str = "4da71b67e7e50ae4ad06a71e505244f8b490da55fc58c50386c908f7146d2239"; #[test] @@ -10858,7 +10969,12 @@ fn validate_vt_weight_valid() { DEFAULT_INPUT_VALUE - 2 * 10, ) }; - let x = test_blocks_with_limits(vec![t0], 2 * 493, 0, GENESIS_BLOCK_HASH.parse().unwrap()); + let x = test_blocks_with_limits( + vec![t0], + 2 * 493, + 0, + GENESIS_BLOCK_HASH.parse().unwrap(), + ); x.unwrap(); } @@ -10997,7 +11113,12 @@ fn validate_dr_weight_valid() { DEFAULT_INPUT_VALUE - 2 * dr_value, ) }; - let x = test_blocks_with_limits(vec![t0], 0, 2 * 1605, GENESIS_BLOCK_HASH.parse().unwrap()); + let x = test_blocks_with_limits( + vec![t0], + 0, + 2 * 1605, + GENESIS_BLOCK_HASH.parse().unwrap(), + ); x.unwrap(); } diff --git a/validations/src/validations.rs b/validations/src/validations.rs index cc7c90ca3..8d02bfa5e 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -30,8 +30,8 @@ use witnet_data_structures::{ error::{BlockError, DataRequestError, TransactionError}, radon_report::{RadonReport, ReportContext}, transaction::{ - CommitTransaction, DRTransaction, MintTransaction, RevealTransaction, TallyTransaction, - Transaction, VTTransaction, + CommitTransaction, DRTransaction, MintTransaction, RevealTransaction, StakeOutput, + StakeTransaction, TallyTransaction, Transaction, VTTransaction, }, transaction_factory::{transaction_inputs_sum, transaction_outputs_sum}, types::visitor::Visitor, @@ -49,6 +49,10 @@ use witnet_rad::{ types::{serial_iter_decode, RadonTypes}, }; +// TODO: move to a configuration +const MAX_STAKE_BLOCK_WEIGHT: u32 = 10_000_000; +const MIN_STAKE_NANOWITS: u64 = 10_000_000_000_000; + /// Returns the fee of a value transfer transaction. /// /// The fee is the difference between the outputs and the inputs @@ -95,6 +99,28 @@ pub fn dr_transaction_fee( } } +/// Returns the fee of a stake transaction. +/// +/// The fee is the difference between the output and the inputs +/// of the transaction. The pool parameter is used to find the +/// outputs pointed by the inputs and that contain the actual +/// their value. +pub fn st_transaction_fee( + st_tx: &StakeTransaction, + utxo_diff: &UtxoDiff<'_>, + epoch: Epoch, + epoch_constants: EpochConstants, +) -> Result { + let in_value = transaction_inputs_sum(&st_tx.body.inputs, utxo_diff, epoch, epoch_constants)?; + let out_value = &st_tx.body.output.value; + + if out_value > &in_value { + Err(TransactionError::NegativeFee.into()) + } else { + Ok(in_value - out_value) + } +} + /// Returns the fee of a data request transaction. /// /// The fee is the difference between the outputs (with the data request value) @@ -339,8 +365,6 @@ pub fn validate_vt_transaction<'a>( let fee = vt_transaction_fee(vt_tx, utxo_diff, epoch, epoch_constants)?; - // FIXME(#514): Implement value transfer transaction validation - Ok(( vt_tx.body.inputs.iter().collect(), vt_tx.body.outputs.iter().collect(), @@ -1089,6 +1113,50 @@ pub fn validate_tally_transaction<'a>( Ok((ta_tx.outputs.iter().collect(), tally_extra_fee)) } +/// Function to validate a stake transaction +pub fn validate_stake_transaction<'a>( + st_tx: &'a StakeTransaction, + utxo_diff: &UtxoDiff<'_>, + epoch: Epoch, + epoch_constants: EpochConstants, + signatures_to_verify: &mut Vec, +) -> Result<(Vec<&'a Input>, &'a StakeOutput, u64, u32, &'a Option), failure::Error> { + // Check that the stake is greater than the min allowed + if st_tx.body.output.value < MIN_STAKE_NANOWITS { + return Err(TransactionError::MinStakeNotReached { + min_stake: MIN_STAKE_NANOWITS, + stake: st_tx.body.output.value, + } + .into()); + } + + validate_transaction_signature( + &st_tx.signatures, + &st_tx.body.inputs, + st_tx.hash(), + utxo_diff, + signatures_to_verify, + )?; + + // A stake transaction must have at least one input + if st_tx.body.inputs.is_empty() { + return Err(TransactionError::NoInputs { + tx_hash: st_tx.hash(), + } + .into()); + } + + let fee = st_transaction_fee(st_tx, utxo_diff, epoch, epoch_constants)?; + + Ok(( + st_tx.body.inputs.iter().collect(), + &st_tx.body.output, + fee, + st_tx.weight(), + &st_tx.body.change, + )) +} + /// Function to validate a block signature pub fn validate_block_signature( block: &Block, @@ -1677,6 +1745,71 @@ pub fn validate_block_transactions( ); } + // validate stake transactions in a block + let mut st_mt = ProgressiveMerkleTree::sha256(); + let mut st_weight: u32 = 0; + + // Check if the block contains more than one stake tx from the same operator + for i in 1..block.txns.stake_txns.len() { + let found = block.txns.stake_txns[i..].iter().find(|stake_tx| { + let stake_tx_aux = &block.txns.stake_txns[i-1]; + + stake_tx.body.output.authorization.public_key == stake_tx_aux.body.output.authorization.public_key + }); + + // TODO: refactor + if found.is_some() { + return Err(BlockError::RepeatedStakeOperator { pkh: found.unwrap().body.output.authorization.public_key.pkh().hash() }.into()); + } + } + + for transaction in &block.txns.stake_txns { + let (inputs, _output, fee, weight, change) = validate_stake_transaction( + transaction, + &utxo_diff, + epoch, + epoch_constants, + signatures_to_verify, + )?; + + total_fee += fee; + + // Update st weight + let acc_weight = st_weight.saturating_add(weight); + if acc_weight > MAX_STAKE_BLOCK_WEIGHT { + return Err(BlockError::TotalStakeWeightLimitExceeded { + weight: acc_weight, + max_weight: MAX_STAKE_BLOCK_WEIGHT, + } + .into()); + } + st_weight = acc_weight; + + // TODO: refactor + if change.is_some() { + let mut outputs : Vec<&ValueTransferOutput>= Vec::new(); + let val = change.clone().unwrap(); + outputs.push(&val); + update_utxo_diff(&mut utxo_diff, inputs, outputs, transaction.hash()); + } else { + update_utxo_diff(&mut utxo_diff, inputs, Vec::new(), transaction.hash()); + } + + // Add new hash to merkle tree + let txn_hash = transaction.hash(); + let Hash::SHA256(sha) = txn_hash; + st_mt.push(Sha256(sha)); + + // TODO: Move validations to a visitor + // // Execute visitor + // if let Some(visitor) = &mut visitor { + // let transaction = Transaction::ValueTransfer(transaction.clone()); + // visitor.visit(&(transaction, fee, weight)); + // } + } + + let st_hash_merkle_root = st_mt.root(); + // Validate Merkle Root let merkle_roots = BlockMerkleRoots { mint_hash: block.txns.mint.hash(), @@ -1685,6 +1818,7 @@ pub fn validate_block_transactions( commit_hash_merkle_root: Hash::from(co_hash_merkle_root), reveal_hash_merkle_root: Hash::from(re_hash_merkle_root), tally_hash_merkle_root: Hash::from(ta_hash_merkle_root), + stake_hash_merkle_root: Hash::from(st_hash_merkle_root), }; if merkle_roots != block.block_header.merkle_roots { @@ -1851,6 +1985,14 @@ pub fn validate_new_transaction( Transaction::Reveal(tx) => { validate_reveal_transaction(tx, data_request_pool, signatures_to_verify) } + Transaction::Stake(tx) => validate_stake_transaction( + tx, + &utxo_diff, + current_epoch, + epoch_constants, + signatures_to_verify, + ) + .map(|(_, _, fee, _, _)| fee), _ => Err(TransactionError::NotValidTransaction.into()), } } @@ -2123,6 +2265,7 @@ pub fn validate_merkle_tree(block: &Block) -> bool { commit_hash_merkle_root: merkle_tree_root(&block.txns.commit_txns), reveal_hash_merkle_root: merkle_tree_root(&block.txns.reveal_txns), tally_hash_merkle_root: merkle_tree_root(&block.txns.tally_txns), + stake_hash_merkle_root: merkle_tree_root(&block.txns.stake_txns), }; merkle_roots == block.block_header.merkle_roots diff --git a/wallet/src/model.rs b/wallet/src/model.rs index ec8ac954b..35ef8bdba 100644 --- a/wallet/src/model.rs +++ b/wallet/src/model.rs @@ -187,6 +187,8 @@ pub enum TransactionData { Mint(MintData), #[serde(rename = "commit")] Commit(VtData), + // #[serde(rename = "stake")] + // Stake(StakeData), } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] diff --git a/wallet/src/repository/wallet/mod.rs b/wallet/src/repository/wallet/mod.rs index 6e401c586..215989653 100644 --- a/wallet/src/repository/wallet/mod.rs +++ b/wallet/src/repository/wallet/mod.rs @@ -1490,6 +1490,7 @@ where Transaction::Reveal(_) => None, Transaction::Tally(_) => None, Transaction::Mint(_) => None, + Transaction::Stake(tx) => Some(&tx.body.inputs), }; let empty_hashset = HashSet::default(); diff --git a/wallet/src/types.rs b/wallet/src/types.rs index 84c9a9513..63924f646 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -22,7 +22,7 @@ use witnet_data_structures::{ fee::Fee, transaction::{ CommitTransaction, DRTransaction, DRTransactionBody, MintTransaction, RevealTransaction, - TallyTransaction, Transaction, VTTransaction, VTTransactionBody, + StakeTransaction, TallyTransaction, Transaction, VTTransaction, VTTransactionBody, }, utxo_pool::UtxoSelectionStrategy, }; @@ -322,6 +322,7 @@ pub enum TransactionHelper { Reveal(RevealTransaction), Tally(TallyTransaction), Mint(MintTransaction), + Stake(StakeTransaction), } impl From for TransactionHelper { @@ -337,6 +338,7 @@ impl From for TransactionHelper { Transaction::Reveal(revealtransaction) => TransactionHelper::Reveal(revealtransaction), Transaction::Tally(tallytransaction) => TransactionHelper::Tally(tallytransaction), Transaction::Mint(minttransaction) => TransactionHelper::Mint(minttransaction), + Transaction::Stake(staketransaction) => TransactionHelper::Stake(staketransaction), } } } @@ -354,6 +356,7 @@ impl From for Transaction { TransactionHelper::Reveal(revealtransaction) => Transaction::Reveal(revealtransaction), TransactionHelper::Tally(tallytransaction) => Transaction::Tally(tallytransaction), TransactionHelper::Mint(minttransaction) => Transaction::Mint(minttransaction), + TransactionHelper::Stake(staketransaction) => Transaction::Stake(staketransaction), } } }