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) + } } } }