From 2de204c2ea56d1080d353d78cf91dc3ff8d15f03 Mon Sep 17 00:00:00 2001 From: tommytrg Date: Mon, 9 Oct 2023 18:29:38 +0200 Subject: [PATCH 1/4] feat: add StakeTransaction (split commit) --- data_structures/src/chain/mod.rs | 11 +- data_structures/src/error.rs | 18 +++ data_structures/src/superblock.rs | 3 + data_structures/src/transaction.rs | 108 +++++++++++++ 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 | 112 +++++++++++++- validations/src/validations.rs | 171 ++++++++++++++++++++- wallet/src/model.rs | 2 + wallet/src/repository/wallet/mod.rs | 1 + wallet/src/types.rs | 5 +- 13 files changed, 453 insertions(+), 8 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index a6a1de429..6c5567548 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..a19457cd1 100644 --- a/data_structures/src/error.rs +++ b/data_structures/src/error.rs @@ -278,6 +278,15 @@ 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 +410,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 7e3d30ad3..8cd978d0f 100644 --- a/data_structures/src/superblock.rs +++ b/data_structures/src/superblock.rs @@ -806,6 +806,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, @@ -855,6 +856,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, @@ -870,6 +872,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..d9974ffc4 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,90 @@ 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 +814,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 +866,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 +881,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..18eb94f54 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,113 @@ 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] diff --git a/validations/src/validations.rs b/validations/src/validations.rs index cc7c90ca3..fab04907f 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,59 @@ 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 +1754,82 @@ 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 +1838,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 +2005,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 +2285,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), } } } From f6da31394c8f9aadedc16fce3bb3a7fe94e1ff53 Mon Sep 17 00:00:00 2001 From: tommytrg Date: Wed, 18 Oct 2023 19:09:19 +0200 Subject: [PATCH 2/4] missing logic --- data_structures/src/chain/mod.rs | 192 ++++++++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 5 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index 6c5567548..1888c7afc 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -507,8 +507,16 @@ impl Block { vt_weight } + pub fn st_weight(&self) -> u32 { + let mut st_weight = 0; + for st_txn in self.txns.stake_txns.iter() { + st_weight += st_txn.weight(); + } + st_weight + } + pub fn weight(&self) -> u32 { - self.dr_weight() + self.vt_weight() + self.dr_weight() + self.vt_weight() + self.st_weight() } } @@ -522,6 +530,7 @@ impl BlockTransactions { + self.commit_txns.len() + self.reveal_txns.len() + self.tally_txns.len() + + self.stake_txns.len() } /// Returns true if this block contains no transactions @@ -533,6 +542,7 @@ impl BlockTransactions { && self.commit_txns.is_empty() && self.reveal_txns.is_empty() && self.tally_txns.is_empty() + && self.stake_txns.is_empty() } /// Get a transaction given the `TransactionPointer` @@ -564,6 +574,11 @@ impl BlockTransactions { .get(i as usize) .cloned() .map(Transaction::Tally), + TransactionPointer::Stake(i) => self + .stake_txns + .get(i as usize) + .cloned() + .map(Transaction::Stake), } } @@ -606,6 +621,11 @@ impl BlockTransactions { TransactionPointer::Tally(u32::try_from(i).unwrap()); items_to_add.push((tx.hash(), pointer_to_block.clone())); } + for (i, tx) in self.stake_txns.iter().enumerate() { + pointer_to_block.transaction_index = + TransactionPointer::Stake(u32::try_from(i).unwrap()); + items_to_add.push((tx.hash(), pointer_to_block.clone())); + } items_to_add } @@ -1955,6 +1975,7 @@ impl From for RADTally { type PrioritizedHash = (OrderedFloat, Hash); type PrioritizedVTTransaction = (OrderedFloat, VTTransaction); type PrioritizedDRTransaction = (OrderedFloat, DRTransaction); +type PrioritizedStakeTransaction = (OrderedFloat, StakeTransaction); #[derive(Debug, Clone, Default)] struct UnconfirmedTransactions { @@ -2011,6 +2032,8 @@ pub struct TransactionsPool { total_vt_weight: u64, // Total size of all data request transactions inside the pool in weight units total_dr_weight: u64, + // Total size of all stake transactions inside the pool in weight units + total_st_weight: u64, // TransactionsPool size limit in weight units weight_limit: u64, // Ratio of value transfer transaction to data request transaction that should be in the @@ -2031,6 +2054,11 @@ pub struct TransactionsPool { required_reward_collateral_ratio: u64, // Map for unconfirmed transactions unconfirmed_transactions: UnconfirmedTransactions, + st_transactions: HashMap, + sorted_st_index: BTreeSet, + // Minimum fee required to include a Stake Transaction into a block. We check for this fee in the + // TransactionPool so we can choose not to insert a transaction we will not mine anyway. + minimum_st_fee: u64, } impl Default for TransactionsPool { @@ -2047,17 +2075,22 @@ impl Default for TransactionsPool { output_pointer_map: Default::default(), total_vt_weight: 0, total_dr_weight: 0, + total_st_weight: 0, // Unlimited by default weight_limit: u64::MAX, // Try to keep the same amount of value transfer weight and data request weight vt_to_dr_factor: 1.0, // Default is to include all transactions into the pool and blocks minimum_vtt_fee: 0, + // Default is to include all transactions into the pool and blocks + minimum_st_fee: 0, // Collateral minimum from consensus constants collateral_minimum: 0, // Required minimum reward to collateral percentage is defined as a consensus constant required_reward_collateral_ratio: u64::MAX, unconfirmed_transactions: Default::default(), + st_transactions: Default::default(), + sorted_st_index: Default::default(), } } } @@ -2090,7 +2123,7 @@ impl TransactionsPool { ) -> Vec { self.weight_limit = weight_limit; self.vt_to_dr_factor = vt_to_dr_factor; - + // TODO: take into account stake tx self.remove_transactions_for_size_limit() } @@ -2129,6 +2162,7 @@ impl TransactionsPool { && self.dr_transactions.is_empty() && self.co_transactions.is_empty() && self.re_transactions.is_empty() + && self.st_transactions.is_empty() } /// Remove all the transactions but keep the allocated memory for reuse. @@ -2145,12 +2179,16 @@ impl TransactionsPool { output_pointer_map, total_vt_weight, total_dr_weight, + total_st_weight, weight_limit: _, vt_to_dr_factor: _, minimum_vtt_fee: _, + minimum_st_fee: _, collateral_minimum: _, required_reward_collateral_ratio: _, unconfirmed_transactions, + st_transactions, + sorted_st_index, } = self; vt_transactions.clear(); @@ -2164,7 +2202,10 @@ impl TransactionsPool { output_pointer_map.clear(); *total_vt_weight = 0; *total_dr_weight = 0; + *total_st_weight = 0; unconfirmed_transactions.clear(); + st_transactions.clear(); + sorted_st_index.clear(); } /// Returns the number of value transfer transactions in the pool. @@ -2209,6 +2250,27 @@ impl TransactionsPool { self.dr_transactions.len() } + /// Returns the number of stake transactions in the pool. + /// + /// # Examples: + /// + /// ``` + /// # use witnet_data_structures::chain::{TransactionsPool, Hash}; + /// # use witnet_data_structures::transaction::{Transaction, StakeTransaction}; + /// let mut pool = TransactionsPool::new(); + /// + /// let transaction = Transaction::Stake(StakeTransaction::default()); + /// + /// assert_eq!(pool.st_len(), 0); + /// + /// pool.insert(transaction, 0); + /// + /// assert_eq!(pool.st_len(), 1); + /// ``` + pub fn st_len(&self) -> usize { + self.st_transactions.len() + } + /// Clear commit transactions in TransactionsPool pub fn clear_commits(&mut self) { self.co_transactions.clear(); @@ -2250,7 +2312,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"), + Transaction::Stake(_mt) => Ok(self.st_contains(&tx_hash)), } } @@ -2343,6 +2405,30 @@ impl TransactionsPool { .unwrap_or(Ok(false)) } + /// Returns `true` if the pool contains a stake + /// transaction for the specified hash. + /// + /// The `key` may be any borrowed form of the hash, but `Hash` and + /// `Eq` on the borrowed form must match those for the key type. + /// + /// # Examples: + /// ``` + /// # use witnet_data_structures::chain::{TransactionsPool, Hash, Hashable}; + /// # use witnet_data_structures::transaction::{Transaction, StakeTransaction}; + /// let mut pool = TransactionsPool::new(); + /// + /// let transaction = Transaction::Stake(StakeTransaction::default()); + /// let hash = transaction.hash(); + /// assert!(!pool.st_contains(&hash)); + /// + /// pool.insert(transaction, 0); + /// + /// assert!(pool.t_contains(&hash)); + /// ``` + pub fn st_contains(&self, key: &Hash) -> bool { + self.st_transactions.contains_key(key) + } + /// Remove a value transfer transaction from the pool and make sure that other transactions /// that may try to spend the same UTXOs are also removed. /// This should be used to remove transactions that got included in a consolidated block. @@ -2552,6 +2638,59 @@ impl TransactionsPool { (commits_vector, total_fee, dr_pointer_vec) } + /// Remove a stake transaction from the pool and make sure that other transactions + /// that may try to spend the same UTXOs are also removed. + /// This should be used to remove transactions that got included in a consolidated block. + /// + /// Returns an `Option` with the value transfer transaction for the specified hash or `None` if not exist. + /// + /// The `key` may be any borrowed form of the hash, but `Hash` and + /// `Eq` on the borrowed form must match those for the key type. + /// + /// # Examples: + /// ``` + /// # use witnet_data_structures::chain::{TransactionsPool, Hash, Hashable}; + /// # use witnet_data_structures::transaction::{Transaction, StakeTransaction}; + /// let mut pool = TransactionsPool::new(); + /// let vt_transaction = StakeTransaction::default(); + /// let transaction = Transaction::Stake(st_transaction.clone()); + /// pool.insert(transaction.clone(),0); + /// + /// assert!(pool.st_contains(&transaction.hash())); + /// + /// let op_transaction_removed = pool.st_remove(&st_transaction); + /// + /// assert_eq!(Some(st_transaction), op_transaction_removed); + /// assert!(!pool.st_contains(&transaction.hash())); + /// ``` + pub fn st_remove(&mut self, tx: &StakeTransaction) -> Option { + let key = tx.hash(); + let transaction = self.st_remove_inner(&key, true); + + self.remove_inputs(&tx.body.inputs); + + transaction + } + + /// Remove a stake transaction from the pool but do not remove other transactions that + /// may try to spend the same UTXOs. + /// This should be used to remove transactions that did not get included in a consolidated + /// block. + /// If the transaction did get included in a consolidated block, use `st_remove` instead. + fn st_remove_inner(&mut self, key: &Hash, consolidated: bool) -> Option { + // TODO: is this taking into account the change and the stake output? + self.st_transactions + .remove(key) + .map(|(weight, transaction)| { + self.sorted_st_index.remove(&(weight, *key)); + self.total_st_weight -= u64::from(transaction.weight()); + if !consolidated { + self.remove_tx_from_output_pointer_map(key, &transaction.body.inputs); + } + transaction + }) + } + /// Returns a tuple with a vector of reveal transactions and the value /// of all the fees obtained with those reveals pub fn get_reveals(&self, dr_pool: &DataRequestPool) -> (Vec<&RevealTransaction>, u64) { @@ -2618,11 +2757,13 @@ impl TransactionsPool { /// Returns a list of all the removed transactions. fn remove_transactions_for_size_limit(&mut self) -> Vec { let mut removed_transactions = vec![]; - - while self.total_vt_weight + self.total_dr_weight > self.weight_limit { + // TODO: Don't we have a method that make the following sum?? + while self.total_vt_weight + self.total_dr_weight + self.total_st_weight > self.weight_limit + { // Try to split the memory between value transfer and data requests using the same // ratio as the one used in blocks // The ratio of vt to dr in blocks is currently 4:1 + // TODO: What the criteria to delete st? #[allow(clippy::cast_precision_loss)] let more_vtts_than_drs = self.total_vt_weight as f64 >= self.total_dr_weight as f64 * self.vt_to_dr_factor; @@ -2745,6 +2886,26 @@ impl TransactionsPool { .or_default() .insert(pkh, tx_hash); } + Transaction::Stake(st_tx) => { + let weight = f64::from(st_tx.weight()); + let priority = OrderedFloat(fee as f64 / weight); + + if fee < self.minimum_st_fee { + return vec![Transaction::Stake(st_tx)]; + } else { + self.total_st_weight += u64::from(st_tx.weight()); + + for input in &st_tx.body.inputs { + self.output_pointer_map + .entry(input.output_pointer) + .or_insert_with(Vec::new) + .push(st_tx.hash()); + } + + self.st_transactions.insert(key, (priority, st_tx)); + self.sorted_st_index.insert((priority, key)); + } + } tx => { panic!( "Transaction kind not supported by TransactionsPool: {:?}", @@ -2793,6 +2954,15 @@ impl TransactionsPool { .filter_map(move |(_, h)| self.dr_transactions.get(h).map(|(_, t)| t)) } + /// An iterator visiting all the stake transactions + /// in the pool + pub fn st_iter(&self) -> impl Iterator { + self.sorted_st_index + .iter() + .rev() + .filter_map(move |(_, h)| self.st_transactions.get(h).map(|(_, t)| t)) + } + /// Returns a reference to the value corresponding to the key. /// /// Examples: @@ -2811,6 +2981,7 @@ impl TransactionsPool { /// /// assert!(pool.vt_get(&hash).is_some()); /// ``` + // TODO: dead code pub fn vt_get(&self, key: &Hash) -> Option<&VTTransaction> { self.vt_transactions .get(key) @@ -2844,6 +3015,7 @@ impl TransactionsPool { /// pool.vt_retain(|tx| tx.body.outputs.len()>0); /// assert_eq!(pool.vt_len(), 1); /// ``` + // TODO: dead code pub fn vt_retain(&mut self, mut f: F) where F: FnMut(&VTTransaction) -> bool, @@ -2885,6 +3057,11 @@ impl TransactionsPool { self.re_hash_index .get(hash) .map(|rt| Transaction::Reveal(rt.clone())) + .or_else(|| { + self.st_transactions + .get(hash) + .map(|(_, st)| Transaction::Stake(st.clone())) + }) }) } @@ -2910,6 +3087,9 @@ impl TransactionsPool { Transaction::DataRequest(_) => { let _x = self.dr_remove_inner(&hash, false); } + Transaction::Stake(_) => { + let _x = self.st_remove_inner(&hash, false); + } _ => continue, } @@ -3016,6 +3196,8 @@ pub enum TransactionPointer { Tally(u32), /// Mint Mint, + // Stake + Stake(u32), } /// This is how transactions are stored in the database: hash of the containing block, plus index From a21c0a8830ab0bd3f85fff1bc496369344cdc9b6 Mon Sep 17 00:00:00 2001 From: tommytrg Date: Thu, 19 Oct 2023 11:01:02 +0200 Subject: [PATCH 3/4] PR comments --- Cargo.lock | 19 ++++-- data_structures/src/error.rs | 22 ++++--- data_structures/src/transaction.rs | 19 +----- data_structures/src/transaction_factory.rs | 3 + schemas/witnet/witnet.proto | 2 +- validations/Cargo.toml | 2 +- validations/src/tests/mod.rs | 14 ++--- validations/src/validations.rs | 68 +++++++++------------- 8 files changed, 67 insertions(+), 82 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f0d09be9..93ea7786f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1707,6 +1707,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -4967,7 +4976,7 @@ dependencies = [ "failure", "futures 0.3.28", "hex", - "itertools", + "itertools 0.8.2", "lazy_static", "log 0.4.19", "num-format", @@ -5083,7 +5092,7 @@ dependencies = [ "failure", "futures 0.3.28", "hex", - "itertools", + "itertools 0.8.2", "lazy_static", "log 0.4.19", "num-traits", @@ -5149,7 +5158,7 @@ dependencies = [ "futures-util", "glob", "hex", - "itertools", + "itertools 0.8.2", "jsonrpc-core 18.0.0", "jsonrpc-pubsub 18.0.0", "log 0.4.19", @@ -5272,7 +5281,7 @@ dependencies = [ "bencher", "failure", "hex", - "itertools", + "itertools 0.11.0", "log 0.4.19", "url", "witnet_config", @@ -5296,7 +5305,7 @@ dependencies = [ "futures 0.3.28", "futures-util", "hex", - "itertools", + "itertools 0.8.2", "jsonrpc-core 15.1.0", "jsonrpc-pubsub 15.1.0", "log 0.4.19", diff --git a/data_structures/src/error.rs b/data_structures/src/error.rs index a19457cd1..fca6ee194 100644 --- a/data_structures/src/error.rs +++ b/data_structures/src/error.rs @@ -278,14 +278,17 @@ pub enum TransactionError { max_weight: u32, dr_output: Box, }, - /// Stake weight limit exceeded + /// Stake amount below minimum #[fail( - display = "Stake ({}) doesn't reach the minimum amount ({})", + display = "The amount of coins in stake ({}) is less than the minimum allowed ({})", 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)] + StakeBelowMinimum { min_stake: u64, stake: u64 }, + /// A stake output with zero value does not make sense + #[fail( + display = "Transaction {} contains a stake output with zero value", + 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 {}", @@ -410,15 +413,18 @@ pub enum BlockError { weight, max_weight )] TotalDataRequestWeightLimitExceeded { weight: u32, max_weight: u32 }, - /// Stake weight limit exceeded + /// Stake weight limit exceeded by a block candidate #[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 }, + #[fail( + display = "A single operator is receiving stake more than once in a block: ({}) ", + pkh + )] + RepeatedStakeOperator { pkh: PublicKeyHash }, /// Missing expected tallies #[fail( display = "{} expected tally transactions are missing in block candidate {}", diff --git a/data_structures/src/transaction.rs b/data_structures/src/transaction.rs index d9974ffc4..5ac3054d1 100644 --- a/data_structures/src/transaction.rs +++ b/data_structures/src/transaction.rs @@ -697,6 +697,7 @@ pub struct StakeTransaction { pub body: StakeTransactionBody, pub signatures: Vec, } + impl StakeTransaction { // Creates a new stake transaction. pub fn new(body: StakeTransactionBody, signatures: Vec) -> Self { @@ -704,8 +705,8 @@ impl StakeTransaction { } /// 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 + /// 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() } @@ -724,20 +725,6 @@ pub struct StakeTransactionBody { } 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 diff --git a/data_structures/src/transaction_factory.rs b/data_structures/src/transaction_factory.rs index 57687f3cb..9b31e444e 100644 --- a/data_structures/src/transaction_factory.rs +++ b/data_structures/src/transaction_factory.rs @@ -537,9 +537,12 @@ pub fn transaction_outputs_sum(outputs: &[ValueTransferOutput]) -> Result Result { + // TODO: add stake transaction factory logic here !unimplemented!() } + #[cfg(test)] mod tests { use std::{ diff --git a/schemas/witnet/witnet.proto b/schemas/witnet/witnet.proto index 11883f872..d08283acb 100644 --- a/schemas/witnet/witnet.proto +++ b/schemas/witnet/witnet.proto @@ -239,7 +239,7 @@ message StakeOutput { message StakeTransactionBody { repeated Input inputs = 1; StakeOutput output = 2; - ValueTransferOutput change = 3; + optional ValueTransferOutput change = 3; } message StakeTransaction { diff --git a/validations/Cargo.toml b/validations/Cargo.toml index eb00b2011..6cf51b7b3 100644 --- a/validations/Cargo.toml +++ b/validations/Cargo.toml @@ -8,7 +8,7 @@ workspace = ".." [dependencies] failure = "0.1.8" -itertools = "0.8.2" +itertools = "0.11.0" log = "0.4.8" url = "2.2.2" diff --git a/validations/src/tests/mod.rs b/validations/src/tests/mod.rs index 18eb94f54..c1e27ae9a 100644 --- a/validations/src/tests/mod.rs +++ b/validations/src/tests/mod.rs @@ -8457,20 +8457,14 @@ fn st_no_inputs() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); - // Try to create an stake tx with no inputs + // Try to create a 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, @@ -8523,7 +8517,7 @@ fn st_one_input_but_no_signature() { } #[test] -fn st_min_stake_not_reach() { +fn st_below_min_stake() { let mut signatures_to_verify = vec![]; let utxo_set = UnspentOutputsPool::default(); let block_number = 0; @@ -8551,7 +8545,7 @@ fn st_min_stake_not_reach() { ); assert_eq!( x.unwrap_err().downcast::().unwrap(), - TransactionError::MinStakeNotReached { + TransactionError::StakeBelowMinimum { min_stake: MIN_STAKE_NANOWITS, stake: 1 } diff --git a/validations/src/validations.rs b/validations/src/validations.rs index fab04907f..1224bba14 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -101,10 +101,7 @@ 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. +/// The fee is the difference between the output and the inputs of the transaction. pub fn st_transaction_fee( st_tx: &StakeTransaction, utxo_diff: &UtxoDiff<'_>, @@ -112,9 +109,15 @@ pub fn st_transaction_fee( 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; + let out_value = &st_tx.body.output.value + - &st_tx + .body + .change + .clone() + .unwrap_or(Default::default()) + .value; - if out_value > &in_value { + if out_value > in_value { Err(TransactionError::NegativeFee.into()) } else { Ok(in_value - out_value) @@ -1113,7 +1116,7 @@ pub fn validate_tally_transaction<'a>( Ok((ta_tx.outputs.iter().collect(), tally_extra_fee)) } -/// Function to validate a stake transaction +/// Function to validate a stake transaction. pub fn validate_stake_transaction<'a>( st_tx: &'a StakeTransaction, utxo_diff: &UtxoDiff<'_>, @@ -1130,9 +1133,9 @@ pub fn validate_stake_transaction<'a>( ), failure::Error, > { - // Check that the stake is greater than the min allowed + // Check that the amount of coins to stake is equal or greater than the minimum allowed if st_tx.body.output.value < MIN_STAKE_NANOWITS { - return Err(TransactionError::MinStakeNotReached { + return Err(TransactionError::StakeBelowMinimum { min_stake: MIN_STAKE_NANOWITS, stake: st_tx.body.output.value, } @@ -1759,28 +1762,20 @@ pub fn validate_block_transactions( 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 - }); + let duplicate = block + .txns + .stake_txns + .clone() + .into_iter() + .map(|stake_tx| stake_tx.body.output.authorization.public_key) + .duplicates() + .next(); - // TODO: refactor - if found.is_some() { - return Err(BlockError::RepeatedStakeOperator { - pkh: found - .unwrap() - .body - .output - .authorization - .public_key - .pkh() - .hash(), - } - .into()); + if duplicate.is_some() { + return Err(BlockError::RepeatedStakeOperator { + pkh: duplicate.unwrap().pkh(), } + .into()); } for transaction in &block.txns.stake_txns { @@ -1805,20 +1800,11 @@ pub fn validate_block_transactions( } 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()); - } + let outputs = change.into_iter().collect_vec(); + update_utxo_diff(&mut utxo_diff, inputs, outputs, transaction.hash()); // Add new hash to merkle tree - let txn_hash = transaction.hash(); - let Hash::SHA256(sha) = txn_hash; - st_mt.push(Sha256(sha)); + st_mt.push(transaction.hash().into()); // TODO: Move validations to a visitor // // Execute visitor From 8d9834bf7674b462de07b44cd57b34759bf80b0b Mon Sep 17 00:00:00 2001 From: tommytrg Date: Fri, 20 Oct 2023 12:18:54 +0200 Subject: [PATCH 4/4] w --- data_structures/src/chain/mod.rs | 204 ++++++++++++++++++++++-- data_structures/src/error.rs | 34 +++- data_structures/src/superblock.rs | 3 + data_structures/src/transaction.rs | 82 +++++++++- data_structures/src/types.rs | 1 + node/src/actors/chain_manager/mining.rs | 5 + schemas/witnet/witnet.proto | 14 ++ validations/src/validations.rs | 139 +++++++++++++++- wallet/src/repository/wallet/mod.rs | 1 + wallet/src/types.rs | 10 +- 10 files changed, 478 insertions(+), 15 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index 1888c7afc..ebf568bed 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -47,7 +47,7 @@ use crate::{ transaction::{ CommitTransaction, DRTransaction, DRTransactionBody, Memoized, MintTransaction, RevealTransaction, StakeTransaction, TallyTransaction, Transaction, TxInclusionProof, - VTTransaction, + UnstakeTransaction, VTTransaction, }, transaction::{ MemoHash, MemoizedHashable, BETA, COMMIT_WEIGHT, OUTPUT_SIZE, REVEAL_WEIGHT, TALLY_WEIGHT, @@ -419,6 +419,8 @@ pub struct BlockTransactions { pub tally_txns: Vec, /// A list of signed stake transactions pub stake_txns: Vec, + /// A list of signed unstake transactions + pub unstake_txns: Vec, } impl Block { @@ -448,6 +450,7 @@ impl Block { reveal_txns: vec![], tally_txns: vec![], stake_txns: vec![], + unstake_txns: vec![], }; /// Function to calculate a merkle tree from a transaction vector @@ -473,6 +476,7 @@ impl Block { 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), + unstake_hash_merkle_root: merkle_tree_root(&txns.unstake_txns), }; Block::new( @@ -515,8 +519,16 @@ impl Block { st_weight } + pub fn ut_weight(&self) -> u32 { + let mut ut_weight = 0; + for ut_txn in self.txns.unstake_txns.iter() { + ut_weight += ut_txn.weight(); + } + ut_weight + } + pub fn weight(&self) -> u32 { - self.dr_weight() + self.vt_weight() + self.st_weight() + self.dr_weight() + self.vt_weight() + self.st_weight() + self.ut_weight() } } @@ -531,6 +543,7 @@ impl BlockTransactions { + self.reveal_txns.len() + self.tally_txns.len() + self.stake_txns.len() + + self.unstake_txns.len() } /// Returns true if this block contains no transactions @@ -543,6 +556,7 @@ impl BlockTransactions { && self.reveal_txns.is_empty() && self.tally_txns.is_empty() && self.stake_txns.is_empty() + && self.unstake_txns.is_empty() } /// Get a transaction given the `TransactionPointer` @@ -579,6 +593,11 @@ impl BlockTransactions { .get(i as usize) .cloned() .map(Transaction::Stake), + TransactionPointer::Unstake(i) => self + .unstake_txns + .get(i as usize) + .cloned() + .map(Transaction::Unstake), } } @@ -626,6 +645,11 @@ impl BlockTransactions { TransactionPointer::Stake(u32::try_from(i).unwrap()); items_to_add.push((tx.hash(), pointer_to_block.clone())); } + for (i, tx) in self.unstake_txns.iter().enumerate() { + pointer_to_block.transaction_index = + TransactionPointer::Unstake(u32::try_from(i).unwrap()); + items_to_add.push((tx.hash(), pointer_to_block.clone())); + } items_to_add } @@ -709,6 +733,8 @@ pub struct BlockMerkleRoots { 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, + /// A 256-bit hash based on all of the unstake transactions committed to this block + pub unstake_hash_merkle_root: Hash, } /// Function to calculate a merkle tree from a transaction vector @@ -738,6 +764,7 @@ impl BlockMerkleRoots { 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), + unstake_hash_merkle_root: merkle_tree_root(&txns.unstake_txns), } } } @@ -1976,6 +2003,7 @@ type PrioritizedHash = (OrderedFloat, Hash); type PrioritizedVTTransaction = (OrderedFloat, VTTransaction); type PrioritizedDRTransaction = (OrderedFloat, DRTransaction); type PrioritizedStakeTransaction = (OrderedFloat, StakeTransaction); +type PrioritizedUnstakeTransaction = (OrderedFloat, UnstakeTransaction); #[derive(Debug, Clone, Default)] struct UnconfirmedTransactions { @@ -2034,6 +2062,8 @@ pub struct TransactionsPool { total_dr_weight: u64, // Total size of all stake transactions inside the pool in weight units total_st_weight: u64, + // Total size of all unstake transactions inside the pool in weight units + total_ut_weight: u64, // TransactionsPool size limit in weight units weight_limit: u64, // Ratio of value transfer transaction to data request transaction that should be in the @@ -2056,9 +2086,14 @@ pub struct TransactionsPool { unconfirmed_transactions: UnconfirmedTransactions, st_transactions: HashMap, sorted_st_index: BTreeSet, + ut_transactions: HashMap, + sorted_ut_index: BTreeSet, // Minimum fee required to include a Stake Transaction into a block. We check for this fee in the // TransactionPool so we can choose not to insert a transaction we will not mine anyway. minimum_st_fee: u64, + // Minimum fee required to include a Unstake Transaction into a block. We check for this fee in the + // TransactionPool so we can choose not to insert a transaction we will not mine anyway. + minimum_ut_fee: u64, } impl Default for TransactionsPool { @@ -2076,6 +2111,7 @@ impl Default for TransactionsPool { total_vt_weight: 0, total_dr_weight: 0, total_st_weight: 0, + total_ut_weight: 0, // Unlimited by default weight_limit: u64::MAX, // Try to keep the same amount of value transfer weight and data request weight @@ -2084,6 +2120,8 @@ impl Default for TransactionsPool { minimum_vtt_fee: 0, // Default is to include all transactions into the pool and blocks minimum_st_fee: 0, + // Default is to include all transactions into the pool and blocks + minimum_ut_fee: 0, // Collateral minimum from consensus constants collateral_minimum: 0, // Required minimum reward to collateral percentage is defined as a consensus constant @@ -2091,6 +2129,8 @@ impl Default for TransactionsPool { unconfirmed_transactions: Default::default(), st_transactions: Default::default(), sorted_st_index: Default::default(), + ut_transactions: Default::default(), + sorted_ut_index: Default::default(), } } } @@ -2163,6 +2203,7 @@ impl TransactionsPool { && self.co_transactions.is_empty() && self.re_transactions.is_empty() && self.st_transactions.is_empty() + && self.ut_transactions.is_empty() } /// Remove all the transactions but keep the allocated memory for reuse. @@ -2180,15 +2221,19 @@ impl TransactionsPool { total_vt_weight, total_dr_weight, total_st_weight, + total_ut_weight, weight_limit: _, vt_to_dr_factor: _, minimum_vtt_fee: _, minimum_st_fee: _, + minimum_ut_fee: _, collateral_minimum: _, required_reward_collateral_ratio: _, unconfirmed_transactions, st_transactions, sorted_st_index, + ut_transactions, + sorted_ut_index, } = self; vt_transactions.clear(); @@ -2203,9 +2248,12 @@ impl TransactionsPool { *total_vt_weight = 0; *total_dr_weight = 0; *total_st_weight = 0; + *total_ut_weight = 0; unconfirmed_transactions.clear(); st_transactions.clear(); sorted_st_index.clear(); + ut_transactions.clear(); + sorted_ut_index.clear(); } /// Returns the number of value transfer transactions in the pool. @@ -2271,6 +2319,27 @@ impl TransactionsPool { self.st_transactions.len() } + /// Returns the number of unstake transactions in the pool. + /// + /// # Examples: + /// + /// ``` + /// # use witnet_data_structures::chain::{TransactionsPool, Hash}; + /// # use witnet_data_structures::transaction::{Transaction, StakeTransaction}; + /// let mut pool = TransactionsPool::new(); + /// + /// let transaction = Transaction::Stake(StakeTransaction::default()); + /// + /// assert_eq!(pool.st_len(), 0); + /// + /// pool.insert(transaction, 0); + /// + /// assert_eq!(pool.st_len(), 1); + /// ``` + pub fn ut_len(&self) -> usize { + self.ut_transactions.len() + } + /// Clear commit transactions in TransactionsPool pub fn clear_commits(&mut self) { self.co_transactions.clear(); @@ -2312,7 +2381,8 @@ 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) => Ok(self.st_contains(&tx_hash)), + Transaction::Stake(_st) => Ok(self.st_contains(&tx_hash)), + Transaction::Unstake(_ut) => Ok(self.ut_contains(&tx_hash)), } } @@ -2429,6 +2499,30 @@ impl TransactionsPool { self.st_transactions.contains_key(key) } + /// Returns `true` if the pool contains an unstake transaction for the + /// specified hash. + /// + /// The `key` may be any borrowed form of the hash, but `Hash` and + /// `Eq` on the borrowed form must match those for the key type. + /// + /// # Examples: + /// ``` + /// # use witnet_data_structures::chain::{TransactionsPool, Hash, Hashable}; + /// # use witnet_data_structures::transaction::{Transaction, UnstakeTransaction}; + /// let mut pool = TransactionsPool::new(); + /// + /// let transaction = Transaction::Stake(UnstakeTransaction::default()); + /// let hash = transaction.hash(); + /// assert!(!pool.ut_contains(&hash)); + /// + /// pool.insert(transaction, 0); + /// + /// assert!(pool.t_contains(&hash)); + /// ``` + pub fn ut_contains(&self, key: &Hash) -> bool { + self.ut_transactions.contains_key(key) + } + /// Remove a value transfer transaction from the pool and make sure that other transactions /// that may try to spend the same UTXOs are also removed. /// This should be used to remove transactions that got included in a consolidated block. @@ -2691,6 +2785,58 @@ impl TransactionsPool { }) } + /// Remove an unstake transaction from the pool and make sure that other transactions + /// that may try to spend the same UTXOs are also removed. + /// This should be used to remove transactions that got included in a consolidated block. + /// + /// Returns an `Option` with the value transfer transaction for the specified hash or `None` if not exist. + /// + /// The `key` may be any borrowed form of the hash, but `Hash` and + /// `Eq` on the borrowed form must match those for the key type. + /// + /// # Examples: + /// ``` + /// # use witnet_data_structures::chain::{TransactionsPool, Hash, Hashable}; + /// # use witnet_data_structures::transaction::{Transaction, UnstakeTransaction}; + /// let mut pool = TransactionsPool::new(); + /// let vt_transaction = UnstakeTransaction::default(); + /// let transaction = Transaction::Unstake(ut_transaction.clone()); + /// pool.insert(transaction.clone(),0); + /// + /// assert!(pool.ut_contains(&transaction.hash())); + /// + /// let op_transaction_removed = pool.ut_remove(&ut_transaction); + /// + /// assert_eq!(Some(ut_transaction), op_transaction_removed); + /// assert!(!pool.ut_contains(&transaction.hash())); + /// ``` + pub fn ut_remove(&mut self, tx: &UnstakeTransaction) -> Option { + let key = tx.hash(); + let transaction = self.ut_remove_inner(&key, true); + + transaction + } + + /// Remove a stake transaction from the pool but do not remove other transactions that + /// may try to spend the same UTXOs. + /// This should be used to remove transactions that did not get included in a consolidated + /// block. + /// If the transaction did get included in a consolidated block, use `st_remove` instead. + fn ut_remove_inner(&mut self, key: &Hash, consolidated: bool) -> Option { + // TODO: is this taking into account the change and the stake output? + self.ut_transactions + .remove(key) + .map(|(weight, transaction)| { + self.sorted_ut_index.remove(&(weight, *key)); + self.total_ut_weight -= u64::from(transaction.weight()); + // TODO? + // if !consolidated { + // self.remove_tx_from_output_pointer_map(key, &transaction.body.); + // } + transaction + }) + } + /// Returns a tuple with a vector of reveal transactions and the value /// of all the fees obtained with those reveals pub fn get_reveals(&self, dr_pool: &DataRequestPool) -> (Vec<&RevealTransaction>, u64) { @@ -2898,7 +3044,7 @@ impl TransactionsPool { for input in &st_tx.body.inputs { self.output_pointer_map .entry(input.output_pointer) - .or_insert_with(Vec::new) + .or_default() .push(st_tx.hash()); } @@ -2906,6 +3052,27 @@ impl TransactionsPool { self.sorted_st_index.insert((priority, key)); } } + Transaction::Unstake(ut_tx) => { + let weight = f64::from(ut_tx.weight()); + let priority = OrderedFloat(fee as f64 / weight); + + if fee < self.minimum_ut_fee { + return vec![Transaction::Unstake(ut_tx)]; + } else { + self.total_st_weight += u64::from(ut_tx.weight()); + + // TODO + // for input in &ut_tx.body.inputs { + // self.output_pointer_map + // .entry(input.output_pointer) + // .or_insert_with(Vec::new) + // .push(ut_tx.hash()); + // } + + self.ut_transactions.insert(key, (priority, ut_tx)); + self.sorted_ut_index.insert((priority, key)); + } + } tx => { panic!( "Transaction kind not supported by TransactionsPool: {:?}", @@ -2963,6 +3130,15 @@ impl TransactionsPool { .filter_map(move |(_, h)| self.st_transactions.get(h).map(|(_, t)| t)) } + /// An iterator visiting all the unstake transactions + /// in the pool + pub fn ut_iter(&self) -> impl Iterator { + self.sorted_ut_index + .iter() + .rev() + .filter_map(move |(_, h)| self.ut_transactions.get(h).map(|(_, t)| t)) + } + /// Returns a reference to the value corresponding to the key. /// /// Examples: @@ -3057,11 +3233,16 @@ impl TransactionsPool { self.re_hash_index .get(hash) .map(|rt| Transaction::Reveal(rt.clone())) - .or_else(|| { - self.st_transactions - .get(hash) - .map(|(_, st)| Transaction::Stake(st.clone())) - }) + }) + .or_else(|| { + self.st_transactions + .get(hash) + .map(|(_, st)| Transaction::Stake(st.clone())) + }) + .or_else(|| { + self.ut_transactions + .get(hash) + .map(|(_, ut)| Transaction::Unstake(ut.clone())) }) } @@ -3090,6 +3271,9 @@ impl TransactionsPool { Transaction::Stake(_) => { let _x = self.st_remove_inner(&hash, false); } + Transaction::Unstake(_) => { + let _x = self.ut_remove_inner(&hash, false); + } _ => continue, } @@ -3198,6 +3382,8 @@ pub enum TransactionPointer { Mint, // Stake Stake(u32), + // Unstake + Unstake(u32), } /// This is how transactions are stored in the database: hash of the containing block, plus index diff --git a/data_structures/src/error.rs b/data_structures/src/error.rs index fca6ee194..f0bd23232 100644 --- a/data_structures/src/error.rs +++ b/data_structures/src/error.rs @@ -284,12 +284,34 @@ pub enum TransactionError { min_stake, stake )] StakeBelowMinimum { min_stake: u64, stake: u64 }, - /// A stake output with zero value does not make sense + /// Unstaking more than the total staked #[fail( - display = "Transaction {} contains a stake output with zero value", - tx_hash + display = "Unstaking ({}) more than the total staked ({})", + unstake, stake )] + UnstakingMoreThanStaked { stake: u64, unstake: 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 }, + /// Invalid unstake signature + #[fail( + display = "Invalid unstake signature: ({}), withdrawal ({}), operator ({})", + signature, withdrawal, operator + )] + InvalidUnstakeSignature { + signature: Hash, + withdrawal: Hash, + operator: Hash, + }, + /// Invalid unstake time_lock + #[fail( + display = "The unstake timelock: ({}) is lower than the minimum unstaking delay ({})", + time_lock, unstaking_delay_seconds + )] + InvalidUnstakeTimelock { + time_lock: u64, + unstaking_delay_seconds: u32, + }, #[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 @@ -419,6 +441,12 @@ pub enum BlockError { weight, max_weight )] TotalStakeWeightLimitExceeded { weight: u32, max_weight: u32 }, + /// Unstake weight limit exceeded + #[fail( + display = "Total weight of Unstake Transactions in a block ({}) exceeds the limit ({})", + weight, max_weight + )] + TotalUnstakeWeightLimitExceeded { weight: u32, max_weight: u32 }, /// Repeated operator Stake #[fail( display = "A single operator is receiving stake more than once in a block: ({}) ", diff --git a/data_structures/src/superblock.rs b/data_structures/src/superblock.rs index 8cd978d0f..9394ddf83 100644 --- a/data_structures/src/superblock.rs +++ b/data_structures/src/superblock.rs @@ -807,6 +807,7 @@ mod tests { reveal_hash_merkle_root: default_hash, tally_hash_merkle_root: tally_merkle_root_1, stake_hash_merkle_root: default_hash, + unstake_hash_merkle_root: default_hash, }, proof: default_proof, bn256_public_key: None, @@ -857,6 +858,7 @@ mod tests { reveal_hash_merkle_root: default_hash, tally_hash_merkle_root: tally_merkle_root_1, stake_hash_merkle_root: default_hash, + unstake_hash_merkle_root: default_hash, }, proof: default_proof.clone(), bn256_public_key: None, @@ -873,6 +875,7 @@ mod tests { reveal_hash_merkle_root: default_hash, tally_hash_merkle_root: tally_merkle_root_2, stake_hash_merkle_root: default_hash, + unstake_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 5ac3054d1..e26c3041e 100644 --- a/data_structures/src/transaction.rs +++ b/data_structures/src/transaction.rs @@ -13,7 +13,7 @@ use crate::{ proto::{schema::witnet, ProtobufConvert}, vrf::DataRequestEligibilityClaim, }; - +pub const UNSTAKE_TRANSACTION_WEIGHT: u32 = 153; // These constants were calculated in: // https://github.com/witnet/WIPs/blob/master/wip-0007.md pub const INPUT_SIZE: u32 = 133; @@ -132,6 +132,7 @@ pub enum Transaction { Tally(TallyTransaction), Mint(MintTransaction), Stake(StakeTransaction), + Unstake(UnstakeTransaction), } impl From for Transaction { @@ -176,6 +177,12 @@ impl From for Transaction { } } +impl From for Transaction { + fn from(transaction: UnstakeTransaction) -> Self { + Self::Unstake(transaction) + } +} + impl AsRef for Transaction { fn as_ref(&self) -> &Self { self @@ -762,6 +769,64 @@ impl StakeOutput { } } +#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash)] +#[protobuf_convert(pb = "witnet::UnstakeTransaction")] +pub struct UnstakeTransaction { + pub body: UnstakeTransactionBody, + pub signature: KeyedSignature, +} +impl UnstakeTransaction { + // Creates a new unstake transaction. + pub fn new(body: UnstakeTransactionBody, signature: KeyedSignature) -> Self { + UnstakeTransaction { body, signature } + } + + /// Returns the weight of a unstake 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::UnstakeTransactionBody")] +pub struct UnstakeTransactionBody { + pub operator: PublicKeyHash, + pub withdrawal: ValueTransferOutput, + pub change: Option, + + #[protobuf_convert(skip)] + #[serde(skip)] + hash: MemoHash, +} + +impl UnstakeTransactionBody { + /// Creates a new stake transaction body. + pub fn new( + operator: PublicKeyHash, + withdrawal: ValueTransferOutput, + change: Option, + ) -> Self { + UnstakeTransactionBody { + operator, + withdrawal, + change, + hash: MemoHash::new(), + } + } + + /// Stake transaction weight. It is calculated as: + /// + /// ```text + /// ST_weight = 153 + /// + /// ``` + pub fn weight(&self) -> u32 { + UNSTAKE_TRANSACTION_WEIGHT + } +} + impl MemoizedHashable for VTTransactionBody { fn hashable_bytes(&self) -> Vec { self.to_pb_bytes().unwrap() @@ -810,6 +875,15 @@ impl MemoizedHashable for StakeTransactionBody { &self.hash } } +impl MemoizedHashable for UnstakeTransactionBody { + 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(); @@ -858,6 +932,11 @@ impl Hashable for StakeTransaction { self.body.hash() } } +impl Hashable for UnstakeTransaction { + fn hash(&self) -> Hash { + self.body.hash() + } +} impl Hashable for Transaction { fn hash(&self) -> Hash { @@ -869,6 +948,7 @@ impl Hashable for Transaction { Transaction::Tally(tx) => tx.hash(), Transaction::Mint(tx) => tx.hash(), Transaction::Stake(tx) => tx.hash(), + Transaction::Unstake(tx) => tx.hash(), } } } diff --git a/data_structures/src/types.rs b/data_structures/src/types.rs index 04370407e..6feda901e 100644 --- a/data_structures/src/types.rs +++ b/data_structures/src/types.rs @@ -76,6 +76,7 @@ impl fmt::Display for Command { Transaction::Tally(_) => f.write_str("TALLY_TRANSACTION")?, Transaction::Mint(_) => f.write_str("MINT_TRANSACTION")?, Transaction::Stake(_) => f.write_str("STAKE_TRANSACTION")?, + Transaction::Unstake(_) => f.write_str("UNSTAKE_TRANSACTION")?, } write!(f, ": {}", tx.hash()) } diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index e57632750..230ba47b6 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -841,6 +841,8 @@ pub fn build_block( let mut tally_txns = Vec::new(); // TODO: handle stake tx let stake_txns = Vec::new(); + // TODO: handle unstake tx + let unstake_txns = Vec::new(); let min_vt_weight = VTTransactionBody::new(vec![Input::default()], vec![ValueTransferOutput::default()]) @@ -1003,6 +1005,7 @@ pub fn build_block( 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 unstake_hash_merkle_root = merkle_tree_root(&unstake_txns); let merkle_roots = BlockMerkleRoots { mint_hash: mint.hash(), vt_hash_merkle_root, @@ -1011,6 +1014,7 @@ pub fn build_block( reveal_hash_merkle_root, tally_hash_merkle_root, stake_hash_merkle_root, + unstake_hash_merkle_root, }; let block_header = BlockHeader { @@ -1029,6 +1033,7 @@ pub fn build_block( reveal_txns, tally_txns, stake_txns, + unstake_txns, }; (block_header, txns) diff --git a/schemas/witnet/witnet.proto b/schemas/witnet/witnet.proto index d08283acb..237e472ae 100644 --- a/schemas/witnet/witnet.proto +++ b/schemas/witnet/witnet.proto @@ -60,6 +60,7 @@ message Block { Hash reveal_hash_merkle_root = 5; Hash tally_hash_merkle_root = 6; Hash stake_hash_merkle_root = 7; + Hash unstake_hash_merkle_root = 8; } uint32 signals = 1; CheckpointBeacon beacon = 2; @@ -75,6 +76,7 @@ message Block { repeated RevealTransaction reveal_txns = 5; repeated TallyTransaction tally_txns = 6; repeated StakeTransaction stake_txns = 7; + repeated UnstakeTransaction unstake_txns = 8; } BlockHeader block_header = 1; @@ -247,6 +249,17 @@ message StakeTransaction { repeated KeyedSignature signatures = 2; } +message UnstakeTransactionBody { + PublicKeyHash operator = 1; + ValueTransferOutput withdrawal = 2; + ValueTransferOutput change = 3; +} + +message UnstakeTransaction { + UnstakeTransactionBody body = 1 ; + KeyedSignature signature = 2; +} + message Transaction { oneof kind { VTTransaction ValueTransfer = 1; @@ -256,6 +269,7 @@ message Transaction { TallyTransaction Tally = 5; MintTransaction Mint = 6; StakeTransaction Stake = 7; + UnstakeTransaction Unstake = 8; } } diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 1224bba14..ece4d53c6 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -31,7 +31,7 @@ use witnet_data_structures::{ radon_report::{RadonReport, ReportContext}, transaction::{ CommitTransaction, DRTransaction, MintTransaction, RevealTransaction, StakeOutput, - StakeTransaction, TallyTransaction, Transaction, VTTransaction, + StakeTransaction, TallyTransaction, Transaction, UnstakeTransaction, VTTransaction, }, transaction_factory::{transaction_inputs_sum, transaction_outputs_sum}, types::visitor::Visitor, @@ -52,6 +52,8 @@ use witnet_rad::{ // TODO: move to a configuration const MAX_STAKE_BLOCK_WEIGHT: u32 = 10_000_000; const MIN_STAKE_NANOWITS: u64 = 10_000_000_000_000; +const MAX_UNSTAKE_BLOCK_WEIGHT: u32 = 5_000; +const UNSTAKING_DELAY_SECONDS: u32 = 1_209_600; /// Returns the fee of a value transfer transaction. /// @@ -124,6 +126,28 @@ pub fn st_transaction_fee( } } +/// Returns the fee of a unstake 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 ut_transaction_fee(ut_tx: &UnstakeTransaction) -> Result { + let in_value = ut_tx.body.withdrawal.value; + let out_value = ut_tx + .body + .clone() + .change + .unwrap_or(Default::default()) + .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) @@ -1169,6 +1193,80 @@ pub fn validate_stake_transaction<'a>( )) } +/// Function to validate a unstake transaction +pub fn validate_unstake_transaction<'a>( + ut_tx: &'a UnstakeTransaction, + st_tx: &'a StakeTransaction, + _utxo_diff: &UtxoDiff<'_>, + _epoch: Epoch, + _epoch_constants: EpochConstants, +) -> Result<(u64, u32, &'a Option), failure::Error> { + // Check if is unstaking more than the total stake + let amount_to_unstake = ut_tx.body.withdrawal.value; + if amount_to_unstake > st_tx.body.output.value { + return Err(TransactionError::UnstakingMoreThanStaked { + unstake: MIN_STAKE_NANOWITS, + stake: st_tx.body.output.value, + } + .into()); + } + + // Check that the stake is greater than the min allowed + if amount_to_unstake - st_tx.body.output.value < MIN_STAKE_NANOWITS { + return Err(TransactionError::StakeBelowMinimum { + min_stake: MIN_STAKE_NANOWITS, + stake: st_tx.body.output.value, + } + .into()); + } + + // TODO: take the operator from the StakesTracker when implemented + let operator = PublicKeyHash::default(); + // validate unstake_signature + validate_unstake_signature(&ut_tx, operator)?; + + // Validate unstake timestamp + validate_unstake_timelock(&ut_tx)?; + + // let fee = ut_tx.body.withdrawal.value; + let fee = ut_transaction_fee(ut_tx)?; + let weight = st_tx.weight(); + Ok((fee, weight, &ut_tx.body.change)) +} + +/// Validate unstake timelock +pub fn validate_unstake_timelock(ut_tx: &UnstakeTransaction) -> Result<(), failure::Error> { + // TODO: is this correct or should we use calculate it from the staking tx epoch? + if ut_tx.body.withdrawal.time_lock >= UNSTAKING_DELAY_SECONDS.into() { + return Err(TransactionError::InvalidUnstakeTimelock { + time_lock: ut_tx.body.withdrawal.time_lock, + unstaking_delay_seconds: UNSTAKING_DELAY_SECONDS, + } + .into()); + } + + Ok(()) +} + +/// Function to validate a unstake authorization +pub fn validate_unstake_signature<'a>( + ut_tx: &'a UnstakeTransaction, + operator: PublicKeyHash, +) -> Result<(), failure::Error> { + let ut_tx_pkh = ut_tx.signature.public_key.hash(); + // TODO: move to variables and use better names + if ut_tx_pkh != ut_tx.body.withdrawal.pkh.hash() || ut_tx_pkh != operator.hash() { + return Err(TransactionError::InvalidUnstakeSignature { + signature: ut_tx_pkh, + withdrawal: ut_tx.body.withdrawal.pkh.hash(), + operator: operator.hash(), + } + .into()); + } + + Ok(()) +} + /// Function to validate a block signature pub fn validate_block_signature( block: &Block, @@ -1816,6 +1914,43 @@ pub fn validate_block_transactions( let st_hash_merkle_root = st_mt.root(); + let mut ut_mt = ProgressiveMerkleTree::sha256(); + let mut ut_weight: u32 = 0; + + for transaction in &block.txns.unstake_txns { + // TODO: get tx, default to compile + let st_tx = StakeTransaction::default(); + let (fee, weight, _change) = + validate_unstake_transaction(transaction, &st_tx, &utxo_diff, epoch, epoch_constants)?; + + total_fee += fee; + + // Update ut weight + let acc_weight = ut_weight.saturating_add(weight); + if acc_weight > MAX_UNSTAKE_BLOCK_WEIGHT { + return Err(BlockError::TotalUnstakeWeightLimitExceeded { + weight: acc_weight, + max_weight: MAX_UNSTAKE_BLOCK_WEIGHT, + } + .into()); + } + ut_weight = acc_weight; + + // Add new hash to merkle tree + let txn_hash = transaction.hash(); + let Hash::SHA256(sha) = txn_hash; + ut_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 ut_hash_merkle_root = ut_mt.root(); + // Validate Merkle Root let merkle_roots = BlockMerkleRoots { mint_hash: block.txns.mint.hash(), @@ -1825,6 +1960,7 @@ pub fn validate_block_transactions( 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), + unstake_hash_merkle_root: Hash::from(ut_hash_merkle_root), }; if merkle_roots != block.block_header.merkle_roots { @@ -2272,6 +2408,7 @@ pub fn validate_merkle_tree(block: &Block) -> bool { 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), + unstake_hash_merkle_root: merkle_tree_root(&block.txns.unstake_txns), }; merkle_roots == block.block_header.merkle_roots diff --git a/wallet/src/repository/wallet/mod.rs b/wallet/src/repository/wallet/mod.rs index 215989653..175cc7bc7 100644 --- a/wallet/src/repository/wallet/mod.rs +++ b/wallet/src/repository/wallet/mod.rs @@ -1491,6 +1491,7 @@ where Transaction::Tally(_) => None, Transaction::Mint(_) => None, Transaction::Stake(tx) => Some(&tx.body.inputs), + Transaction::Unstake(_) => None, }; let empty_hashset = HashSet::default(); diff --git a/wallet/src/types.rs b/wallet/src/types.rs index 63924f646..4bc63fe28 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -22,7 +22,8 @@ use witnet_data_structures::{ fee::Fee, transaction::{ CommitTransaction, DRTransaction, DRTransactionBody, MintTransaction, RevealTransaction, - StakeTransaction, TallyTransaction, Transaction, VTTransaction, VTTransactionBody, + StakeTransaction, TallyTransaction, Transaction, UnstakeTransaction, VTTransaction, + VTTransactionBody, }, utxo_pool::UtxoSelectionStrategy, }; @@ -323,6 +324,7 @@ pub enum TransactionHelper { Tally(TallyTransaction), Mint(MintTransaction), Stake(StakeTransaction), + Unstake(UnstakeTransaction), } impl From for TransactionHelper { @@ -339,6 +341,9 @@ impl From for TransactionHelper { Transaction::Tally(tallytransaction) => TransactionHelper::Tally(tallytransaction), Transaction::Mint(minttransaction) => TransactionHelper::Mint(minttransaction), Transaction::Stake(staketransaction) => TransactionHelper::Stake(staketransaction), + Transaction::Unstake(unstaketransaction) => { + TransactionHelper::Unstake(unstaketransaction) + } } } } @@ -357,6 +362,9 @@ impl From for Transaction { TransactionHelper::Tally(tallytransaction) => Transaction::Tally(tallytransaction), TransactionHelper::Mint(minttransaction) => Transaction::Mint(minttransaction), TransactionHelper::Stake(staketransaction) => Transaction::Stake(staketransaction), + TransactionHelper::Unstake(unstaketransaction) => { + Transaction::Unstake(unstaketransaction) + } } } }