From a60981d18ee085011309c4fd37a4047845d3e850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Tue, 13 Jun 2023 18:27:10 +0200 Subject: [PATCH 01/12] feat(data_structures): add basic staking tracker with tests --- data_structures/src/lib.rs | 3 + data_structures/src/staking/mod.rs | 495 +++++++++++++++++++++++++++++ 2 files changed, 498 insertions(+) create mode 100644 data_structures/src/staking/mod.rs diff --git a/data_structures/src/lib.rs b/data_structures/src/lib.rs index 0b83c5cc90..6a991c7d2f 100644 --- a/data_structures/src/lib.rs +++ b/data_structures/src/lib.rs @@ -38,6 +38,9 @@ pub mod fee; /// Module containing data_request structures pub mod data_request; +/// Module containing data structures for the staking functionality +pub mod staking; + /// Module containing superblock structures pub mod superblock; diff --git a/data_structures/src/staking/mod.rs b/data_structures/src/staking/mod.rs new file mode 100644 index 0000000000..44d2c51273 --- /dev/null +++ b/data_structures/src/staking/mod.rs @@ -0,0 +1,495 @@ +use num_traits::Zero; +use std::collections::BTreeMap; + +use crate::wit::NANOWITS_PER_WIT; +use crate::{ + chain::{Epoch, PublicKeyHash}, + wit::Wit, +}; + +/// A minimum stakeable amount needs to exist to prevent spamming of the tracker. +const MINIMUM_STAKEABLE_AMOUNT_WITS: u64 = 10; +/// A maximum coin age is enforced to prevent an actor from monopolizing eligibility by means of +/// hoarding coin age. +const MAXIMUM_COIN_AGE_EPOCHS: u64 = 53_760; + +/// Type alias that represents the power of an identity in the network on a certain epoch. +/// +/// This is expected to be used for deriving eligibility. +pub type Power = u64; + +#[derive(Debug, PartialEq)] +pub enum StakesTrackerError { + AmountIsBelowMinimum { amount: Wit, minimum: Wit }, + EpochInThePast { epoch: Epoch, latest: Epoch }, + EpochOverflow { computed: u64, maximum: Epoch }, + IdentityNotFound { identity: PublicKeyHash }, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct StakesEntry { + /// How many coins does an identity have in stake + coins: Wit, + /// The weighted average of the epochs in which the stake was added + epoch: Epoch, + /// Further entries representing coins that are queued for unstaking + exiting_coins: Vec>, +} + +impl StakesEntry { + /// Updates an entry for a given epoch with a certain amount of coins. + /// + /// - Amounts are added together. + /// - Epochs are weight-averaged, using the amounts as the weight. + /// + /// This type of averaging makes the entry equivalent to an unbounded record of all stake + /// additions and removals, without the overhead in memory and computation. + pub fn add_stake( + &mut self, + amount: Wit, + epoch: Epoch, + ) -> Result<&StakesEntry, StakesTrackerError> { + // Make sure that the amount to be staked is equal or greater than the minimum + let minimum = Wit::from_wits(MINIMUM_STAKEABLE_AMOUNT_WITS); + if amount < minimum { + return Err(StakesTrackerError::AmountIsBelowMinimum { amount, minimum }); + } + + let coins_before = self.coins; + let epoch_before = self.epoch; + + // These "products" simply use the staked amount as the weight for the weighted average + let product_before = coins_before.nanowits() * u64::from(epoch_before); + let product_added = amount.nanowits() * u64::from(epoch); + + let coins_after = coins_before + amount; + let epoch_after = (product_before + product_added) / coins_after.nanowits(); + + self.coins = coins_after; + self.epoch = + Epoch::try_from(epoch_after).map_err(|_| StakesTrackerError::EpochOverflow { + computed: epoch_after, + maximum: Epoch::MAX, + })?; + + return Ok(self); + } + + /// Derives the power of an identity in the network on a certain epoch from an entry. + /// + /// A cap on coin age is enforced, and thus the maximum power is the total supply multiplied by + /// that cap. + pub fn power(&self, epoch: Epoch) -> Power { + let age = u64::from(epoch.saturating_sub(self.epoch)).min(MAXIMUM_COIN_AGE_EPOCHS); + let nano_wits = self.coins.nanowits(); + let power = nano_wits.saturating_mul(age) / NANOWITS_PER_WIT; + + power + } + + /// Remove a certain amount of staked coins. + pub fn remove_stake(&mut self, amount: Wit) -> Result<&StakesEntry, StakesTrackerError> { + // Make sure that the amount left in staked is equal or greater than the minimum + let minimum = Wit::from_wits(MINIMUM_STAKEABLE_AMOUNT_WITS); + let coins_after = + Wit::from_nanowits(self.coins.nanowits().saturating_sub(amount.nanowits())); + if coins_after > Wit::zero() && coins_after < minimum { + return Err(StakesTrackerError::AmountIsBelowMinimum { amount, minimum }); + } + + self.coins = coins_after; + + return Ok(self); + } +} + +/// Accumulates global stats about the staking tracker. +#[derive(Debug, Default, PartialEq)] +pub struct StakingStats { + /// Represents the average amount and epoch of the staked coins. + pub average: StakesEntry, + /// The latest epoch for which there is information in the tracker. + pub latest_epoch: Epoch, +} + +#[derive(Default)] +pub struct StakesTracker { + /// The individual stake records for all identities with a non-zero stake. + entries: BTreeMap, + /// Accumulates global stats about the staking tracker, as derived from the entries. + stats: StakingStats, +} + +impl StakesTracker { + /// Register a certain amount of additional stake for a certain identity and epoch. + pub fn add_stake( + &mut self, + identity: &PublicKeyHash, + amount: Wit, + epoch: Epoch, + ) -> Result<&StakesEntry, StakesTrackerError> { + // Refuse to add a stake for an epoch in the past + let latest = self.stats.latest_epoch; + if epoch < latest { + return Err(StakesTrackerError::EpochInThePast { epoch, latest }); + } + + // Find the entry or create it, then add the stake to it + let entry = self + .entries + .entry(*identity) + .or_insert_with(StakesEntry::default) + .add_stake(amount, epoch)?; + + // Because the entry was updated, let's also update all the derived data + self.stats.latest_epoch = epoch; + self.stats.average.add_stake(amount, epoch + 1)?; + + Ok(entry) + } + + /// Tells what is the power of an identity in the network on a certain epoch. + pub fn query_power(&self, identity: &PublicKeyHash, epoch: Epoch) -> Power { + self.entries + .get(identity) + .map(|entry| entry.power(epoch)) + .unwrap_or_default() + } + + /// Tells what is the share of the power of an identity in the network on a certain epoch. + pub fn query_share(&self, identity: &PublicKeyHash, epoch: Epoch) -> f64 { + let power = self.query_power(identity, epoch); + let total_power = self.stats.average.power(epoch).max(1); + let share = (power as f64 / total_power as f64).min(1.0); + + share + } + + /// Tells how many entries are there in the tracker, paired with some other statistics. + pub fn stats(&self) -> (usize, &StakingStats) { + let entries_count = self.entries.len(); + let stats = &self.stats; + + (entries_count, stats) + } + + /// Remove a certain amount of staked coins from a given identity at a given epoch. + pub fn remove_stake( + &mut self, + identity: &PublicKeyHash, + amount: Wit, + ) -> Result, StakesTrackerError> { + // Find the entry or create it, then remove the stake from it + let entry = self + .entries + .entry(*identity) + .or_insert_with(StakesEntry::default) + .remove_stake(amount)? + .clone(); + + // If the identity is left without stake, it can be dropped from the tracker + if entry.coins == Wit::zero() { + self.entries.remove(identity); + return Ok(None); + } + + // Because the entry was updated, let's also update all the derived data + self.stats.average.remove_stake(amount)?; + + Ok(Some(entry)) + } + + /// Removes and adds an amount of stake at once, i.e. the amount remains the same, but the age + /// gets reset. + pub fn use_stake( + &mut self, + identity: &PublicKeyHash, + amount: Wit, + epoch: Epoch, + ) -> Result { + // First remove the stake + self.remove_stake(identity, amount)?; + // Then add it again at the same epoch + self.add_stake(identity, amount, epoch).cloned() + } +} + +#[cfg(test)] +mod tests { + use crate::chain::Environment; + + use super::*; + + #[test] + fn test_tracker_initialization() { + let tracker = StakesTracker::default(); + let (count, stats) = tracker.stats(); + assert_eq!(count, 0); + assert_eq!(stats, &StakingStats::default()); + } + + #[test] + fn test_add_stake() { + let mut tracker = StakesTracker::default(); + let alice = PublicKeyHash::from_bech32( + Environment::Mainnet, + "wit1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwrt3a4", + ) + .unwrap(); + let bob = PublicKeyHash::from_bech32( + Environment::Mainnet, + "wit100000000000000000000000000000000r0v4g2", + ) + .unwrap(); + + // Let's check default power and share + assert_eq!(tracker.query_power(&alice, 0), 0); + assert_eq!(tracker.query_share(&alice, 0), 0.0); + assert_eq!(tracker.query_power(&alice, 1_000), 0); + assert_eq!(tracker.query_share(&alice, 1_000), 0.0); + + // Let's make Alice stake 100 Wit at epoch 100 + let updated = tracker.add_stake(&alice, Wit::from_wits(100), 100).unwrap(); + assert_eq!( + updated, + &StakesEntry { + coins: Wit::from_wits(100), + epoch: 100, + exiting_coins: vec![], + } + ); + let (count, stats) = tracker.stats(); + assert_eq!(count, 1); + assert_eq!( + stats, + &StakingStats { + average: StakesEntry { + coins: Wit::from_wits(100), + epoch: 101, + exiting_coins: vec![], + }, + latest_epoch: 100, + } + ); + assert_eq!(tracker.query_power(&alice, 99), 0); + assert_eq!(tracker.query_share(&alice, 99), 0.0); + assert_eq!(tracker.query_power(&alice, 100), 0); + assert_eq!(tracker.query_share(&alice, 100), 0.0); + assert_eq!(tracker.query_power(&alice, 101), 100); + assert_eq!(tracker.query_share(&alice, 101), 1.0); + assert_eq!(tracker.query_power(&alice, 200), 10_000); + assert_eq!(tracker.query_share(&alice, 200), 1.0); + + // Let's make Alice stake 50 Wits at epoch 150 this time + let updated = tracker.add_stake(&alice, Wit::from_wits(50), 300).unwrap(); + assert_eq!( + updated, + &StakesEntry { + coins: Wit::from_wits(150), + epoch: 166, + exiting_coins: vec![], + } + ); + let (count, stats) = tracker.stats(); + assert_eq!(count, 1); + assert_eq!( + stats, + &StakingStats { + average: StakesEntry { + coins: Wit::from_wits(150), + epoch: 167, + exiting_coins: vec![], + }, + latest_epoch: 300, + } + ); + assert_eq!(tracker.query_power(&alice, 299), 19_950); + assert_eq!(tracker.query_share(&alice, 299), 1.0); + assert_eq!(tracker.query_power(&alice, 300), 20_100); + assert_eq!(tracker.query_share(&alice, 300), 1.0); + assert_eq!(tracker.query_power(&alice, 301), 20_250); + assert_eq!(tracker.query_share(&alice, 301), 1.0); + assert_eq!(tracker.query_power(&alice, 400), 35_100); + assert_eq!(tracker.query_share(&alice, 400), 1.0); + + // Now let's make Bob stake 50 Wits at epoch 150 this time + let updated = tracker.add_stake(&bob, Wit::from_wits(10), 1_000).unwrap(); + assert_eq!( + updated, + &StakesEntry { + coins: Wit::from_wits(10), + epoch: 1_000, + exiting_coins: vec![], + } + ); + let (count, stats) = tracker.stats(); + assert_eq!(count, 2); + assert_eq!( + stats, + &StakingStats { + average: StakesEntry { + coins: Wit::from_wits(160), + epoch: 219, + exiting_coins: vec![], + }, + latest_epoch: 1_000, + } + ); + // Before Bob stakes, Alice has all the power and share + assert_eq!(tracker.query_power(&bob, 999), 0); + assert_eq!(tracker.query_share(&bob, 999), 0.0); + assert_eq!(tracker.query_share(&alice, 999), 1.0); + assert_eq!( + tracker.query_share(&alice, 999) + tracker.query_share(&bob, 999), + 1.0 + ); + // New stakes don't change power or share in the same epoch + assert_eq!(tracker.query_power(&bob, 1_000), 0); + assert_eq!(tracker.query_share(&bob, 1_000), 0.0); + assert_eq!(tracker.query_share(&alice, 1_000), 1.0); + assert_eq!( + tracker.query_share(&alice, 1_000) + tracker.query_share(&bob, 1_000), + 1.0 + ); + // Shortly as Bob's stake gains power, Alice loses a roughly equivalent share + assert_eq!(tracker.query_power(&bob, 1_100), 1_000); + assert_eq!(tracker.query_share(&bob, 1_100), 0.007094211123723042); + assert_eq!(tracker.query_share(&alice, 1_100), 0.9938989784335982); + assert_eq!( + tracker.query_share(&alice, 1_100) + tracker.query_share(&bob, 1_100), + 1.0009931895573212 + ); + // After enough time, both's shares should become proportional to their stake, and add up to 1.0 again + assert_eq!(tracker.query_power(&bob, 1_000_000), 537600); + assert_eq!(tracker.query_share(&bob, 1_000_000), 0.0625); + assert_eq!(tracker.query_share(&alice, 1_000_000), 0.9375); + assert_eq!( + tracker.query_share(&alice, 1_000_000) + tracker.query_share(&bob, 1_000_000), + 1.0 + ); + } + + #[test] + fn test_minimum_stake() { + let mut tracker = StakesTracker::default(); + let alice = PublicKeyHash::from_bech32( + Environment::Mainnet, + "wit1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwrt3a4", + ) + .unwrap(); + let error = tracker + .add_stake( + &alice, + Wit::from_wits(MINIMUM_STAKEABLE_AMOUNT_WITS - 1), + 100, + ) + .unwrap_err(); + + assert_eq!( + error, + StakesTrackerError::AmountIsBelowMinimum { + amount: Wit::from_wits(MINIMUM_STAKEABLE_AMOUNT_WITS - 1), + minimum: Wit::from_wits(MINIMUM_STAKEABLE_AMOUNT_WITS) + } + ); + } + + #[test] + fn test_maximum_coin_age() { + let mut tracker = StakesTracker::default(); + let alice = PublicKeyHash::from_bech32( + Environment::Mainnet, + "wit1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwrt3a4", + ) + .unwrap(); + tracker + .add_stake(&alice, Wit::from_wits(MINIMUM_STAKEABLE_AMOUNT_WITS), 0) + .unwrap(); + assert_eq!(tracker.query_power(&alice, 0), 0); + assert_eq!( + tracker.query_power(&alice, 1), + MINIMUM_STAKEABLE_AMOUNT_WITS + ); + assert_eq!( + tracker.query_power(&alice, MAXIMUM_COIN_AGE_EPOCHS as Epoch - 1), + MINIMUM_STAKEABLE_AMOUNT_WITS * (MAXIMUM_COIN_AGE_EPOCHS - 1) + ); + assert_eq!( + tracker.query_power(&alice, MAXIMUM_COIN_AGE_EPOCHS as Epoch), + MINIMUM_STAKEABLE_AMOUNT_WITS * MAXIMUM_COIN_AGE_EPOCHS + ); + assert_eq!( + tracker.query_power(&alice, MAXIMUM_COIN_AGE_EPOCHS as Epoch + 1), + MINIMUM_STAKEABLE_AMOUNT_WITS * MAXIMUM_COIN_AGE_EPOCHS + ); + } + + #[test] + fn test_remove_stake() { + let mut tracker = StakesTracker::default(); + let alice = PublicKeyHash::from_bech32( + Environment::Mainnet, + "wit1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwrt3a4", + ) + .unwrap(); + let updated = tracker.add_stake(&alice, Wit::from_wits(100), 100).unwrap(); + assert_eq!( + updated, + &StakesEntry { + coins: Wit::from_wits(100), + epoch: 100, + exiting_coins: vec![], + } + ); + // Removing stake should reduce the amount, but keep the age the same + let updated = tracker.remove_stake(&alice, Wit::from_wits(50)).unwrap(); + assert_eq!( + updated, + Some(StakesEntry { + coins: Wit::from_wits(50), + epoch: 100, + exiting_coins: vec![], + }) + ); + } + + #[test] + fn test_use_stake() { + let mut tracker = StakesTracker::default(); + let alice = PublicKeyHash::from_bech32( + Environment::Mainnet, + "wit1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwrt3a4", + ) + .unwrap(); + let updated = tracker.add_stake(&alice, Wit::from_wits(100), 0).unwrap(); + assert_eq!( + updated, + &StakesEntry { + coins: Wit::from_wits(100), + epoch: 0, + exiting_coins: vec![], + } + ); + // After using all the stake, the amount should stay the same, but the epoch should be reset. + let updated = tracker.use_stake(&alice, Wit::from_wits(100), 100).unwrap(); + assert_eq!( + updated, + StakesEntry { + coins: Wit::from_wits(100), + epoch: 100, + exiting_coins: vec![], + } + ); + // But if we use half the stake, again the amount should stay the same, and the epoch should + // be updated to a point in the middle. + let updated = tracker.use_stake(&alice, Wit::from_wits(50), 200).unwrap(); + assert_eq!( + updated, + StakesEntry { + coins: Wit::from_wits(100), + epoch: 150, + exiting_coins: vec![], + } + ); + } +} From bcdfeb775fbc3cff7ec02d178f20a3d8c4beb0b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Mon, 16 Oct 2023 19:56:30 +0200 Subject: [PATCH 02/12] feat(data_structures): adapt stakes tracker to latest specs fix #2398 --- data_structures/Cargo.toml | 4 + data_structures/benches/staking.rs | 85 ++++ data_structures/src/capabilities.rs | 44 ++ data_structures/src/lib.rs | 3 + data_structures/src/staking/aux.rs | 37 ++ data_structures/src/staking/constants.rs | 2 + data_structures/src/staking/errors.rs | 41 ++ data_structures/src/staking/mod.rs | 586 ++++------------------- data_structures/src/staking/simple.rs | 0 data_structures/src/staking/stake.rs | 122 +++++ data_structures/src/staking/stakes.rs | 466 ++++++++++++++++++ 11 files changed, 903 insertions(+), 487 deletions(-) create mode 100644 data_structures/benches/staking.rs create mode 100644 data_structures/src/capabilities.rs create mode 100644 data_structures/src/staking/aux.rs create mode 100644 data_structures/src/staking/constants.rs create mode 100644 data_structures/src/staking/errors.rs create mode 100644 data_structures/src/staking/simple.rs create mode 100644 data_structures/src/staking/stake.rs create mode 100644 data_structures/src/staking/stakes.rs diff --git a/data_structures/Cargo.toml b/data_structures/Cargo.toml index f09cf96999..17f6ea586b 100644 --- a/data_structures/Cargo.toml +++ b/data_structures/Cargo.toml @@ -51,3 +51,7 @@ rand_distr = "0.4.3" [[bench]] name = "sort_active_identities" harness = false + +[[bench]] +name = "staking" +harness = false diff --git a/data_structures/benches/staking.rs b/data_structures/benches/staking.rs new file mode 100644 index 0000000000..8bbee63f89 --- /dev/null +++ b/data_structures/benches/staking.rs @@ -0,0 +1,85 @@ +#[macro_use] +extern crate bencher; +use bencher::Bencher; +use rand::Rng; +use witnet_data_structures::staking::prelude::*; + +fn populate(b: &mut Bencher) { + let mut stakes = Stakes::::default(); + let mut i = 1; + + b.iter(|| { + let address = format!("{i}"); + let coins = i; + let epoch = i; + stakes.add_stake(address, coins, epoch).unwrap(); + + i += 1; + }); +} + +fn rank(b: &mut Bencher) { + let mut stakes = Stakes::::default(); + let mut i = 1; + + let stakers = 100_000; + let rf = 10; + + let mut rng = rand::thread_rng(); + + loop { + let coins = i; + let epoch = i; + let address = format!("{}", rng.gen::()); + + stakes.add_stake(address, coins, epoch).unwrap(); + + i += 1; + + if i == stakers { + break; + } + } + + b.iter(|| { + let rank = stakes.rank(Capability::Mining, i); + let mut top = rank.take(usize::try_from(stakers / rf).unwrap()); + let _first = top.next(); + let _last = top.last(); + + i += 1; + }) +} + +fn query_power(b: &mut Bencher) { + let mut stakes = Stakes::::default(); + let mut i = 1; + + let stakers = 100_000; + + loop { + let coins = i; + let epoch = i; + let address = format!("{i}"); + + stakes.add_stake(address, coins, epoch).unwrap(); + + i += 1; + + if i == stakers { + break; + } + } + + i = 1; + + b.iter(|| { + let address = format!("{i}"); + let _power = stakes.query_power(&address, Capability::Mining, i); + + i += 1; + }) +} + +benchmark_main!(benches); +benchmark_group!(benches, populate, rank, query_power); diff --git a/data_structures/src/capabilities.rs b/data_structures/src/capabilities.rs new file mode 100644 index 0000000000..80cd8257b8 --- /dev/null +++ b/data_structures/src/capabilities.rs @@ -0,0 +1,44 @@ +#[repr(u8)] +#[derive(Clone, Copy, Debug)] +pub enum Capability { + /// The base block mining and superblock voting capability + Mining = 0, + /// The universal HTTP GET / HTTP POST / WIP-0019 RNG capability + Witnessing = 1, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct CapabilityMap +where + T: Default, +{ + pub mining: T, + pub witnessing: T, +} + +impl CapabilityMap +where + T: Copy + Default, +{ + #[inline] + pub fn get(&self, capability: Capability) -> T { + match capability { + Capability::Mining => self.mining, + Capability::Witnessing => self.witnessing, + } + } + + #[inline] + pub fn update(&mut self, capability: Capability, value: T) { + match capability { + Capability::Mining => self.mining = value, + Capability::Witnessing => self.witnessing = value, + } + } + + #[inline] + pub fn update_all(&mut self, value: T) { + self.mining = value; + self.witnessing = value; + } +} diff --git a/data_structures/src/lib.rs b/data_structures/src/lib.rs index 6a991c7d2f..e14acd6e5e 100644 --- a/data_structures/src/lib.rs +++ b/data_structures/src/lib.rs @@ -72,6 +72,9 @@ mod serialization_helpers; /// Provides convenient constants, structs and methods for handling values denominated in Wit. pub mod wit; +/// Provides support for segmented protocol capabilities. +pub mod capabilities; + lazy_static! { /// Environment in which we are running: mainnet or testnet. /// This is used for Bech32 serialization. diff --git a/data_structures/src/staking/aux.rs b/data_structures/src/staking/aux.rs new file mode 100644 index 0000000000..4241581648 --- /dev/null +++ b/data_structures/src/staking/aux.rs @@ -0,0 +1,37 @@ +use std::rc::Rc; +use std::sync::RwLock; + +use super::prelude::*; + +/// Type alias for a reference-counted and read-write-locked instance of `Stake`. +pub type SyncStake = Rc>>; + +/// The resulting type for all the fallible functions in this module. +pub type Result = + std::result::Result>; + +/// Couples an amount of coins and an address together. This is to be used in `Stakes` as the index +/// of the `by_coins` index.. +#[derive(Eq, Ord, PartialEq, PartialOrd)] +pub struct CoinsAndAddress { + /// An amount of coins. + pub coins: Coins, + /// The address of a staker. + pub address: Address, +} + +/// Allows telling the `census` method in `Stakes` to source addresses from its internal `by_coins` +/// following different strategies. +#[repr(u8)] +#[derive(Clone, Copy, Debug)] +pub enum CensusStrategy { + /// Retrieve all addresses, ordered by decreasing power. + All = 0, + /// Retrieve every Nth address, ordered by decreasing power. + StepBy(usize) = 1, + /// Retrieve the most powerful N addresses, ordered by decreasing power. + Take(usize) = 2, + /// Retrieve a total of N addresses, evenly distributed from the index, ordered by decreasing + /// power. + Evenly(usize) = 3, +} diff --git a/data_structures/src/staking/constants.rs b/data_structures/src/staking/constants.rs new file mode 100644 index 0000000000..d461b0560e --- /dev/null +++ b/data_structures/src/staking/constants.rs @@ -0,0 +1,2 @@ +/// A minimum stakeable amount needs to exist to prevent spamming of the tracker. +pub const MINIMUM_STAKEABLE_AMOUNT_WITS: u64 = 10_000; diff --git a/data_structures/src/staking/errors.rs b/data_structures/src/staking/errors.rs new file mode 100644 index 0000000000..6169073f47 --- /dev/null +++ b/data_structures/src/staking/errors.rs @@ -0,0 +1,41 @@ +use std::sync::PoisonError; + +/// All errors related to the staking functionality. +#[derive(Debug, PartialEq)] +pub enum StakesError { + /// The amount of coins being staked or the amount that remains after unstaking is below the + /// minimum stakeable amount. + AmountIsBelowMinimum { + /// The number of coins being staked or remaining after staking. + amount: Coins, + /// The minimum stakeable amount. + minimum: Coins, + }, + /// Tried to query `Stakes` for information that belongs to the past. + EpochInThePast { + /// The Epoch being referred. + epoch: Epoch, + /// The latest Epoch. + latest: Epoch, + }, + /// An operation thrown an Epoch value that overflows. + EpochOverflow { + /// The computed Epoch value. + computed: u64, + /// The maximum Epoch. + maximum: Epoch, + }, + /// Tried to query `Stakes` for the address of a staker that is not registered in `Stakes`. + IdentityNotFound { + /// The unknown address. + identity: Address, + }, + /// Tried to obtain a lock on a write-locked piece of data that is already locked. + PoisonedLock, +} + +impl From> for StakesError { + fn from(_value: PoisonError) -> Self { + StakesError::PoisonedLock + } +} diff --git a/data_structures/src/staking/mod.rs b/data_structures/src/staking/mod.rs index 44d2c51273..1a5b21418a 100644 --- a/data_structures/src/staking/mod.rs +++ b/data_structures/src/staking/mod.rs @@ -1,495 +1,107 @@ -use num_traits::Zero; -use std::collections::BTreeMap; - -use crate::wit::NANOWITS_PER_WIT; -use crate::{ - chain::{Epoch, PublicKeyHash}, - wit::Wit, -}; - -/// A minimum stakeable amount needs to exist to prevent spamming of the tracker. -const MINIMUM_STAKEABLE_AMOUNT_WITS: u64 = 10; -/// A maximum coin age is enforced to prevent an actor from monopolizing eligibility by means of -/// hoarding coin age. -const MAXIMUM_COIN_AGE_EPOCHS: u64 = 53_760; - -/// Type alias that represents the power of an identity in the network on a certain epoch. -/// -/// This is expected to be used for deriving eligibility. -pub type Power = u64; - -#[derive(Debug, PartialEq)] -pub enum StakesTrackerError { - AmountIsBelowMinimum { amount: Wit, minimum: Wit }, - EpochInThePast { epoch: Epoch, latest: Epoch }, - EpochOverflow { computed: u64, maximum: Epoch }, - IdentityNotFound { identity: PublicKeyHash }, -} - -#[derive(Clone, Debug, Default, PartialEq)] -pub struct StakesEntry { - /// How many coins does an identity have in stake - coins: Wit, - /// The weighted average of the epochs in which the stake was added - epoch: Epoch, - /// Further entries representing coins that are queued for unstaking - exiting_coins: Vec>, -} - -impl StakesEntry { - /// Updates an entry for a given epoch with a certain amount of coins. - /// - /// - Amounts are added together. - /// - Epochs are weight-averaged, using the amounts as the weight. - /// - /// This type of averaging makes the entry equivalent to an unbounded record of all stake - /// additions and removals, without the overhead in memory and computation. - pub fn add_stake( - &mut self, - amount: Wit, - epoch: Epoch, - ) -> Result<&StakesEntry, StakesTrackerError> { - // Make sure that the amount to be staked is equal or greater than the minimum - let minimum = Wit::from_wits(MINIMUM_STAKEABLE_AMOUNT_WITS); - if amount < minimum { - return Err(StakesTrackerError::AmountIsBelowMinimum { amount, minimum }); - } - - let coins_before = self.coins; - let epoch_before = self.epoch; - - // These "products" simply use the staked amount as the weight for the weighted average - let product_before = coins_before.nanowits() * u64::from(epoch_before); - let product_added = amount.nanowits() * u64::from(epoch); - - let coins_after = coins_before + amount; - let epoch_after = (product_before + product_added) / coins_after.nanowits(); - - self.coins = coins_after; - self.epoch = - Epoch::try_from(epoch_after).map_err(|_| StakesTrackerError::EpochOverflow { - computed: epoch_after, - maximum: Epoch::MAX, - })?; - - return Ok(self); - } - - /// Derives the power of an identity in the network on a certain epoch from an entry. - /// - /// A cap on coin age is enforced, and thus the maximum power is the total supply multiplied by - /// that cap. - pub fn power(&self, epoch: Epoch) -> Power { - let age = u64::from(epoch.saturating_sub(self.epoch)).min(MAXIMUM_COIN_AGE_EPOCHS); - let nano_wits = self.coins.nanowits(); - let power = nano_wits.saturating_mul(age) / NANOWITS_PER_WIT; - - power - } - - /// Remove a certain amount of staked coins. - pub fn remove_stake(&mut self, amount: Wit) -> Result<&StakesEntry, StakesTrackerError> { - // Make sure that the amount left in staked is equal or greater than the minimum - let minimum = Wit::from_wits(MINIMUM_STAKEABLE_AMOUNT_WITS); - let coins_after = - Wit::from_nanowits(self.coins.nanowits().saturating_sub(amount.nanowits())); - if coins_after > Wit::zero() && coins_after < minimum { - return Err(StakesTrackerError::AmountIsBelowMinimum { amount, minimum }); - } - - self.coins = coins_after; - - return Ok(self); - } -} - -/// Accumulates global stats about the staking tracker. -#[derive(Debug, Default, PartialEq)] -pub struct StakingStats { - /// Represents the average amount and epoch of the staked coins. - pub average: StakesEntry, - /// The latest epoch for which there is information in the tracker. - pub latest_epoch: Epoch, -} - -#[derive(Default)] -pub struct StakesTracker { - /// The individual stake records for all identities with a non-zero stake. - entries: BTreeMap, - /// Accumulates global stats about the staking tracker, as derived from the entries. - stats: StakingStats, -} - -impl StakesTracker { - /// Register a certain amount of additional stake for a certain identity and epoch. - pub fn add_stake( - &mut self, - identity: &PublicKeyHash, - amount: Wit, - epoch: Epoch, - ) -> Result<&StakesEntry, StakesTrackerError> { - // Refuse to add a stake for an epoch in the past - let latest = self.stats.latest_epoch; - if epoch < latest { - return Err(StakesTrackerError::EpochInThePast { epoch, latest }); - } - - // Find the entry or create it, then add the stake to it - let entry = self - .entries - .entry(*identity) - .or_insert_with(StakesEntry::default) - .add_stake(amount, epoch)?; - - // Because the entry was updated, let's also update all the derived data - self.stats.latest_epoch = epoch; - self.stats.average.add_stake(amount, epoch + 1)?; - - Ok(entry) - } - - /// Tells what is the power of an identity in the network on a certain epoch. - pub fn query_power(&self, identity: &PublicKeyHash, epoch: Epoch) -> Power { - self.entries - .get(identity) - .map(|entry| entry.power(epoch)) - .unwrap_or_default() - } - - /// Tells what is the share of the power of an identity in the network on a certain epoch. - pub fn query_share(&self, identity: &PublicKeyHash, epoch: Epoch) -> f64 { - let power = self.query_power(identity, epoch); - let total_power = self.stats.average.power(epoch).max(1); - let share = (power as f64 / total_power as f64).min(1.0); - - share - } - - /// Tells how many entries are there in the tracker, paired with some other statistics. - pub fn stats(&self) -> (usize, &StakingStats) { - let entries_count = self.entries.len(); - let stats = &self.stats; - - (entries_count, stats) - } - - /// Remove a certain amount of staked coins from a given identity at a given epoch. - pub fn remove_stake( - &mut self, - identity: &PublicKeyHash, - amount: Wit, - ) -> Result, StakesTrackerError> { - // Find the entry or create it, then remove the stake from it - let entry = self - .entries - .entry(*identity) - .or_insert_with(StakesEntry::default) - .remove_stake(amount)? - .clone(); - - // If the identity is left without stake, it can be dropped from the tracker - if entry.coins == Wit::zero() { - self.entries.remove(identity); - return Ok(None); - } - - // Because the entry was updated, let's also update all the derived data - self.stats.average.remove_stake(amount)?; - - Ok(Some(entry)) - } - - /// Removes and adds an amount of stake at once, i.e. the amount remains the same, but the age - /// gets reset. - pub fn use_stake( - &mut self, - identity: &PublicKeyHash, - amount: Wit, - epoch: Epoch, - ) -> Result { - // First remove the stake - self.remove_stake(identity, amount)?; - // Then add it again at the same epoch - self.add_stake(identity, amount, epoch).cloned() - } +#![deny(missing_docs)] + +/// Auxiliary convenience types and data structures. +pub mod aux; +/// Constants related to the staking functionality. +pub mod constants; +/// Errors related to the staking functionality. +pub mod errors; +/// The data structure and related logic for stake entries. +pub mod stake; +/// The data structure and related logic for keeping track of multiple stake entries. +pub mod stakes; + +/// Module re-exporting virtually every submodule on a single level to ease importing of everything +/// staking-related. +pub mod prelude { + pub use crate::capabilities::*; + + pub use super::aux::*; + pub use super::constants::*; + pub use super::errors::*; + pub use super::stake::*; + pub use super::stakes::*; } #[cfg(test)] -mod tests { - use crate::chain::Environment; - - use super::*; - - #[test] - fn test_tracker_initialization() { - let tracker = StakesTracker::default(); - let (count, stats) = tracker.stats(); - assert_eq!(count, 0); - assert_eq!(stats, &StakingStats::default()); - } - - #[test] - fn test_add_stake() { - let mut tracker = StakesTracker::default(); - let alice = PublicKeyHash::from_bech32( - Environment::Mainnet, - "wit1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwrt3a4", - ) - .unwrap(); - let bob = PublicKeyHash::from_bech32( - Environment::Mainnet, - "wit100000000000000000000000000000000r0v4g2", - ) - .unwrap(); - - // Let's check default power and share - assert_eq!(tracker.query_power(&alice, 0), 0); - assert_eq!(tracker.query_share(&alice, 0), 0.0); - assert_eq!(tracker.query_power(&alice, 1_000), 0); - assert_eq!(tracker.query_share(&alice, 1_000), 0.0); - - // Let's make Alice stake 100 Wit at epoch 100 - let updated = tracker.add_stake(&alice, Wit::from_wits(100), 100).unwrap(); - assert_eq!( - updated, - &StakesEntry { - coins: Wit::from_wits(100), - epoch: 100, - exiting_coins: vec![], - } - ); - let (count, stats) = tracker.stats(); - assert_eq!(count, 1); - assert_eq!( - stats, - &StakingStats { - average: StakesEntry { - coins: Wit::from_wits(100), - epoch: 101, - exiting_coins: vec![], - }, - latest_epoch: 100, - } - ); - assert_eq!(tracker.query_power(&alice, 99), 0); - assert_eq!(tracker.query_share(&alice, 99), 0.0); - assert_eq!(tracker.query_power(&alice, 100), 0); - assert_eq!(tracker.query_share(&alice, 100), 0.0); - assert_eq!(tracker.query_power(&alice, 101), 100); - assert_eq!(tracker.query_share(&alice, 101), 1.0); - assert_eq!(tracker.query_power(&alice, 200), 10_000); - assert_eq!(tracker.query_share(&alice, 200), 1.0); - - // Let's make Alice stake 50 Wits at epoch 150 this time - let updated = tracker.add_stake(&alice, Wit::from_wits(50), 300).unwrap(); - assert_eq!( - updated, - &StakesEntry { - coins: Wit::from_wits(150), - epoch: 166, - exiting_coins: vec![], - } - ); - let (count, stats) = tracker.stats(); - assert_eq!(count, 1); - assert_eq!( - stats, - &StakingStats { - average: StakesEntry { - coins: Wit::from_wits(150), - epoch: 167, - exiting_coins: vec![], - }, - latest_epoch: 300, - } - ); - assert_eq!(tracker.query_power(&alice, 299), 19_950); - assert_eq!(tracker.query_share(&alice, 299), 1.0); - assert_eq!(tracker.query_power(&alice, 300), 20_100); - assert_eq!(tracker.query_share(&alice, 300), 1.0); - assert_eq!(tracker.query_power(&alice, 301), 20_250); - assert_eq!(tracker.query_share(&alice, 301), 1.0); - assert_eq!(tracker.query_power(&alice, 400), 35_100); - assert_eq!(tracker.query_share(&alice, 400), 1.0); - - // Now let's make Bob stake 50 Wits at epoch 150 this time - let updated = tracker.add_stake(&bob, Wit::from_wits(10), 1_000).unwrap(); - assert_eq!( - updated, - &StakesEntry { - coins: Wit::from_wits(10), - epoch: 1_000, - exiting_coins: vec![], - } - ); - let (count, stats) = tracker.stats(); - assert_eq!(count, 2); - assert_eq!( - stats, - &StakingStats { - average: StakesEntry { - coins: Wit::from_wits(160), - epoch: 219, - exiting_coins: vec![], - }, - latest_epoch: 1_000, - } - ); - // Before Bob stakes, Alice has all the power and share - assert_eq!(tracker.query_power(&bob, 999), 0); - assert_eq!(tracker.query_share(&bob, 999), 0.0); - assert_eq!(tracker.query_share(&alice, 999), 1.0); - assert_eq!( - tracker.query_share(&alice, 999) + tracker.query_share(&bob, 999), - 1.0 - ); - // New stakes don't change power or share in the same epoch - assert_eq!(tracker.query_power(&bob, 1_000), 0); - assert_eq!(tracker.query_share(&bob, 1_000), 0.0); - assert_eq!(tracker.query_share(&alice, 1_000), 1.0); - assert_eq!( - tracker.query_share(&alice, 1_000) + tracker.query_share(&bob, 1_000), - 1.0 - ); - // Shortly as Bob's stake gains power, Alice loses a roughly equivalent share - assert_eq!(tracker.query_power(&bob, 1_100), 1_000); - assert_eq!(tracker.query_share(&bob, 1_100), 0.007094211123723042); - assert_eq!(tracker.query_share(&alice, 1_100), 0.9938989784335982); - assert_eq!( - tracker.query_share(&alice, 1_100) + tracker.query_share(&bob, 1_100), - 1.0009931895573212 - ); - // After enough time, both's shares should become proportional to their stake, and add up to 1.0 again - assert_eq!(tracker.query_power(&bob, 1_000_000), 537600); - assert_eq!(tracker.query_share(&bob, 1_000_000), 0.0625); - assert_eq!(tracker.query_share(&alice, 1_000_000), 0.9375); - assert_eq!( - tracker.query_share(&alice, 1_000_000) + tracker.query_share(&bob, 1_000_000), - 1.0 - ); - } +pub mod test { + use super::prelude::*; #[test] - fn test_minimum_stake() { - let mut tracker = StakesTracker::default(); - let alice = PublicKeyHash::from_bech32( - Environment::Mainnet, - "wit1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwrt3a4", - ) - .unwrap(); - let error = tracker - .add_stake( - &alice, - Wit::from_wits(MINIMUM_STAKEABLE_AMOUNT_WITS - 1), - 100, - ) - .unwrap_err(); - - assert_eq!( - error, - StakesTrackerError::AmountIsBelowMinimum { - amount: Wit::from_wits(MINIMUM_STAKEABLE_AMOUNT_WITS - 1), - minimum: Wit::from_wits(MINIMUM_STAKEABLE_AMOUNT_WITS) - } - ); - } - - #[test] - fn test_maximum_coin_age() { - let mut tracker = StakesTracker::default(); - let alice = PublicKeyHash::from_bech32( - Environment::Mainnet, - "wit1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwrt3a4", - ) - .unwrap(); - tracker - .add_stake(&alice, Wit::from_wits(MINIMUM_STAKEABLE_AMOUNT_WITS), 0) - .unwrap(); - assert_eq!(tracker.query_power(&alice, 0), 0); - assert_eq!( - tracker.query_power(&alice, 1), - MINIMUM_STAKEABLE_AMOUNT_WITS - ); - assert_eq!( - tracker.query_power(&alice, MAXIMUM_COIN_AGE_EPOCHS as Epoch - 1), - MINIMUM_STAKEABLE_AMOUNT_WITS * (MAXIMUM_COIN_AGE_EPOCHS - 1) - ); - assert_eq!( - tracker.query_power(&alice, MAXIMUM_COIN_AGE_EPOCHS as Epoch), - MINIMUM_STAKEABLE_AMOUNT_WITS * MAXIMUM_COIN_AGE_EPOCHS - ); - assert_eq!( - tracker.query_power(&alice, MAXIMUM_COIN_AGE_EPOCHS as Epoch + 1), - MINIMUM_STAKEABLE_AMOUNT_WITS * MAXIMUM_COIN_AGE_EPOCHS - ); - } - - #[test] - fn test_remove_stake() { - let mut tracker = StakesTracker::default(); - let alice = PublicKeyHash::from_bech32( - Environment::Mainnet, - "wit1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwrt3a4", - ) - .unwrap(); - let updated = tracker.add_stake(&alice, Wit::from_wits(100), 100).unwrap(); - assert_eq!( - updated, - &StakesEntry { - coins: Wit::from_wits(100), - epoch: 100, - exiting_coins: vec![], - } - ); - // Removing stake should reduce the amount, but keep the age the same - let updated = tracker.remove_stake(&alice, Wit::from_wits(50)).unwrap(); - assert_eq!( - updated, - Some(StakesEntry { - coins: Wit::from_wits(50), - epoch: 100, - exiting_coins: vec![], - }) - ); - } - - #[test] - fn test_use_stake() { - let mut tracker = StakesTracker::default(); - let alice = PublicKeyHash::from_bech32( - Environment::Mainnet, - "wit1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwrt3a4", - ) - .unwrap(); - let updated = tracker.add_stake(&alice, Wit::from_wits(100), 0).unwrap(); - assert_eq!( - updated, - &StakesEntry { - coins: Wit::from_wits(100), - epoch: 0, - exiting_coins: vec![], - } - ); - // After using all the stake, the amount should stay the same, but the epoch should be reset. - let updated = tracker.use_stake(&alice, Wit::from_wits(100), 100).unwrap(); - assert_eq!( - updated, - StakesEntry { - coins: Wit::from_wits(100), - epoch: 100, - exiting_coins: vec![], - } - ); - // But if we use half the stake, again the amount should stay the same, and the epoch should - // be updated to a point in the middle. - let updated = tracker.use_stake(&alice, Wit::from_wits(50), 200).unwrap(); - assert_eq!( - updated, - StakesEntry { - coins: Wit::from_wits(100), - epoch: 150, - exiting_coins: vec![], - } + fn test_e2e() { + let mut stakes = Stakes::::with_minimum(1); + + // Alpha stakes 2 @ epoch 0 + stakes.add_stake("Alpha", 2, 0).unwrap(); + + // Nobody holds any power just yet + let rank = stakes.rank(Capability::Mining, 0).collect::>(); + assert_eq!(rank, vec![("Alpha".into(), 0)]); + + // One epoch later, Alpha starts to hold power + let rank = stakes.rank(Capability::Mining, 1).collect::>(); + assert_eq!(rank, vec![("Alpha".into(), 2)]); + + // Beta stakes 5 @ epoch 10 + stakes.add_stake("Beta", 5, 10).unwrap(); + + // Alpha is still leading, but Beta has scheduled its takeover + let rank = stakes.rank(Capability::Mining, 10).collect::>(); + assert_eq!(rank, vec![("Alpha".into(), 20), ("Beta".into(), 0)]); + + // Beta eventually takes over after epoch 16 + let rank = stakes.rank(Capability::Mining, 16).collect::>(); + assert_eq!(rank, vec![("Alpha".into(), 32), ("Beta".into(), 30)]); + let rank = stakes.rank(Capability::Mining, 17).collect::>(); + assert_eq!(rank, vec![("Beta".into(), 35), ("Alpha".into(), 34)]); + + // Gamma should never take over, even in a million epochs, because it has only 1 coin + stakes.add_stake("Gamma", 1, 30).unwrap(); + let rank = stakes + .rank(Capability::Mining, 1_000_000) + .collect::>(); + assert_eq!( + rank, + vec![ + ("Beta".into(), 4_999_950), + ("Alpha".into(), 2_000_000), + ("Gamma".into(), 999_970) + ] + ); + + // But Delta is here to change it all + stakes.add_stake("Delta", 1_000, 50).unwrap(); + let rank = stakes.rank(Capability::Mining, 50).collect::>(); + assert_eq!( + rank, + vec![ + ("Beta".into(), 200), + ("Alpha".into(), 100), + ("Gamma".into(), 20), + ("Delta".into(), 0) + ] + ); + let rank = stakes.rank(Capability::Mining, 51).collect::>(); + assert_eq!( + rank, + vec![ + ("Delta".into(), 1_000), + ("Beta".into(), 205), + ("Alpha".into(), 102), + ("Gamma".into(), 21) + ] + ); + + // If Alpha removes all of its stake, it should immediately disappear + stakes.remove_stake("Alpha", 2).unwrap(); + let rank = stakes.rank(Capability::Mining, 51).collect::>(); + assert_eq!( + rank, + vec![ + ("Delta".into(), 1_000), + ("Beta".into(), 205), + ("Gamma".into(), 21), + ] ); } } diff --git a/data_structures/src/staking/simple.rs b/data_structures/src/staking/simple.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/data_structures/src/staking/stake.rs b/data_structures/src/staking/stake.rs new file mode 100644 index 0000000000..38fff6ae4c --- /dev/null +++ b/data_structures/src/staking/stake.rs @@ -0,0 +1,122 @@ +use std::marker::PhantomData; + +use super::prelude::*; + +/// A data structure that keeps track of a staker's staked coins and the epochs for different +/// capabilities. +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct Stake +where + Address: Default, + Epoch: Default, +{ + /// An amount of staked coins. + pub coins: Coins, + /// The average epoch used to derive coin age for different capabilities. + pub epochs: CapabilityMap, + // These two phantom fields are here just for the sake of specifying generics. + phantom_address: PhantomData
, + phantom_power: PhantomData, +} + +impl Stake +where + Address: Default, + Coins: Copy + + From + + PartialOrd + + num_traits::Zero + + std::ops::Add + + std::ops::Sub + + std::ops::Mul + + std::ops::Mul, + Epoch: Copy + Default + num_traits::Saturating + std::ops::Sub, + Power: std::ops::Add + + std::ops::Div + + std::ops::Div, +{ + /// Increase the amount of coins staked by a certain staker. + /// + /// When adding stake: + /// - Amounts are added together. + /// - Epochs are weight-averaged, using the amounts as the weight. + /// + /// This type of averaging makes the entry equivalent to an unbounded record of all stake + /// additions and removals, without the overhead in memory and computation. + pub fn add_stake( + &mut self, + coins: Coins, + epoch: Epoch, + minimum_stakeable: Option, + ) -> Result { + // Make sure that the amount to be staked is equal or greater than the minimum + let minimum = minimum_stakeable.unwrap_or(Coins::from(MINIMUM_STAKEABLE_AMOUNT_WITS)); + if coins < minimum { + Err(StakesError::AmountIsBelowMinimum { + amount: coins, + minimum, + })?; + } + + let coins_before = self.coins; + let epoch_before = self.epochs.get(Capability::Mining); + + let product_before = coins_before * epoch_before; + let product_added = coins * epoch; + + let coins_after = coins_before + coins; + let epoch_after = (product_before + product_added) / coins_after; + + self.coins = coins_after; + self.epochs.update_all(epoch_after); + + Ok(coins_after) + } + + /// Construct a Stake entry from a number of coins and a capability map. This is only useful for + /// tests. + #[cfg(test)] + pub fn from_parts(coins: Coins, epochs: CapabilityMap) -> Self { + Self { + coins, + epochs, + phantom_address: Default::default(), + phantom_power: Default::default(), + } + } + + /// Derives the power of an identity in the network on a certain epoch from an entry. Most + /// normally, the epoch is the current epoch. + pub fn power(&self, capability: Capability, current_epoch: Epoch) -> Power { + self.coins * (current_epoch.saturating_sub(self.epochs.get(capability))) + } + + /// Remove a certain amount of staked coins. + pub fn remove_stake( + &mut self, + coins: Coins, + minimum_stakeable: Option, + ) -> Result { + let coins_after = self.coins.sub(coins); + + if coins_after > Coins::zero() { + let minimum = minimum_stakeable.unwrap_or(Coins::from(MINIMUM_STAKEABLE_AMOUNT_WITS)); + + if coins_after < minimum { + Err(StakesError::AmountIsBelowMinimum { + amount: coins_after, + minimum, + })?; + } + } + + self.coins = coins_after; + + Ok(self.coins) + } + + /// Set the epoch for a certain capability. Most normally, the epoch is the current epoch. + pub fn reset_age(&mut self, capability: Capability, current_epoch: Epoch) { + self.epochs.update(capability, current_epoch); + } +} diff --git a/data_structures/src/staking/stakes.rs b/data_structures/src/staking/stakes.rs new file mode 100644 index 0000000000..13d0896d2b --- /dev/null +++ b/data_structures/src/staking/stakes.rs @@ -0,0 +1,466 @@ +use std::collections::btree_map::Entry; +use std::collections::BTreeMap; + +use itertools::Itertools; + +use super::prelude::*; + +/// The main data structure that provides the "stakes tracker" functionality. +/// +/// This structure holds indexes of stake entries. Because the entries themselves are reference +/// counted and write-locked, we can have as many indexes here as we need at a negligible cost. +#[derive(Default)] +pub struct Stakes +where + Address: Default, + Epoch: Default, +{ + /// A listing of all the stakers, indexed by their address. + by_address: BTreeMap>, + /// A listing of all the stakers, indexed by their coins and address. + /// + /// Because this uses a compound key to prevent duplicates, if we want to know which addresses + /// have staked a particular amount, we just need to run a range lookup on the tree. + by_coins: BTreeMap, SyncStake>, + /// The amount of coins that can be staked or can be left staked after unstaking. + minimum_stakeable: Option, +} + +impl Stakes +where + Address: Default, + Coins: Copy + + Default + + Ord + + From + + num_traits::Zero + + std::ops::Add + + std::ops::Sub + + std::ops::Mul + + std::ops::Mul, + Address: Clone + Ord + 'static, + Epoch: Copy + Default + num_traits::Saturating + std::ops::Sub, + Power: Copy + + Default + + Ord + + std::ops::Add + + std::ops::Div + + std::ops::Div + + 'static, +{ + /// Register a certain amount of additional stake for a certain address and epoch. + pub fn add_stake( + &mut self, + address: IA, + coins: Coins, + epoch: Epoch, + ) -> Result, Address, Coins, Epoch> + where + IA: Into
, + { + let address = address.into(); + let stake_arc = self.by_address.entry(address.clone()).or_default(); + + // Actually increase the number of coins + stake_arc + .write()? + .add_stake(coins, epoch, self.minimum_stakeable)?; + + // Update the position of the staker in the `by_coins` index + // If this staker was not indexed by coins, this will index it now + let key = CoinsAndAddress { + coins, + address: address.clone(), + }; + self.by_coins.remove(&key); + self.by_coins.insert(key, stake_arc.clone()); + + Ok(stake_arc.read()?.clone()) + } + + /// Obtain a list of stakers, conveniently ordered by one of several strategies. + /// + /// ## Strategies + /// + /// - `All`: retrieve all addresses, ordered by decreasing power. + /// - `StepBy`: retrieve every Nth address, ordered by decreasing power. + /// - `Take`: retrieve the most powerful N addresses, ordered by decreasing power. + /// - `Evenly`: retrieve a total of N addresses, evenly distributed from the index, ordered by + /// decreasing power. + pub fn census( + &self, + capability: Capability, + epoch: Epoch, + strategy: CensusStrategy, + ) -> Box> { + let iterator = self.rank(capability, epoch).map(|(address, _)| address); + + match strategy { + CensusStrategy::All => Box::new(iterator), + CensusStrategy::StepBy(step) => Box::new(iterator.step_by(step)), + CensusStrategy::Take(head) => Box::new(iterator.take(head)), + CensusStrategy::Evenly(count) => { + let collected = iterator.collect::>(); + let step = collected.len() / count; + + Box::new(collected.into_iter().step_by(step).take(count)) + } + } + } + + /// Tells what is the power of an identity in the network on a certain epoch. + pub fn query_power( + &self, + address: &Address, + capability: Capability, + epoch: Epoch, + ) -> Result { + Ok(self + .by_address + .get(address) + .ok_or(StakesError::IdentityNotFound { + identity: address.clone(), + })? + .read()? + .power(capability, epoch)) + } + + /// For a given capability, obtain the full list of stakers ordered by their power in that + /// capability. + pub fn rank( + &self, + capability: Capability, + current_epoch: Epoch, + ) -> impl Iterator + 'static { + self.by_coins + .iter() + .flat_map(move |(CoinsAndAddress { address, .. }, stake)| { + stake + .read() + .map(move |stake| (address.clone(), stake.power(capability, current_epoch))) + }) + .sorted_by_key(|(_, power)| *power) + .rev() + } + + /// Remove a certain amount of staked coins from a given identity at a given epoch. + pub fn remove_stake( + &mut self, + address: IA, + coins: Coins, + ) -> Result + where + IA: Into
, + { + let address = address.into(); + if let Entry::Occupied(mut by_address_entry) = self.by_address.entry(address.clone()) { + let (initial_coins, final_coins) = { + let mut stake = by_address_entry.get_mut().write()?; + + // Check the former amount of stake + let initial_coins = stake.coins; + + // Reduce the amount of stake + let final_coins = stake.remove_stake(coins, self.minimum_stakeable)?; + + (initial_coins, final_coins) + }; + + // No need to keep the entry if the stake has gone to zero + if final_coins.is_zero() { + by_address_entry.remove(); + self.by_coins.remove(&CoinsAndAddress { + coins: initial_coins, + address, + }); + } + + Ok(final_coins) + } else { + Err(StakesError::IdentityNotFound { identity: address }) + } + } + + /// Set the epoch for a certain address and capability. Most normally, the epoch is the current + /// epoch. + pub fn reset_age( + &mut self, + address: IA, + capability: Capability, + current_epoch: Epoch, + ) -> Result<(), Address, Coins, Epoch> + where + IA: Into
, + { + let address = address.into(); + let mut stake = self + .by_address + .get_mut(&address) + .ok_or(StakesError::IdentityNotFound { identity: address })? + .write()?; + stake.epochs.update(capability, current_epoch); + + Ok(()) + } + + /// Creates an instance of `Stakes` with a custom minimum stakeable amount. + pub fn with_minimum(minimum: Coins) -> Self { + Stakes { + minimum_stakeable: Some(minimum), + ..Default::default() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_stakes_initialization() { + let stakes = Stakes::::default(); + let ranking = stakes.rank(Capability::Mining, 0).collect::>(); + assert_eq!(ranking, Vec::default()); + } + + #[test] + fn test_add_stake() { + let mut stakes = Stakes::::with_minimum(5); + let alice = "Alice".into(); + let bob = "Bob".into(); + + // Let's check default power + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 0), + Err(StakesError::IdentityNotFound { + identity: alice.clone() + }) + ); + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 1_000), + Err(StakesError::IdentityNotFound { + identity: alice.clone() + }) + ); + + // Let's make Alice stake 100 Wit at epoch 100 + assert_eq!( + stakes.add_stake(&alice, 100, 100).unwrap(), + Stake::from_parts( + 100, + CapabilityMap { + mining: 100, + witnessing: 100 + } + ) + ); + + // Let's see how Alice's stake accrues power over time + assert_eq!(stakes.query_power(&alice, Capability::Mining, 99), Ok(0)); + assert_eq!(stakes.query_power(&alice, Capability::Mining, 100), Ok(0)); + assert_eq!(stakes.query_power(&alice, Capability::Mining, 101), Ok(100)); + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 200), + Ok(10_000) + ); + + // Let's make Alice stake 50 Wits at epoch 150 this time + assert_eq!( + stakes.add_stake(&alice, 50, 300).unwrap(), + Stake::from_parts( + 150, + CapabilityMap { + mining: 166, + witnessing: 166 + } + ) + ); + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 299), + Ok(19_950) + ); + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 300), + Ok(20_100) + ); + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 301), + Ok(20_250) + ); + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 400), + Ok(35_100) + ); + + // Now let's make Bob stake 500 Wits at epoch 1000 this time + assert_eq!( + stakes.add_stake(&bob, 500, 1_000).unwrap(), + Stake::from_parts( + 500, + CapabilityMap { + mining: 1_000, + witnessing: 1_000 + } + ) + ); + + // Before Bob stakes, Alice has all the power + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 999), + Ok(124950) + ); + assert_eq!(stakes.query_power(&bob, Capability::Mining, 999), Ok(0)); + + // New stakes don't change power in the same epoch + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 1_000), + Ok(125100) + ); + assert_eq!(stakes.query_power(&bob, Capability::Mining, 1_000), Ok(0)); + + // Shortly after, Bob's stake starts to gain power + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 1_001), + Ok(125250) + ); + assert_eq!(stakes.query_power(&bob, Capability::Mining, 1_001), Ok(500)); + + // After enough time, Bob overpowers Alice + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 2_000), + Ok(275_100) + ); + assert_eq!( + stakes.query_power(&bob, Capability::Mining, 2_000), + Ok(500_000) + ); + } + + #[test] + fn test_coin_age_resets() { + // First, lets create a setup with a few stakers + let mut stakes = Stakes::::with_minimum(5); + let alice = "Alice".into(); + let bob = "Bob".into(); + let charlie = "Charlie".into(); + + stakes.add_stake(&alice, 10, 0).unwrap(); + stakes.add_stake(&bob, 20, 20).unwrap(); + stakes.add_stake(&charlie, 30, 30).unwrap(); + + // Let's really start our test at epoch 100 + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 100), + Ok(1_000) + ); + assert_eq!(stakes.query_power(&bob, Capability::Mining, 100), Ok(1_600)); + assert_eq!( + stakes.query_power(&charlie, Capability::Mining, 100), + Ok(2_100) + ); + assert_eq!( + stakes.query_power(&alice, Capability::Witnessing, 100), + Ok(1_000) + ); + assert_eq!( + stakes.query_power(&bob, Capability::Witnessing, 100), + Ok(1_600) + ); + assert_eq!( + stakes.query_power(&charlie, Capability::Witnessing, 100), + Ok(2_100) + ); + assert_eq!( + stakes.rank(Capability::Mining, 100).collect::>(), + [ + (charlie.clone(), 2100), + (bob.clone(), 1600), + (alice.clone(), 1000) + ] + ); + assert_eq!( + stakes.rank(Capability::Witnessing, 100).collect::>(), + [ + (charlie.clone(), 2100), + (bob.clone(), 1600), + (alice.clone(), 1000) + ] + ); + + // Now let's slash Charlie's mining coin age right after + stakes.reset_age(&charlie, Capability::Mining, 101).unwrap(); + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 101), + Ok(1_010) + ); + assert_eq!(stakes.query_power(&bob, Capability::Mining, 101), Ok(1_620)); + assert_eq!(stakes.query_power(&charlie, Capability::Mining, 101), Ok(0)); + assert_eq!( + stakes.query_power(&alice, Capability::Witnessing, 101), + Ok(1_010) + ); + assert_eq!( + stakes.query_power(&bob, Capability::Witnessing, 101), + Ok(1_620) + ); + assert_eq!( + stakes.query_power(&charlie, Capability::Witnessing, 101), + Ok(2_130) + ); + assert_eq!( + stakes.rank(Capability::Mining, 101).collect::>(), + [ + (bob.clone(), 1_620), + (alice.clone(), 1_010), + (charlie.clone(), 0) + ] + ); + assert_eq!( + stakes.rank(Capability::Witnessing, 101).collect::>(), + [ + (charlie.clone(), 2_130), + (bob.clone(), 1_620), + (alice.clone(), 1_010) + ] + ); + + // Don't panic, Charlie! After enough time, you can take over again ;) + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 300), + Ok(3_000) + ); + assert_eq!(stakes.query_power(&bob, Capability::Mining, 300), Ok(5_600)); + assert_eq!( + stakes.query_power(&charlie, Capability::Mining, 300), + Ok(5_970) + ); + assert_eq!( + stakes.query_power(&alice, Capability::Witnessing, 300), + Ok(3_000) + ); + assert_eq!( + stakes.query_power(&bob, Capability::Witnessing, 300), + Ok(5_600) + ); + assert_eq!( + stakes.query_power(&charlie, Capability::Witnessing, 300), + Ok(8_100) + ); + assert_eq!( + stakes.rank(Capability::Mining, 300).collect::>(), + [ + (charlie.clone(), 5_970), + (bob.clone(), 5_600), + (alice.clone(), 3_000) + ] + ); + assert_eq!( + stakes.rank(Capability::Witnessing, 300).collect::>(), + [ + (charlie.clone(), 8_100), + (bob.clone(), 5_600), + (alice.clone(), 3_000) + ] + ); + } +} From 88a7f41ac0a92724d6b82727c6a1e05b681dbefc Mon Sep 17 00:00:00 2001 From: tommytrg Date: Mon, 9 Oct 2023 18:29:38 +0200 Subject: [PATCH 03/12] feat: add StakeTransaction (split commit) --- Cargo.lock | 19 +- data_structures/src/chain/mod.rs | 210 ++++++++++++++++++++- data_structures/src/error.rs | 24 +++ data_structures/src/superblock.rs | 3 + data_structures/src/transaction.rs | 95 ++++++++++ data_structures/src/transaction_factory.rs | 8 +- data_structures/src/types.rs | 1 + node/src/actors/chain_manager/mining.rs | 5 + schemas/witnet/witnet.proto | 19 ++ validations/Cargo.toml | 2 +- validations/src/tests/mod.rs | 106 ++++++++++- validations/src/validations.rs | 156 ++++++++++++++- wallet/src/model.rs | 2 + wallet/src/repository/wallet/mod.rs | 1 + wallet/src/types.rs | 5 +- 15 files changed, 637 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c918b6d2b0..98489c7c19 100755 --- 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/chain/mod.rs b/data_structures/src/chain/mod.rs index 8f905f8859..bc9e376563 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( @@ -502,9 +507,18 @@ 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() } + } impl BlockTransactions { @@ -517,6 +531,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 @@ -528,6 +543,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` @@ -559,6 +575,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), } } @@ -601,6 +622,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 } @@ -682,6 +708,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 +738,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), } } } @@ -1947,6 +1976,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 { @@ -2003,6 +2033,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 @@ -2023,6 +2055,14 @@ pub struct TransactionsPool { required_reward_collateral_ratio: u64, // Map for unconfirmed transactions unconfirmed_transactions: UnconfirmedTransactions, + // TODO: refactor to use Rc> or + // Arc> to prevent the current indirect lookup (having to + // first query the index for the hash, and then using the hash to find the actual data) + 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 { @@ -2039,17 +2079,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(), } } } @@ -2082,7 +2127,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() } @@ -2122,6 +2167,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. @@ -2138,12 +2184,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(); @@ -2157,7 +2207,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. @@ -2202,6 +2255,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(); @@ -2243,6 +2317,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) => Ok(self.st_contains(&tx_hash)), } } @@ -2335,6 +2410,29 @@ 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. @@ -2469,6 +2567,7 @@ impl TransactionsPool { for hash in hashes.iter() { self.vt_remove_inner(hash, false); self.dr_remove_inner(hash, false); + self.st_remove_inner(hash, false); } } } @@ -2544,6 +2643,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 stake 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) { @@ -2610,11 +2762,12 @@ 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 { + while self.total_transactions_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 + // The ratio of vt to dr in blocks is currently 1:4 + // TODO: What the criteria to delete st? It should be 1:8 #[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; @@ -2737,6 +2890,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_default() + .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: {:?}", @@ -2785,6 +2958,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: @@ -2803,6 +2985,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) @@ -2836,6 +3019,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, @@ -2877,6 +3061,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())) + }) }) } @@ -2902,6 +3091,9 @@ impl TransactionsPool { Transaction::DataRequest(_) => { let _x = self.dr_remove_inner(&hash, false); } + Transaction::Stake(_) => { + let _x = self.st_remove_inner(&hash, false); + } _ => continue, } @@ -2915,6 +3107,10 @@ impl TransactionsPool { v } + + pub fn total_transactions_weight (&self) -> u64 { + self.total_vt_weight + self.total_dr_weight + self.total_st_weight + } } /// Unspent output data structure (equivalent of Bitcoin's UTXO) @@ -3008,6 +3204,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 diff --git a/data_structures/src/error.rs b/data_structures/src/error.rs index 18e8073523..5c190877ad 100644 --- a/data_structures/src/error.rs +++ b/data_structures/src/error.rs @@ -288,6 +288,18 @@ pub enum TransactionError { max_weight: u32, dr_output: Box, }, + /// Stake amount below minimum + #[fail( + display = "The amount of coins in stake ({}) is less than the minimum allowed ({})", + min_stake, stake + )] + 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 {}", reward_collateral_ratio, required_reward_collateral_ratio @@ -411,6 +423,18 @@ pub enum BlockError { weight, max_weight )] TotalDataRequestWeightLimitExceeded { weight: u32, max_weight: u32 }, + /// 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 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/superblock.rs b/data_structures/src/superblock.rs index c627f84f73..07f430d66b 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 4c98820e65..5ac3054d16 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,77 @@ 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 { + /// 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 +801,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 +853,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 +868,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 31296b2676..5c835096d9 100644 --- a/data_structures/src/transaction_factory.rs +++ b/data_structures/src/transaction_factory.rs @@ -13,7 +13,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, @@ -583,6 +583,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/data_structures/src/types.rs b/data_structures/src/types.rs index fa6b0cd1bc..04370407e8 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 231bdfb9c4..e576327501 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 64b1b04e0b..d08283acb9 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; + optional 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/Cargo.toml b/validations/Cargo.toml index eb00b2011f..6cf51b7b3c 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 b14bb19c32..c1e27ae9a1 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,107 @@ 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 a stake tx with no inputs + let st_output = StakeOutput { + value: MIN_STAKE_NANOWITS + 1, + authorization: KeyedSignature::default(), + }; + + let st_body = StakeTransactionBody::new(Vec::new(), st_output, None); + let st_tx = StakeTransaction::new(st_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_below_min_stake() { + 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::StakeBelowMinimum { + 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 d49dc4c73b..770aa05bcb 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, @@ -50,6 +50,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 @@ -96,6 +100,31 @@ pub fn dr_transaction_fee( } } +/// Returns the fee of a stake transaction. +/// +/// The fee is the difference between the outputs and the inputs of the transaction. +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 + - &st_tx + .body + .change + .clone() + .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) @@ -373,8 +402,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(), @@ -1125,6 +1152,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 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::StakeBelowMinimum { + 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, @@ -1714,6 +1794,64 @@ 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 + let duplicate = block + .txns + .stake_txns + .iter() + .map(|stake_tx| &stake_tx.body.output.authorization.public_key) + .duplicates() + .next(); + + if let Some(duplicate) = duplicate { + return Err(BlockError::RepeatedStakeOperator { + pkh: duplicate.pkh(), + } + .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; + + let outputs = change.into_iter().collect_vec(); + update_utxo_diff(&mut utxo_diff, inputs, outputs, transaction.hash()); + + // Add new hash to merkle tree + st_mt.push(transaction.hash().into()); + + // 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(), @@ -1722,6 +1860,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 { @@ -1890,6 +2029,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()), } } @@ -2162,6 +2309,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 ec8ac954b5..35ef8bdba1 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 6e401c5861..215989653e 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 84c9a95131..63924f6469 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 4bd482dca7abec26f48a036ba15290761000823b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Mon, 30 Oct 2023 14:14:44 +0100 Subject: [PATCH 04/12] feat(data_structures): add transaction factories for `StakeTransaction` --- config/src/defaults.rs | 7 + .../examples/transactions_pool_overhead.rs | 2 +- data_structures/src/chain/mod.rs | 96 ++++++--- data_structures/src/data_request.rs | 6 +- data_structures/src/transaction.rs | 74 +++++-- data_structures/src/transaction_factory.rs | 196 +++++++++++++++++- data_structures/tests/inclusion_proofs.rs | 2 +- node/src/actors/chain_manager/mining.rs | 14 +- node/src/actors/json_rpc/api.rs | 2 +- validations/src/tests/mod.rs | 114 +++++----- validations/src/validations.rs | 45 ++-- wallet/src/repository/wallet/mod.rs | 2 +- wallet/src/types.rs | 2 +- 13 files changed, 412 insertions(+), 150 deletions(-) diff --git a/config/src/defaults.rs b/config/src/defaults.rs index f903904848..6d7d924822 100644 --- a/config/src/defaults.rs +++ b/config/src/defaults.rs @@ -486,6 +486,13 @@ pub const PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO: u64 = 125; // TODO: modify the value directly in ConsensusConstants pub const PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE: u32 = 13440; +/// Maximum weight units that a block can devote to `StakeTransaction`s. +pub const PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT: u32 = 10_000_000; + +/// Minimum amount of nanoWits that a `StakeTransaction` can add, and minimum amount that can be +/// left in stake by an `UnstakeTransaction`. +pub const PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS: u64 = 10_000_000_000_000; + /// Struct that will implement all the development defaults pub struct Development; diff --git a/data_structures/examples/transactions_pool_overhead.rs b/data_structures/examples/transactions_pool_overhead.rs index 5f82934443..478d114d2d 100644 --- a/data_structures/examples/transactions_pool_overhead.rs +++ b/data_structures/examples/transactions_pool_overhead.rs @@ -100,7 +100,7 @@ fn random_transaction() -> (Transaction, u64) { } else { let dr_output = random_dr_output(); Transaction::DataRequest(DRTransaction { - body: DRTransactionBody::new(inputs, outputs, dr_output), + body: DRTransactionBody::new(inputs, dr_output, outputs), signatures: vec![signature; num_inputs], }) }; diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index bc9e376563..81592e2940 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -518,7 +518,6 @@ impl Block { pub fn weight(&self) -> u32 { self.dr_weight() + self.vt_weight() + self.st_weight() } - } impl BlockTransactions { @@ -1370,6 +1369,18 @@ pub struct ValueTransferOutput { pub time_lock: u64, } +impl ValueTransferOutput { + #[inline] + pub fn value(&self) -> u64 { + self.value + } + + #[inline] + pub fn weight(&self) -> u32 { + OUTPUT_SIZE + } +} + /// Data request output transaction data structure #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash, Default)] #[protobuf_convert(pb = "witnet::DataRequestOutput")] @@ -1436,6 +1447,44 @@ impl DataRequestOutput { } } +#[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 { + #[inline] + pub fn weight(&self) -> u32 { + crate::transaction::STAKE_OUTPUT_WEIGHT + } +} + +pub enum Output { + DataRequest(DataRequestOutput), + Stake(StakeOutput), + ValueTransfer(ValueTransferOutput), +} + +impl Output { + pub fn value(&self) -> Result { + match self { + Output::DataRequest(output) => output.checked_total_value(), + Output::Stake(output) => Ok(output.value), + Output::ValueTransfer(output) => Ok(output.value), + } + } + + pub fn weight(&self) -> u32 { + match self { + Output::DataRequest(output) => output.weight(), + Output::Stake(output) => output.weight(), + Output::ValueTransfer(output) => output.weight(), + } + } +} + /// Information about the total supply #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SupplyInfo { @@ -2762,8 +2811,7 @@ 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_transactions_weight() > self.weight_limit - { + while self.total_transactions_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 1:4 @@ -3108,8 +3156,8 @@ impl TransactionsPool { v } - pub fn total_transactions_weight (&self) -> u64 { - self.total_vt_weight + self.total_dr_weight + self.total_st_weight + pub fn total_transactions_weight(&self) -> u64 { + self.total_vt_weight + self.total_dr_weight + self.total_st_weight } } @@ -4219,7 +4267,7 @@ pub fn transaction_example() -> Transaction { let outputs = vec![value_transfer_output]; Transaction::DataRequest(DRTransaction::new( - DRTransactionBody::new(inputs, outputs, data_request_output), + DRTransactionBody::new(inputs, data_request_output, outputs), keyed_signature, )) } @@ -4323,7 +4371,7 @@ mod tests { .iter() .map(|input| { DRTransaction::new( - DRTransactionBody::new(vec![*input], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![*input], DataRequestOutput::default(), vec![]), vec![], ) }) @@ -4797,14 +4845,14 @@ mod tests { ); let dr_1 = DRTransaction::new( - DRTransactionBody::new(vec![input], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![input], DataRequestOutput::default(), vec![]), vec![], ); let dr_2 = DRTransaction::new( DRTransactionBody::new( vec![input], - vec![ValueTransferOutput::default()], DataRequestOutput::default(), + vec![ValueTransferOutput::default()], ), vec![], ); @@ -4874,14 +4922,14 @@ mod tests { ); let dr_1 = DRTransaction::new( - DRTransactionBody::new(vec![input], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![input], DataRequestOutput::default(), vec![]), vec![], ); let dr_2 = DRTransaction::new( DRTransactionBody::new( vec![input2], - vec![ValueTransferOutput::default()], DataRequestOutput::default(), + vec![ValueTransferOutput::default()], ), vec![], ); @@ -4975,11 +5023,11 @@ mod tests { assert_ne!(input0, input1); let dr_1 = DRTransaction::new( - DRTransactionBody::new(vec![input0], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![input0], DataRequestOutput::default(), vec![]), vec![], ); let dr_2 = DRTransaction::new( - DRTransactionBody::new(vec![input0, input1], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![input0, input1], DataRequestOutput::default(), vec![]), vec![], ); @@ -5041,7 +5089,7 @@ mod tests { fn transactions_pool_malleability_dr() { let input = Input::default(); let mut dr_1 = DRTransaction::new( - DRTransactionBody::new(vec![input], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![input], DataRequestOutput::default(), vec![]), vec![KeyedSignature::default()], ); // Add dummy signature, but pretend it is valid @@ -5236,12 +5284,12 @@ mod tests { Transaction::DataRequest(DRTransaction::new( DRTransactionBody::new( vec![Input::default()], + DataRequestOutput::default(), vec![ValueTransferOutput { pkh: Default::default(), value: i, time_lock: 0, }], - DataRequestOutput::default(), ), vec![], )) @@ -5479,7 +5527,7 @@ mod tests { witnesses: 1, ..Default::default() }; - let drb = DRTransactionBody::new(vec![], vec![], dro); + let drb = DRTransactionBody::new(vec![], dro, vec![]); let drt = DRTransaction::new( drb, vec![KeyedSignature { @@ -5517,7 +5565,7 @@ mod tests { commit_and_reveal_fee: 501, ..Default::default() }; - let drb1 = DRTransactionBody::new(vec![], vec![], dro1); + let drb1 = DRTransactionBody::new(vec![], dro1, vec![]); let drt1 = DRTransaction::new( drb1, vec![KeyedSignature { @@ -5532,7 +5580,7 @@ mod tests { commit_and_reveal_fee: 100, ..Default::default() }; - let drb2 = DRTransactionBody::new(vec![], vec![], dro2); + let drb2 = DRTransactionBody::new(vec![], dro2, vec![]); let drt2 = DRTransaction::new( drb2, vec![KeyedSignature { @@ -5547,7 +5595,7 @@ mod tests { commit_and_reveal_fee: 500, ..Default::default() }; - let drb3 = DRTransactionBody::new(vec![], vec![], dro3); + let drb3 = DRTransactionBody::new(vec![], dro3, vec![]); let drt3 = DRTransaction::new( drb3, vec![KeyedSignature { @@ -5639,7 +5687,7 @@ mod tests { witnesses: 2, ..Default::default() }; - let drb = DRTransactionBody::new(vec![], vec![], dro); + let drb = DRTransactionBody::new(vec![], dro, vec![]); let drt = DRTransaction::new( drb, vec![KeyedSignature { @@ -5679,7 +5727,7 @@ mod tests { witnesses: 1, ..Default::default() }; - let drb = DRTransactionBody::new(vec![], vec![], dro); + let drb = DRTransactionBody::new(vec![], dro, vec![]); let drt = DRTransaction::new( drb, vec![KeyedSignature { @@ -5718,7 +5766,7 @@ mod tests { witnesses: 1, ..Default::default() }; - let drb = DRTransactionBody::new(vec![], vec![], dro); + let drb = DRTransactionBody::new(vec![], dro, vec![]); let drt = DRTransaction::new( drb, vec![KeyedSignature { @@ -5756,7 +5804,7 @@ mod tests { witnesses: 2, ..Default::default() }; - let drb = DRTransactionBody::new(vec![], vec![], dro); + let drb = DRTransactionBody::new(vec![], dro, vec![]); let drt = DRTransaction::new( drb, vec![KeyedSignature { @@ -5794,7 +5842,7 @@ mod tests { witnesses: 2, ..Default::default() }; - let drb = DRTransactionBody::new(vec![], vec![], dro); + let drb = DRTransactionBody::new(vec![], dro, vec![]); let drt = DRTransaction::new( drb, vec![KeyedSignature { diff --git a/data_structures/src/data_request.rs b/data_structures/src/data_request.rs index fc3823037c..4e74ad0b80 100644 --- a/data_structures/src/data_request.rs +++ b/data_structures/src/data_request.rs @@ -742,7 +742,7 @@ mod tests { ..DataRequestInfo::default() }; let dr_transaction = DRTransaction::new( - DRTransactionBody::new(vec![Input::default()], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![Input::default()], DataRequestOutput::default(), vec![]), vec![KeyedSignature::default()], ); let dr_pointer = dr_transaction.hash(); @@ -777,7 +777,7 @@ mod tests { ..DataRequestOutput::default() }; let dr_transaction = DRTransaction::new( - DRTransactionBody::new(vec![Input::default()], vec![], dr_output), + DRTransactionBody::new(vec![Input::default()], dr_output, vec![]), vec![KeyedSignature::default()], ); let dr_pointer = dr_transaction.hash(); @@ -812,7 +812,7 @@ mod tests { ..DataRequestOutput::default() }; let dr_transaction = DRTransaction::new( - DRTransactionBody::new(vec![Input::default()], vec![], dr_output), + DRTransactionBody::new(vec![Input::default()], dr_output, vec![]), vec![KeyedSignature::default()], ); let dr_pointer = dr_transaction.hash(); diff --git a/data_structures/src/transaction.rs b/data_structures/src/transaction.rs index 5ac3054d16..3d8826c1d5 100644 --- a/data_structures/src/transaction.rs +++ b/data_structures/src/transaction.rs @@ -8,8 +8,9 @@ use witnet_crypto::{hash::calculate_sha256, merkle::FullMerkleTree}; use crate::{ chain::{ Block, Bn256PublicKey, DataRequestOutput, Epoch, Hash, Hashable, Input, KeyedSignature, - PublicKeyHash, ValueTransferOutput, + PublicKeyHash, StakeOutput, ValueTransferOutput, }, + error::TransactionError, proto::{schema::witnet, ProtobufConvert}, vrf::DataRequestEligibilityClaim, }; @@ -18,7 +19,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 STAKE_OUTPUT_WEIGHT: u32 = 105; pub const COMMIT_WEIGHT: u32 = 400; pub const REVEAL_WEIGHT: u32 = 200; pub const TALLY_WEIGHT: u32 = 100; @@ -257,6 +258,14 @@ impl VTTransactionBody { } } + pub fn value(&self) -> u64 { + self.outputs + .iter() + .map(ValueTransferOutput::value) + .reduce(|acc, value| acc + value) + .unwrap_or_default() + } + /// Value Transfer transaction weight. It is calculated as: /// /// ```text @@ -383,8 +392,8 @@ impl DRTransactionBody { /// Creates a new data request transaction body. pub fn new( inputs: Vec, - outputs: Vec, dr_output: DataRequestOutput, + outputs: Vec, ) -> Self { DRTransactionBody { inputs, @@ -394,6 +403,18 @@ impl DRTransactionBody { } } + pub fn value(&self) -> Result { + let dr_value = self.dr_output.checked_total_value()?; + let change_value = self + .outputs + .iter() + .map(ValueTransferOutput::value) + .reduce(|acc, value| acc + value) + .unwrap_or_default(); + + Ok(dr_value + change_value) + } + /// Data Request Transaction weight. It is calculated as: /// /// ```text @@ -725,6 +746,31 @@ pub struct StakeTransactionBody { } impl StakeTransactionBody { + /// Construct a `StakeTransactionBody` from a list of inputs and one `StakeOutput`. + pub fn new( + inputs: Vec, + output: StakeOutput, + change: Option, + ) -> Self { + StakeTransactionBody { + inputs, + output, + change, + ..Default::default() + } + } + + pub fn value(&self) -> u64 { + let stake_value = self.output.value; + let change_value = &self + .change + .as_ref() + .map(ValueTransferOutput::value) + .unwrap_or_default(); + + stake_value + change_value + } + /// Stake transaction weight. It is calculated as: /// /// ```text @@ -742,23 +788,7 @@ impl StakeTransactionBody { 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, - } + .saturating_add(STAKE_OUTPUT_WEIGHT) } } @@ -1068,8 +1098,8 @@ mod tests { }; let dr_body = DRTransactionBody::new( vec![Input::default()], - vec![ValueTransferOutput::default()], dro.clone(), + vec![ValueTransferOutput::default()], ); let dr_tx = DRTransaction::new(dr_body, vec![KeyedSignature::default()]); let dr_weight = INPUT_SIZE + OUTPUT_SIZE + dro.weight(); @@ -1089,8 +1119,8 @@ mod tests { }; let dr_body = DRTransactionBody::new( vec![Input::default()], - vec![ValueTransferOutput::default()], dro.clone(), + vec![ValueTransferOutput::default()], ); let dr_tx = DRTransaction::new(dr_body, vec![KeyedSignature::default()]); let dr_weight = INPUT_SIZE + OUTPUT_SIZE + dro.weight(); diff --git a/data_structures/src/transaction_factory.rs b/data_structures/src/transaction_factory.rs index 5c835096d9..94acf3a672 100644 --- a/data_structures/src/transaction_factory.rs +++ b/data_structures/src/transaction_factory.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use crate::{ chain::{ - DataRequestOutput, Epoch, EpochConstants, Input, OutputPointer, PublicKeyHash, + DataRequestOutput, Epoch, EpochConstants, Input, OutputPointer, PublicKeyHash, StakeOutput, ValueTransferOutput, }, error::TransactionError, @@ -75,6 +75,23 @@ impl NodeBalance { } } +#[derive(Clone, Debug)] +pub enum TransactionOutputs { + DataRequest((DataRequestOutput, Option)), + Stake((StakeOutput, Option)), + ValueTransfer(Vec), +} + +impl From for Vec { + fn from(value: TransactionOutputs) -> Self { + match value { + TransactionOutputs::DataRequest((_, change)) => change.into_iter().collect(), + TransactionOutputs::Stake((_, change)) => change.into_iter().collect(), + TransactionOutputs::ValueTransfer(outputs) => outputs, + } + } +} + /// Abstraction that facilitates the creation of new transactions from a set of unspent outputs. /// Transaction factories are expected to operate on this trait so that their business logic /// can be applied on many heterogeneous data structures that may implement it. @@ -160,6 +177,116 @@ pub trait OutputsCollection { } } + /// Generic inputs/outputs builder: can be used to build any kind of transaction. + #[allow(clippy::too_many_arguments)] + fn generic_transaction_factory( + &mut self, + outputs: TransactionOutputs, + fee: Fee, + timestamp: u64, + block_number_limit: Option, + utxo_strategy: &UtxoSelectionStrategy, + max_weight: u32, + ) -> Result { + let output_value; + let mut current_weight; + let inputs = vec![Input::default()]; + + // For the first estimation: 1 input and 1 output more for the change address + match outputs.clone() { + TransactionOutputs::DataRequest((dr_output, change)) => { + let body = DRTransactionBody::new(inputs, dr_output, change.into_iter().collect()); + output_value = body.value()?; + current_weight = body.weight(); + } + TransactionOutputs::Stake((stake_output, change)) => { + let body = StakeTransactionBody::new(inputs, stake_output, change); + output_value = body.value(); + current_weight = body.weight(); + } + TransactionOutputs::ValueTransfer(outputs) => { + let body = VTTransactionBody::new(inputs, outputs); + output_value = body.value(); + current_weight = body.weight(); + } + }; + + match fee { + Fee::Absolute(absolute_fee) => { + let amount = output_value + .checked_add(absolute_fee.as_nanowits()) + .ok_or(TransactionError::FeeOverflow)?; + + let inputs = + self.take_enough_utxos(amount, timestamp, block_number_limit, utxo_strategy)?; + + Ok(TransactionInfo { + fee: absolute_fee, + inputs, + output_value, + outputs: outputs.into(), + }) + } + Fee::Relative(priority) => { + let max_iterations = 1 + ((max_weight - current_weight) / INPUT_SIZE); + for _i in 0..max_iterations { + let absolute_fee = priority.into_absolute(current_weight); + let amount = output_value + .checked_add(absolute_fee.as_nanowits()) + .ok_or(TransactionError::FeeOverflow)?; + + let collected_outputs = self.take_enough_utxos( + amount, + timestamp, + block_number_limit, + utxo_strategy, + )?; + let inputs = collected_outputs + .pointers + .iter() + .cloned() + .map(Input::new) + .collect(); + + let new_weight = match outputs.clone() { + TransactionOutputs::DataRequest((dr_output, change)) => { + let body = DRTransactionBody::new( + inputs, + dr_output, + change.into_iter().collect(), + ); + + body.weight() + } + TransactionOutputs::Stake((stake_output, change)) => { + let body = StakeTransactionBody::new(inputs, stake_output, change); + + body.weight() + } + TransactionOutputs::ValueTransfer(outputs) => { + let body = VTTransactionBody::new(inputs, outputs); + + body.weight() + } + }; + + if new_weight == current_weight { + return Ok(TransactionInfo { + fee: absolute_fee, + inputs: collected_outputs, + output_value, + outputs: outputs.into(), + }); + } else { + current_weight = new_weight; + } + } + + unreachable!("Unexpected exit in build_inputs_outputs method"); + } + } + } + /// Generic inputs/outputs builder: can be used to build /// value transfer transactions and data request transactions. #[allow(clippy::too_many_arguments)] @@ -254,7 +381,7 @@ pub fn calculate_weight( let outputs = vec![ValueTransferOutput::default(); outputs_count]; let weight = if let Some(dr_output) = dro { - let drt = DRTransactionBody::new(inputs, outputs, dr_output.clone()); + let drt = DRTransactionBody::new(inputs, dr_output.clone(), outputs); let dr_weight = drt.weight(); if dr_weight > max_weight { return Err(TransactionError::DataRequestWeightLimitExceeded { @@ -431,8 +558,8 @@ pub fn build_drt( Ok(DRTransactionBody::new( used_pointers.collect::>(), - outputs, dr_output, + outputs, )) } @@ -583,10 +710,65 @@ pub fn transaction_outputs_sum(outputs: &[ValueTransferOutput]) -> Result Result { - // TODO: add stake transaction factory logic here - !unimplemented!() +/// Build stake transaction from existing UTXOs without a need to specify inputs or change. +#[allow(clippy::too_many_arguments)] +pub fn build_st( + output: StakeOutput, + fee: Fee, + own_utxos: &mut OwnUnspentOutputsPool, + own_pkh: PublicKeyHash, + all_utxos: &UnspentOutputsPool, + timestamp: u64, + tx_pending_timeout: u64, + utxo_strategy: &UtxoSelectionStrategy, + max_weight: u32, + dry_run: bool, +) -> Result { + let mut utxos = NodeUtxos { + all_utxos, + own_utxos, + pkh: own_pkh, + }; + + let tx_info = utxos.generic_transaction_factory( + TransactionOutputs::Stake((output.clone(), None)), + fee, + timestamp, + None, + utxo_strategy, + max_weight, + )?; + + let used_pointers = tx_info.inputs.pointers.iter().cloned().map(Input::new); + + // Mark UTXOs as used so we don't double spend + // Save the timestamp after which the transaction will be considered timed out + // and the output will become available for spending it again + if !dry_run { + utxos.set_used_output_pointer(used_pointers.clone(), timestamp + tx_pending_timeout); + } + + // Only use a change output if there is value inserted by inputs that is not consumed by outputs + // or fees + let change_value = tx_info + .inputs + .total_value + .wrapping_sub(tx_info.output_value) + .wrapping_sub(tx_info.fee.as_nanowits()); + let change = if change_value > 0 { + Some(ValueTransferOutput { + pkh: own_pkh, + value: change_value, + time_lock: 0, + }) + } else { + None + }; + + let inputs = used_pointers.collect::>(); + let body = StakeTransactionBody::new(inputs, output, change); + + Ok(body) } #[cfg(test)] diff --git a/data_structures/tests/inclusion_proofs.rs b/data_structures/tests/inclusion_proofs.rs index bf8c975b13..e3f490d8e5 100644 --- a/data_structures/tests/inclusion_proofs.rs +++ b/data_structures/tests/inclusion_proofs.rs @@ -40,7 +40,7 @@ fn example_dr(id: usize) -> DRTransaction { witness_reward: id as u64, ..Default::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dr_output); + let dr_body = DRTransactionBody::new(vec![], dr_output, vec![]); DRTransaction::new(dr_body, vec![]) } diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index e576327501..481485eff7 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -951,7 +951,7 @@ pub fn build_block( witnesses: 1, ..DataRequestOutput::default() }; - let min_dr_weight = DRTransactionBody::new(vec![Input::default()], vec![], dro).weight(); + let min_dr_weight = DRTransactionBody::new(vec![Input::default()], dro, vec![]).weight(); for dr_tx in transactions_pool.dr_iter() { let transaction_weight = dr_tx.weight(); let transaction_fee = match dr_transaction_fee(dr_tx, &utxo_diff, epoch, epoch_constants) { @@ -1451,9 +1451,9 @@ mod tests { let mut dr3 = dr1.clone(); dr3.witnesses = 3; - let dr_body_one_output1 = DRTransactionBody::new(input.clone(), vec![], dr1); - let dr_body_one_output2 = DRTransactionBody::new(input.clone(), vec![], dr2); - let dr_body_one_output3 = DRTransactionBody::new(input, vec![], dr3); + let dr_body_one_output1 = DRTransactionBody::new(input.clone(), dr1, vec![]); + let dr_body_one_output2 = DRTransactionBody::new(input.clone(), dr2, vec![]); + let dr_body_one_output3 = DRTransactionBody::new(input, dr3, vec![]); // Build sample transactions let dr_tx1 = DRTransaction::new(dr_body_one_output1, vec![]); @@ -1547,9 +1547,9 @@ mod tests { let mut dr3 = dr1.clone(); dr3.commit_and_reveal_fee = 3; - let dr_body_one_output1 = DRTransactionBody::new(input.clone(), vec![], dr1); - let dr_body_one_output2 = DRTransactionBody::new(input.clone(), vec![], dr2); - let dr_body_one_output3 = DRTransactionBody::new(input, vec![], dr3); + let dr_body_one_output1 = DRTransactionBody::new(input.clone(), dr1, vec![]); + let dr_body_one_output2 = DRTransactionBody::new(input.clone(), dr2, vec![]); + let dr_body_one_output3 = DRTransactionBody::new(input, dr3, vec![]); // Build sample transactions let dr_tx1 = DRTransaction::new(dr_body_one_output1, vec![]); diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index d7adf886a1..9976796f8c 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -2187,7 +2187,7 @@ mod tests { let inputs = vec![value_transfer_input]; Transaction::DataRequest(DRTransaction::new( - DRTransactionBody::new(inputs, vec![], data_request_output), + DRTransactionBody::new(inputs, data_request_output, vec![]), keyed_signatures, )) } diff --git a/validations/src/tests/mod.rs b/validations/src/tests/mod.rs index c1e27ae9a1..1c43c19715 100644 --- a/validations/src/tests/mod.rs +++ b/validations/src/tests/mod.rs @@ -6,7 +6,10 @@ use std::{ use itertools::Itertools; -use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; +use witnet_config::defaults::{ + PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS, + PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO, +}; use witnet_crypto::{ secp256k1::{PublicKey as Secp256k1_PublicKey, SecretKey as Secp256k1_SecretKey}, signature::sign, @@ -47,8 +50,7 @@ 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 MIN_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS; const REQUIRED_REWARD_COLLATERAL_RATIO: u64 = PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; @@ -1453,7 +1455,7 @@ fn data_request_no_inputs() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![], dr_output, vec![]); let dr_transaction = DRTransaction::new(dr_tx_body, vec![]); let x = validate_dr_transaction( &dr_transaction, @@ -1489,7 +1491,7 @@ fn data_request_no_inputs_but_one_signature() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); let x = validate_dr_transaction( @@ -1534,7 +1536,7 @@ fn data_request_one_input_but_no_signature() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let dr_transaction = DRTransaction::new(dr_tx_body, vec![]); @@ -1579,7 +1581,7 @@ fn data_request_one_input_signatures() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); test_signature_empty_wrong_bad(dr_tx_body, |dr_tx_body, drs| { let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -1625,7 +1627,7 @@ fn data_request_input_double_spend() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![vti; 2], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti; 2], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs; 2]); let x = validate_dr_transaction( @@ -1665,7 +1667,7 @@ fn data_request_input_not_in_utxo() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); let x = validate_dr_transaction( @@ -1710,7 +1712,7 @@ fn data_request_input_not_enough_value() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); let x = validate_dr_transaction( @@ -1779,7 +1781,7 @@ fn data_request_output_value_overflow() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![vti0, vti1], vec![vto0, vto1], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti0, vti1], dr_output, vec![vto0, vto1]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs; 2]); let x = validate_dr_transaction( @@ -1815,7 +1817,7 @@ fn test_drtx(dr_output: DataRequestOutput) -> Result<(), failure::Error> { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2213,7 +2215,7 @@ fn data_request_http_post_before_wip_activation() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2281,7 +2283,7 @@ fn data_request_http_get_with_headers_before_wip_activation() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2339,7 +2341,7 @@ fn data_request_parse_xml_before_wip_activation() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2393,7 +2395,7 @@ fn data_request_parse_xml_after_wip_activation() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2429,8 +2431,8 @@ fn dr_validation_weight_limit_exceeded() { let dr_body = DRTransactionBody::new( vec![Input::default()], - vec![ValueTransferOutput::default()], dro.clone(), + vec![ValueTransferOutput::default()], ); let dr_tx = DRTransaction::new(dr_body, vec![]); let dr_weight = dr_tx.weight(); @@ -2520,7 +2522,7 @@ fn data_request_miner_fee() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2571,7 +2573,7 @@ fn data_request_miner_fee_with_change() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![change_output], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![change_output]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2622,7 +2624,7 @@ fn data_request_change_to_different_pkh() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![change_output], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![change_output]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2683,7 +2685,7 @@ fn data_request_two_change_outputs() { let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); let dr_tx_body = - DRTransactionBody::new(vec![vti], vec![change_output_1, change_output_2], dr_output); + DRTransactionBody::new(vec![vti], dr_output, vec![change_output_1, change_output_2]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2736,7 +2738,7 @@ fn data_request_miner_fee_with_too_much_change() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![change_output], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![change_output]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2784,7 +2786,7 @@ fn data_request_zero_value_output() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![change_output], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![change_output]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2831,7 +2833,7 @@ fn data_request_reward_collateral_ratio_wip() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2902,7 +2904,7 @@ fn data_request_reward_collateral_ratio_limit() { let block_number = 0; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -2933,7 +2935,7 @@ fn data_request_reward_collateral_ratio_limit() { ..DataRequestOutput::default() }; - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dr_output); + let dr_tx_body = DRTransactionBody::new(vec![vti], dr_output, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -3014,7 +3016,7 @@ fn test_commit_with_dr_and_utxo_set( collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3074,7 +3076,7 @@ fn test_commit_difficult_proof() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3161,7 +3163,7 @@ fn test_commit_with_collateral( collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3324,7 +3326,7 @@ fn commitment_no_signature() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3421,7 +3423,7 @@ fn commitment_invalid_proof() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_epoch = 0; @@ -3488,7 +3490,7 @@ fn commitment_dr_in_reveal_stage() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3859,7 +3861,7 @@ fn commitment_collateral_zero_is_minimum() { collateral: 0, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3948,7 +3950,7 @@ fn commitment_timelock() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -4045,7 +4047,7 @@ fn dr_pool_with_dr_in_reveal_stage() -> (DataRequestPool, Hash) { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_pointer = dr_transaction.hash(); @@ -4164,7 +4166,7 @@ fn reveal_dr_in_commit_stage() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_pointer = dr_transaction.hash(); @@ -4295,7 +4297,7 @@ fn reveal_valid_commitment() { ..DataRequestOutput::default() }; let dr_transaction = DRTransaction { - body: DRTransactionBody::new(vec![], vec![], dr_output), + body: DRTransactionBody::new(vec![], dr_output, vec![]), signatures: vec![KeyedSignature::default()], }; let dr_pointer = dr_transaction.hash(); @@ -4580,7 +4582,7 @@ fn dr_pool_with_dr_in_tally_all_errors( // Create DRTransaction let epoch = 0; let dr_transaction = DRTransaction { - body: DRTransactionBody::new(vec![], vec![], dr_output.clone()), + body: DRTransactionBody::new(vec![], dr_output.clone(), vec![]), signatures: vec![KeyedSignature { signature: Default::default(), public_key: dr_public_key.clone(), @@ -4693,7 +4695,7 @@ fn dr_pool_with_dr_in_tally_stage_generic( // Create DRTransaction let epoch = 0; let dr_transaction = DRTransaction { - body: DRTransactionBody::new(vec![], vec![], dr_output.clone()), + body: DRTransactionBody::new(vec![], dr_output.clone(), vec![]), signatures: vec![KeyedSignature { signature: Default::default(), public_key: dr_public_key.clone(), @@ -4910,7 +4912,7 @@ fn tally_dr_not_tally_stage() { data_request: example_data_request(), collateral: DEFAULT_COLLATERAL, }; - let dr_transaction_body = DRTransactionBody::new(vec![], vec![], dr_output.clone()); + let dr_transaction_body = DRTransactionBody::new(vec![], dr_output.clone(), vec![]); let dr_transaction_signature = sign_tx(PRIV_KEY_2, &dr_transaction_body); let dr_transaction = DRTransaction::new(dr_transaction_body, vec![dr_transaction_signature]); let dr_pointer = dr_transaction.hash(); @@ -5208,7 +5210,7 @@ fn generic_tally_test_inner( // Create DRTransaction let epoch = 0; let dr_transaction = DRTransaction { - body: DRTransactionBody::new(vec![], vec![], dr_output), + body: DRTransactionBody::new(vec![], dr_output, vec![]), signatures: vec![KeyedSignature { signature: Default::default(), public_key: dr_public_key.clone(), @@ -8463,7 +8465,7 @@ fn st_no_inputs() { authorization: KeyedSignature::default(), }; - let st_body = StakeTransactionBody::new(Vec::new(), st_output, None); + let st_body = StakeTransactionBody::new(vec![], st_output, None); let st_tx = StakeTransaction::new(st_body, vec![]); let x = validate_stake_transaction( &st_tx, @@ -9202,7 +9204,7 @@ fn block_duplicated_commits() { data_request: example_data_request(), collateral: DEFAULT_COLLATERAL, }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -9295,7 +9297,7 @@ fn block_duplicated_reveals() { data_request: example_data_request(), collateral: DEFAULT_COLLATERAL, }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -9464,7 +9466,7 @@ fn block_before_and_after_hard_fork() { data_request: example_data_request_before_wip19(), collateral: DEFAULT_COLLATERAL, }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro.clone()); + let dr_body = DRTransactionBody::new(vec![], dro.clone(), vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_epoch = 0; @@ -9479,7 +9481,7 @@ fn block_before_and_after_hard_fork() { }; let utxo_set = build_utxo_set_with_mint(vec![vto], None, vec![]); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dro); + let dr_tx_body = DRTransactionBody::new(vec![vti], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -10027,7 +10029,7 @@ fn block_add_drt() { }; let output1_pointer = ONE_WIT_OUTPUT.parse().unwrap(); let dr_tx_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![vto0], dr_output); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dr_output, vec![vto0]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -10063,7 +10065,7 @@ fn block_add_2_drt_same_input() { }; let output1_pointer = ONE_WIT_OUTPUT.parse().unwrap(); let dr_tx_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![vto0], dr_output); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dr_output, vec![vto0]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_tx1 = DRTransaction::new(dr_tx_body, vec![drs]); @@ -10083,7 +10085,7 @@ fn block_add_2_drt_same_input() { }; let output1_pointer = ONE_WIT_OUTPUT.parse().unwrap(); let dr_tx_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![vto0], dr_output); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dr_output, vec![vto0]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_tx2 = DRTransaction::new(dr_tx_body, vec![drs]); @@ -10124,7 +10126,7 @@ fn block_add_1_drt_and_1_vtt_same_input() { }; let output1_pointer = ONE_WIT_OUTPUT.parse().unwrap(); let dr_tx_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![vto0], dr_output); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dr_output, vec![vto0]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_tx = DRTransaction::new(dr_tx_body, vec![drs]); @@ -10610,7 +10612,7 @@ fn validate_commit_transactions_included_in_utxo_diff() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -11002,12 +11004,12 @@ fn validate_dr_weight_overflow() { let dr_value = dro.checked_total_value().unwrap(); let dr_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![], dro.clone()); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dro.clone(), vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_tx = DRTransaction::new(dr_body, vec![drs]); assert_eq!(dr_tx.weight(), 1589); - let dr_body2 = DRTransactionBody::new(vec![Input::new(output2_pointer)], vec![], dro); + let dr_body2 = DRTransactionBody::new(vec![Input::new(output2_pointer)], dro, vec![]); let drs2 = sign_tx(PRIV_KEY_1, &dr_body2); let dr_tx2 = DRTransaction::new(dr_body2, vec![drs2]); assert_eq!(dr_tx2.weight(), 1589); @@ -11044,7 +11046,7 @@ fn validate_dr_weight_overflow_126_witnesses() { let dr_value = dro.checked_total_value().unwrap(); let dr_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![], dro.clone()); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dro.clone(), vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_tx = DRTransaction::new(dr_body, vec![drs]); @@ -11083,12 +11085,12 @@ fn validate_dr_weight_valid() { let dr_value = dro.checked_total_value().unwrap(); let dr_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![], dro.clone()); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dro.clone(), vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_tx = DRTransaction::new(dr_body, vec![drs]); assert_eq!(dr_tx.weight(), 1589); - let dr_body2 = DRTransactionBody::new(vec![Input::new(output2_pointer)], vec![], dro); + let dr_body2 = DRTransactionBody::new(vec![Input::new(output2_pointer)], dro, vec![]); let drs2 = sign_tx(PRIV_KEY_1, &dr_body2); let dr_tx2 = DRTransaction::new(dr_body2, vec![drs2]); assert_eq!(dr_tx2.weight(), 1589); diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 770aa05bcb..b9e95f37f4 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -15,6 +15,7 @@ use witnet_crypto::{ merkle::{merkle_tree_root as crypto_merkle_tree_root, ProgressiveMerkleTree}, signature::{verify, PublicKey, Signature}, }; +use witnet_data_structures::chain::StakeOutput; use witnet_data_structures::{ chain::{ tapi::ActiveWips, Block, BlockMerkleRoots, CheckpointBeacon, CheckpointVRF, @@ -30,8 +31,8 @@ use witnet_data_structures::{ error::{BlockError, DataRequestError, TransactionError}, radon_report::{RadonReport, ReportContext}, transaction::{ - CommitTransaction, DRTransaction, MintTransaction, RevealTransaction, StakeOutput, - StakeTransaction, TallyTransaction, Transaction, VTTransaction, + CommitTransaction, DRTransaction, MintTransaction, RevealTransaction, StakeTransaction, + TallyTransaction, Transaction, VTTransaction, }, transaction_factory::{transaction_inputs_sum, transaction_outputs_sum}, types::visitor::Visitor, @@ -110,13 +111,7 @@ 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 - - &st_tx - .body - .change - .clone() - .unwrap_or(Default::default()) - .value; + let out_value = st_tx.body.output.value; if out_value > in_value { Err(TransactionError::NegativeFee.into()) @@ -1152,6 +1147,15 @@ pub fn validate_tally_transaction<'a>( Ok((ta_tx.outputs.iter().collect(), tally_extra_fee)) } +/// A type alias for the very complex return type of `fn validate_stake_transaction`. +pub type ValidatedStakeTransaction<'a> = ( + Vec<&'a Input>, + &'a StakeOutput, + u64, + u32, + &'a Option, +); + /// Function to validate a stake transaction. pub fn validate_stake_transaction<'a>( st_tx: &'a StakeTransaction, @@ -1159,23 +1163,13 @@ pub fn validate_stake_transaction<'a>( epoch: Epoch, epoch_constants: EpochConstants, signatures_to_verify: &mut Vec, -) -> Result< - ( - Vec<&'a Input>, - &'a StakeOutput, - u64, - u32, - &'a Option, - ), - failure::Error, -> { +) -> Result, failure::Error> { // 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::StakeBelowMinimum { + Err(TransactionError::StakeBelowMinimum { min_stake: MIN_STAKE_NANOWITS, stake: st_tx.body.output.value, - } - .into()); + })?; } validate_transaction_signature( @@ -1188,10 +1182,9 @@ pub fn validate_stake_transaction<'a>( // A stake transaction must have at least one input if st_tx.body.inputs.is_empty() { - return Err(TransactionError::NoInputs { + Err(TransactionError::NoInputs { tx_hash: st_tx.hash(), - } - .into()); + })?; } let fee = st_transaction_fee(st_tx, utxo_diff, epoch, epoch_constants)?; @@ -1836,7 +1829,7 @@ pub fn validate_block_transactions( } st_weight = acc_weight; - let outputs = change.into_iter().collect_vec(); + let outputs = change.iter().collect_vec(); update_utxo_diff(&mut utxo_diff, inputs, outputs, transaction.hash()); // Add new hash to merkle tree diff --git a/wallet/src/repository/wallet/mod.rs b/wallet/src/repository/wallet/mod.rs index 215989653e..afbc7c872b 100644 --- a/wallet/src/repository/wallet/mod.rs +++ b/wallet/src/repository/wallet/mod.rs @@ -1144,7 +1144,7 @@ where .map(Input::new) .collect_vec(); - let body = DRTransactionBody::new(pointers_as_inputs.clone(), outputs, request); + let body = DRTransactionBody::new(pointers_as_inputs.clone(), request, outputs); let sign_data = body.hash(); let signatures = self.create_signatures_from_inputs(pointers_as_inputs, sign_data, &mut state); diff --git a/wallet/src/types.rs b/wallet/src/types.rs index 63924f6469..8dd1d59d27 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -416,7 +416,7 @@ impl From for DRTransactionBodyHelper { impl From for DRTransactionBody { fn from(x: DRTransactionBodyHelper) -> Self { - DRTransactionBody::new(x.inputs, x.outputs, x.dr_output) + DRTransactionBody::new(x.inputs, x.dr_output, x.outputs) } } From 3e566908678d9ff0fe1dbf41ba2487dbc40a20f5 Mon Sep 17 00:00:00 2001 From: tommytrg Date: Fri, 20 Oct 2023 12:18:54 +0200 Subject: [PATCH 05/12] feat(data_structures): add support for `UnstakeTransaction` --- data_structures/src/chain/mod.rs | 170 +++++++++++++++++++-- data_structures/src/error.rs | 34 ++++- data_structures/src/superblock.rs | 3 + data_structures/src/transaction.rs | 79 ++++++++++ data_structures/src/transaction_factory.rs | 30 +++- data_structures/src/types.rs | 1 + node/src/actors/chain_manager/mining.rs | 5 + schemas/witnet/witnet.proto | 14 ++ validations/src/validations.rs | 140 ++++++++++++++++- wallet/src/repository/wallet/mod.rs | 1 + wallet/src/types.rs | 10 +- 11 files changed, 465 insertions(+), 22 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index 81592e2940..d0f8530cfc 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), } } } @@ -2026,6 +2053,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 { @@ -2084,6 +2112,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 @@ -2109,9 +2139,14 @@ pub struct TransactionsPool { // first query the index for the hash, and then using the hash to find the actual data) 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 { @@ -2129,6 +2164,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 @@ -2137,6 +2173,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 @@ -2144,6 +2182,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(), } } } @@ -2217,6 +2257,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. @@ -2234,15 +2275,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(); @@ -2257,9 +2302,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. @@ -2325,6 +2373,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(); @@ -2366,7 +2435,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)), } } @@ -2482,6 +2552,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. @@ -2726,13 +2820,12 @@ impl TransactionsPool { transaction } - /// Remove a stake transaction from the pool but do not remove other transactions that - /// may try to spend the same UTXOs. + /// Remove a stake 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)| { @@ -2745,6 +2838,21 @@ impl TransactionsPool { }) } + /// Remove an unstake transaction from the pool but do not remove other transactions that + /// may try to spend the same UTXOs, because this kind of transactions spend no UTXOs. + /// This should be used to remove transactions that did not get included in a consolidated + /// block. + fn ut_remove_inner(&mut self, key: &Hash) -> Option { + self.ut_transactions + .remove(key) + .map(|(weight, transaction)| { + self.sorted_ut_index.remove(&(weight, *key)); + self.total_ut_weight -= u64::from(transaction.weight()); + + 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) { @@ -2958,6 +3066,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: {:?}", @@ -3015,6 +3144,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: @@ -3109,11 +3247,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())) }) } @@ -3142,6 +3285,9 @@ impl TransactionsPool { Transaction::Stake(_) => { let _x = self.st_remove_inner(&hash, false); } + Transaction::Unstake(_) => { + let _x = self.ut_remove_inner(&hash); + } _ => continue, } @@ -3254,6 +3400,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 5c190877ad..ee2471dbf5 100644 --- a/data_structures/src/error.rs +++ b/data_structures/src/error.rs @@ -294,12 +294,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 @@ -429,6 +451,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 07f430d66b..46f8d17063 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 3d8826c1d5..e300ddfc98 100644 --- a/data_structures/src/transaction.rs +++ b/data_structures/src/transaction.rs @@ -20,6 +20,7 @@ use crate::{ pub const INPUT_SIZE: u32 = 133; pub const OUTPUT_SIZE: u32 = 36; pub const STAKE_OUTPUT_WEIGHT: u32 = 105; +pub const UNSTAKE_TRANSACTION_WEIGHT: u32 = 153; pub const COMMIT_WEIGHT: u32 = 400; pub const REVEAL_WEIGHT: u32 = 200; pub const TALLY_WEIGHT: u32 = 100; @@ -133,6 +134,7 @@ pub enum Transaction { Tally(TallyTransaction), Mint(MintTransaction), Stake(StakeTransaction), + Unstake(UnstakeTransaction), } impl From for Transaction { @@ -177,6 +179,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 @@ -792,6 +800,62 @@ impl StakeTransactionBody { } } +#[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, + + #[protobuf_convert(skip)] + #[serde(skip)] + hash: MemoHash, +} + +impl UnstakeTransactionBody { + /// Creates a new stake transaction body. + pub fn new(operator: PublicKeyHash, withdrawal: ValueTransferOutput) -> Self { + UnstakeTransactionBody { + operator, + withdrawal, + ..Default::default() + } + } + + pub fn value(&self) -> u64 { + self.withdrawal.value + } + + /// 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() @@ -840,6 +904,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(); @@ -888,6 +961,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 { @@ -899,6 +977,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/transaction_factory.rs b/data_structures/src/transaction_factory.rs index 94acf3a672..1731e1d23f 100644 --- a/data_structures/src/transaction_factory.rs +++ b/data_structures/src/transaction_factory.rs @@ -6,6 +6,7 @@ use std::{ use serde::{Deserialize, Serialize}; +use crate::transaction::UnstakeTransactionBody; use crate::{ chain::{ DataRequestOutput, Epoch, EpochConstants, Input, OutputPointer, PublicKeyHash, StakeOutput, @@ -21,7 +22,7 @@ use crate::{ wit::Wit, }; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct CollectedOutputs { pub pointers: Vec, pub resolved: Vec, @@ -79,6 +80,7 @@ impl NodeBalance { pub enum TransactionOutputs { DataRequest((DataRequestOutput, Option)), Stake((StakeOutput, Option)), + Unstake(ValueTransferOutput), ValueTransfer(Vec), } @@ -87,6 +89,7 @@ impl From for Vec { match value { TransactionOutputs::DataRequest((_, change)) => change.into_iter().collect(), TransactionOutputs::Stake((_, change)) => change.into_iter().collect(), + TransactionOutputs::Unstake(output) => vec![output], TransactionOutputs::ValueTransfer(outputs) => outputs, } } @@ -204,6 +207,11 @@ pub trait OutputsCollection { output_value = body.value(); current_weight = body.weight(); } + TransactionOutputs::Unstake(withdrawal) => { + let body = UnstakeTransactionBody::new(Default::default(), withdrawal); + output_value = body.value(); + current_weight = body.weight(); + } TransactionOutputs::ValueTransfer(outputs) => { let body = VTTransactionBody::new(inputs, outputs); output_value = body.value(); @@ -217,8 +225,12 @@ pub trait OutputsCollection { .checked_add(absolute_fee.as_nanowits()) .ok_or(TransactionError::FeeOverflow)?; - let inputs = - self.take_enough_utxos(amount, timestamp, block_number_limit, utxo_strategy)?; + // Avoid collecting UTXOs for unstake transactions, which use no inputs + let inputs = if let &TransactionOutputs::Unstake(_) = &outputs { + Default::default() + } else { + self.take_enough_utxos(amount, timestamp, block_number_limit, utxo_strategy)? + }; Ok(TransactionInfo { fee: absolute_fee, @@ -228,9 +240,18 @@ pub trait OutputsCollection { }) } Fee::Relative(priority) => { + let absolute_fee = priority.into_absolute(current_weight); + if let TransactionOutputs::Unstake(withdrawal) = outputs { + return Ok(TransactionInfo { + fee: absolute_fee, + inputs: Default::default(), + output_value, + outputs: vec![withdrawal], + }); + } + let max_iterations = 1 + ((max_weight - current_weight) / INPUT_SIZE); for _i in 0..max_iterations { - let absolute_fee = priority.into_absolute(current_weight); let amount = output_value .checked_add(absolute_fee.as_nanowits()) .ok_or(TransactionError::FeeOverflow)?; @@ -268,6 +289,7 @@ pub trait OutputsCollection { body.weight() } + _ => unreachable!(), }; if new_weight == current_weight { diff --git a/data_structures/src/types.rs b/data_structures/src/types.rs index 04370407e8..6feda901e1 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 481485eff7..1263987b5e 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 d08283acb9..237e472ae9 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 b9e95f37f4..509370439c 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -15,14 +15,13 @@ use witnet_crypto::{ merkle::{merkle_tree_root as crypto_merkle_tree_root, ProgressiveMerkleTree}, signature::{verify, PublicKey, Signature}, }; -use witnet_data_structures::chain::StakeOutput; use witnet_data_structures::{ chain::{ tapi::ActiveWips, Block, BlockMerkleRoots, CheckpointBeacon, CheckpointVRF, ConsensusConstants, DataRequestOutput, DataRequestStage, DataRequestState, Epoch, EpochConstants, Hash, Hashable, Input, KeyedSignature, OutputPointer, PublicKeyHash, RADRequest, RADTally, RADType, Reputation, ReputationEngine, SignaturesToVerify, - ValueTransferOutput, + StakeOutput, ValueTransferOutput, }, data_request::{ calculate_reward_collateral_ratio, calculate_tally_change, calculate_witness_reward, @@ -32,7 +31,7 @@ use witnet_data_structures::{ radon_report::{RadonReport, ReportContext}, transaction::{ CommitTransaction, DRTransaction, MintTransaction, RevealTransaction, StakeTransaction, - TallyTransaction, Transaction, VTTransaction, + TallyTransaction, Transaction, UnstakeTransaction, VTTransaction, }, transaction_factory::{transaction_inputs_sum, transaction_outputs_sum}, types::visitor::Visitor, @@ -54,6 +53,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. /// @@ -120,6 +121,24 @@ 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 { + // TODO: take in_value from stakes tracker + let in_value = 0; + let out_value = ut_tx.body.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) @@ -1198,6 +1217,82 @@ 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), failure::Error> { + // Check if is unstaking more than the total stake + // FIXME: actually query the stakes tracker for staked value + 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)) +} + +/// 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( + ut_tx: &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, @@ -1845,6 +1940,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) = + 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(), @@ -1854,6 +1986,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 { @@ -2303,6 +2436,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 afbc7c872b..3a3a5af520 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 8dd1d59d27..3f8e73a29b 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) + } } } } From 38d8a4edb14d2d436ef80778d67e8ab1257b4da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Mon, 20 Nov 2023 21:15:05 +0100 Subject: [PATCH 06/12] feat(data_structures): allow backwards-compatibility of key data structures --- data_structures/src/chain/mod.rs | 35 +++++++++- data_structures/src/proto/mod.rs | 1 + data_structures/src/superblock.rs | 96 +++++++++++++++++++++++++--- data_structures/src/vrf.rs | 2 +- data_structures/tests/serializers.rs | 95 +++++++++++++++++++++++++-- node/src/actors/chain_manager/mod.rs | 5 ++ schemas/witnet/witnet.proto | 64 +++++++++++++++++-- 7 files changed, 275 insertions(+), 23 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index d0f8530cfc..fd06513c57 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -4446,6 +4446,7 @@ mod tests { }; use crate::{ + proto::versioning::{ProtocolVersion, VersionedHashable}, superblock::{mining_build_superblock, ARSIdentities}, transaction::{CommitTransactionBody, RevealTransactionBody, VTTransactionBody}, }; @@ -4554,7 +4555,22 @@ mod tests { fn test_block_hashable_trait() { let block = block_example(); let expected = "70e15ac70bb00f49c7a593b2423f722dca187bbae53dc2f22647063b17608c01"; - assert_eq!(block.hash().to_string(), expected); + assert_eq!( + block.versioned_hash(ProtocolVersion::Legacy).to_string(), + expected + ); + let expected = "29ef68357a5c861b9dbe043d351a28472ca450edcda25de4c9b80a4560a28c0f"; + assert_eq!( + block + .versioned_hash(ProtocolVersion::Transition) + .to_string(), + expected + ); + let expected = "29ef68357a5c861b9dbe043d351a28472ca450edcda25de4c9b80a4560a28c0f"; + assert_eq!( + block.versioned_hash(ProtocolVersion::Final).to_string(), + expected + ); } #[test] @@ -6626,6 +6642,7 @@ mod tests { 1, Hash::default(), 1, + ProtocolVersion::Legacy, ); let expected_indices = vec![0, 2, 2]; @@ -6680,6 +6697,7 @@ mod tests { 1, Hash::default(), 1, + ProtocolVersion::Legacy, ); let expected_indices = vec![0, 2, 2, 8, 10, 6, 4, 6]; @@ -6715,6 +6733,7 @@ mod tests { 1, Hash::default(), 1, + ProtocolVersion::Legacy, ); let result = sb.dr_proof_of_inclusion(&[b1, b2], &dr_txs[2]); @@ -6725,7 +6744,14 @@ mod tests { fn test_dr_merkle_root_no_block() { let dr_txs = build_test_dr_txs(3); - let sb = mining_build_superblock(&[], &[Hash::default()], 1, Hash::default(), 1); + let sb = mining_build_superblock( + &[], + &[Hash::default()], + 1, + Hash::default(), + 1, + ProtocolVersion::Legacy, + ); let result = sb.dr_proof_of_inclusion(&[], &dr_txs[2]); assert!(result.is_none()); @@ -6751,6 +6777,7 @@ mod tests { 1, Hash::default(), 1, + ProtocolVersion::Legacy, ); let expected_indices = vec![0, 2]; @@ -6789,6 +6816,7 @@ mod tests { 1, Hash::default(), 1, + ProtocolVersion::Legacy, ); let expected_indices = vec![0, 2, 2]; @@ -6851,6 +6879,7 @@ mod tests { 1, Hash::default(), 1, + ProtocolVersion::Legacy, ); let expected_indices = vec![0, 2, 2, 8, 10, 6, 4, 6]; @@ -6886,6 +6915,7 @@ mod tests { 1, Hash::default(), 1, + ProtocolVersion::Legacy, ); let result = sb.tally_proof_of_inclusion(&[b1, b2], &tally_txs[2]); @@ -6917,6 +6947,7 @@ mod tests { 1, Hash::default(), 1, + ProtocolVersion::Legacy, ); let expected_indices = vec![0, 2, 2]; diff --git a/data_structures/src/proto/mod.rs b/data_structures/src/proto/mod.rs index 4d681d8b96..6fc6ba0e5d 100644 --- a/data_structures/src/proto/mod.rs +++ b/data_structures/src/proto/mod.rs @@ -8,6 +8,7 @@ use std::convert::TryFrom; use std::fmt::Debug; pub mod schema; +pub mod versioning; /// Used for establishing correspondence between rust struct /// and protobuf rust struct diff --git a/data_structures/src/superblock.rs b/data_structures/src/superblock.rs index 46f8d17063..bfc46ae477 100644 --- a/data_structures/src/superblock.rs +++ b/data_structures/src/superblock.rs @@ -13,6 +13,7 @@ use std::{ use serde::{Deserialize, Serialize}; +use crate::proto::versioning::{ProtocolVersion, VersionedHashable}; use witnet_crypto::{ hash::{calculate_sha256, Sha256}, merkle::merkle_tree_root as crypto_merkle_tree_root, @@ -417,6 +418,7 @@ impl SuperBlockState { alt_keys: &AltKeys, sync_superblock: Option, block_epoch: Epoch, + protocol_version: ProtocolVersion, ) -> SuperBlock { let key_leaves = hash_key_leaves(&ars_identities.get_rep_ordered_bn256_list(alt_keys)); @@ -468,13 +470,14 @@ impl SuperBlockState { superblock_index, last_block_in_previous_superblock, self.signing_committee.len() as u32, + ProtocolVersion::Legacy, ) }; // update the superblock_beacon self.current_superblock_beacon = CheckpointBeacon { checkpoint: superblock_index, - hash_prev_block: superblock.hash(), + hash_prev_block: superblock.versioned_hash(protocol_version), }; let old_votes = self.votes_mempool.clear_and_remove_votes(); @@ -673,6 +676,7 @@ pub fn mining_build_superblock( index: u32, last_block_in_previous_superblock: Hash, signing_committee_length: u32, + protocol_version: ProtocolVersion, ) -> SuperBlock { let last_block = block_headers.last(); match last_block { @@ -698,7 +702,7 @@ pub fn mining_build_superblock( ) } Some(last_block_header) => { - let last_block_hash = last_block_header.hash(); + let last_block_hash = last_block_header.versioned_hash(protocol_version); let merkle_drs: Vec = block_headers .iter() .map(|b| b.merkle_roots.dr_hash_merkle_root) @@ -711,7 +715,13 @@ pub fn mining_build_superblock( let ars_root = hash_merkle_tree_root(ars_ordered_hash_leaves); let blocks: Vec<_> = block_headers .iter() - .map(|b| format!("#{}: {}", b.beacon.checkpoint, b.hash())) + .map(|b| { + format!( + "#{}: {}", + b.beacon.checkpoint, + b.versioned_hash(protocol_version) + ) + }) .collect(); log::trace!( "Created superblock #{} with hash_prev_block {}, ARS {}, signing_committee_length: {}, blocks {:?}", @@ -765,7 +775,8 @@ mod tests { #[test] fn test_superblock_creation_no_blocks() { let default_hash = Hash::default(); - let superblock = mining_build_superblock(&[], &[], 0, default_hash, 0); + let superblock = + mining_build_superblock(&[], &[], 0, default_hash, 0, ProtocolVersion::Legacy); let expected = SuperBlock::new( 0, @@ -818,12 +829,19 @@ mod tests { default_hash, dr_merkle_root_1, 0, - block.hash(), + block.versioned_hash(ProtocolVersion::Legacy), default_hash, tally_merkle_root_1, ); - let superblock = mining_build_superblock(&[block], &[default_hash], 0, default_hash, 1); + let superblock = mining_build_superblock( + &[block], + &[default_hash], + 0, + default_hash, + 1, + ProtocolVersion::Legacy, + ); assert_eq!(superblock, expected_superblock); } @@ -886,13 +904,19 @@ mod tests { default_hash, expected_superblock_dr_root, 0, - block_2.hash(), + block_2.versioned_hash(ProtocolVersion::Legacy), default_hash, expected_superblock_tally_root, ); - let superblock = - mining_build_superblock(&[block_1, block_2], &[default_hash], 0, default_hash, 1); + let superblock = mining_build_superblock( + &[block_1, block_2], + &[default_hash], + 0, + default_hash, + 1, + ProtocolVersion::Legacy, + ); assert_eq!(superblock, expected_superblock); } @@ -953,6 +977,7 @@ mod tests { &AltKeys::default(), None, 1, + ProtocolVersion::Legacy, ); let mut v0 = SuperBlockVote::new_unsigned(sb1.hash(), 1); @@ -1007,6 +1032,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let v1 = SuperBlockVote::new_unsigned(sb1.hash(), 0); assert_eq!( @@ -1037,6 +1063,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let expected_superblock = SuperBlock::new( @@ -1091,6 +1118,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let expected_second_superblock = SuperBlock::new( @@ -1114,7 +1142,8 @@ mod tests { genesis_hash, &alt_keys, None, - 1 + 1, + ProtocolVersion::Legacy, ), expected_second_superblock ); @@ -1159,6 +1188,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // After building a new superblock the cache is invalidated but the previous ARS is still empty assert_eq!( @@ -1176,6 +1206,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let v1 = SuperBlockVote::new_unsigned(Hash::SHA256([2; 32]), 1); assert_eq!( @@ -1220,6 +1251,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // The ARS included in superblock 0 is empty, so none of the superblock votes for index 1 @@ -1233,6 +1265,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // The ARS included in superblock 1 contains only identity p1, so only its vote will be @@ -1246,6 +1279,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let mut v1 = SuperBlockVote::new_unsigned(sb2.hash(), 2); v1.secp256k1_signature.public_key = p1.clone(); @@ -1282,6 +1316,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // The ARS included in superblock 0 is empty, so none of the superblock votes for index 1 @@ -1295,6 +1330,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let mut v2 = SuperBlockVote::new_unsigned(Hash::SHA256([2; 32]), 2); @@ -1312,6 +1348,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let mut v1 = SuperBlockVote::new_unsigned(sb2.hash(), 2); v1.secp256k1_signature.public_key = p1; @@ -1345,6 +1382,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // The ARS included in superblock 0 is empty, so none of the superblock votes for index 1 @@ -1358,6 +1396,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // The ARS included in superblock 1 contains only identity p1, so only its vote will be @@ -1371,6 +1410,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let mut v1 = SuperBlockVote::new_unsigned(sb2.hash(), 2); v1.secp256k1_signature.public_key = p1.clone(); @@ -1408,6 +1448,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // The ARS included in superblock 0 is empty, so none of the superblock votes for index 1 @@ -1421,6 +1462,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // The ARS included in superblock 1 contains only identity p1, so only its vote will be @@ -1434,6 +1476,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let mut v1 = SuperBlockVote::new_unsigned(sb2.hash(), 2); v1.secp256k1_signature.public_key = p1; @@ -1500,6 +1543,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let (v1, v2, v3) = create_votes(sb0.hash(), 0); assert_eq!( @@ -1526,6 +1570,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let (v1, v2, v3) = create_votes(sb1.hash(), 1); assert_eq!( @@ -1552,6 +1597,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let (v1, v2, v3) = create_votes(sb2.hash(), 2); assert_eq!(sbs.add_vote(&v1, 2), AddSuperBlockVote::ValidWithSameHash); @@ -1575,6 +1621,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let (v1, v2, v3) = create_votes(sb3.hash(), 3); assert_eq!( @@ -1598,6 +1645,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let (v1, v2, v3) = create_votes(sb4.hash(), 4); assert_eq!( @@ -1656,6 +1704,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let (v1, v2, v3, v4) = create_votes(sb0.hash(), 0); assert_eq!( @@ -1687,6 +1736,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let (v1, _v2, v3, v4) = create_votes(sb1.hash(), 1); let mut v2 = SuperBlockVote::new_unsigned( @@ -1716,6 +1766,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let mut v1 = SuperBlockVote::new_unsigned(sb2.hash(), 5); @@ -1783,6 +1834,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let (v1, v2, v3, v4) = create_votes(sb0.hash(), 0); assert_eq!( @@ -1812,6 +1864,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let (v1, v2, v3, v4) = create_votes(sb1.hash(), 1); assert_eq!(sbs.add_vote(&v1, 1), AddSuperBlockVote::ValidWithSameHash); @@ -1848,6 +1901,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let (v1, v2, v3, v4) = create_votes(sb2.hash(), 2); assert_eq!(sbs.add_vote(&v1, 2), AddSuperBlockVote::ValidWithSameHash); @@ -1898,6 +1952,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let expected_sb2 = mining_build_superblock( @@ -1906,6 +1961,7 @@ mod tests { 1, genesis_hash, 3, + ProtocolVersion::Legacy, ); let sb2_hash = expected_sb2.hash(); @@ -1926,6 +1982,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); assert_eq!(sb2, expected_sb2); let mut hh: HashMap<_, Vec<_>> = HashMap::new(); @@ -1951,6 +2008,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // votes_on_each_superblock are cleared when the local superblock changes assert_eq!(sbs.votes_mempool.get_valid_votes(), HashMap::new()); @@ -1987,6 +2045,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // superblock with index 1 let sb2 = sbs.build_superblock( @@ -1998,6 +2057,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let expected_sb2 = mining_build_superblock( @@ -2006,6 +2066,7 @@ mod tests { 1, genesis_hash, 2, + ProtocolVersion::Legacy, ); assert_eq!(sb2, expected_sb2); @@ -2063,6 +2124,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); assert_eq!(sbs.add_vote(&v0, 9), AddSuperBlockVote::MaybeValid); @@ -2092,6 +2154,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let _sb2 = sbs.build_superblock( @@ -2103,6 +2166,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); assert_eq!( @@ -2147,6 +2211,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // superblock with index 1 let sb2 = sbs.build_superblock( @@ -2158,6 +2223,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); let expected_sb2 = mining_build_superblock( @@ -2166,6 +2232,7 @@ mod tests { 1, genesis_hash, 2, + ProtocolVersion::Legacy, ); assert_eq!(sb2, expected_sb2); @@ -2227,6 +2294,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); sbs.ars_previous_identities = ars_identities.clone(); let committee_size = 4; @@ -2272,6 +2340,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // Signing committee size of 2 has been included @@ -2286,6 +2355,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // SB2_A is different to SB1 and a signing committee size of 3 has been included @@ -2301,6 +2371,7 @@ mod tests { &alt_keys, Some(sb1.clone()), 1, + ProtocolVersion::Legacy, ); // SB2_B is equal to SB1 and a signing committee size of 2 has been included @@ -2346,6 +2417,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); sbs.ars_previous_identities = ars_identities; let committee_size = 4; @@ -2391,6 +2463,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); sbs.ars_previous_identities = ars_identities.clone(); let committee_size = 2; @@ -2652,6 +2725,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // The ARS included in superblock 0 is empty, so none of the superblock votes for index 1 @@ -2665,6 +2739,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // The ARS included in superblock 1 contains only identity p1, so only its vote will be @@ -2678,6 +2753,7 @@ mod tests { &alt_keys, None, 1, + ProtocolVersion::Legacy, ); // 2 valid votes and 3 missing votes -> Unknown diff --git a/data_structures/src/vrf.rs b/data_structures/src/vrf.rs index 57ff40c324..0475945349 100644 --- a/data_structures/src/vrf.rs +++ b/data_structures/src/vrf.rs @@ -122,7 +122,7 @@ impl VrfMessage { /// Block mining eligibility claim #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Default, Hash)] -#[protobuf_convert(pb = "witnet::Block_BlockEligibilityClaim")] +#[protobuf_convert(pb = "witnet::BlockEligibilityClaim")] pub struct BlockEligibilityClaim { /// A Verifiable Random Function proof of the eligibility for a given epoch and public key pub proof: VrfProof, diff --git a/data_structures/tests/serializers.rs b/data_structures/tests/serializers.rs index 2b4ef28c70..28f5bfde7f 100644 --- a/data_structures/tests/serializers.rs +++ b/data_structures/tests/serializers.rs @@ -1,9 +1,12 @@ use witnet_data_structures::{ - proto::ProtobufConvert, + proto::{ + versioning::{ProtocolVersion, Versioned}, + ProtobufConvert, + }, {chain::*, types::*}, }; -const EXAMPLE_BLOCK_VECTOR: &[u8] = &[ +const EXAMPLE_BLOCK_VECTOR_LEGACY: &[u8] = &[ 8, 1, 18, 165, 5, 42, 162, 5, 10, 172, 2, 18, 36, 18, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 216, 1, 10, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -32,6 +35,68 @@ const EXAMPLE_BLOCK_VECTOR: &[u8] = &[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ]; +const EXAMPLE_BLOCK_VECTOR_TRANSITION: &[u8] = &[ + 8, 1, 18, 237, 5, 42, 234, 5, 10, 244, 2, 18, 36, 18, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 160, 2, 10, 34, 10, 32, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 18, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 26, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 34, 10, 32, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 58, + 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 66, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 39, 10, 37, 18, 35, 10, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 0, 18, 41, 10, 2, 10, 0, + 18, 35, 10, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 26, 197, 2, 10, 0, 26, 192, 2, 10, 146, 2, 10, 38, 10, 36, 10, 34, 10, 32, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 18, 24, 10, 22, 10, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 205, 1, + 10, 202, 1, 18, 97, 8, 1, 18, 93, 104, 116, 116, 112, 115, 58, 47, 47, 111, 112, 101, 110, 119, + 101, 97, 116, 104, 101, 114, 109, 97, 112, 46, 111, 114, 103, 47, 100, 97, 116, 97, 47, 50, 46, + 53, 47, 119, 101, 97, 116, 104, 101, 114, 63, 105, 100, 61, 50, 57, 53, 48, 49, 53, 57, 38, 97, + 112, 112, 105, 100, 61, 98, 54, 57, 48, 55, 100, 50, 56, 57, 101, 49, 48, 100, 55, 49, 52, 97, + 54, 101, 56, 56, 98, 51, 48, 55, 54, 49, 102, 97, 101, 50, 50, 18, 97, 8, 1, 18, 93, 104, 116, + 116, 112, 115, 58, 47, 47, 111, 112, 101, 110, 119, 101, 97, 116, 104, 101, 114, 109, 97, 112, + 46, 111, 114, 103, 47, 100, 97, 116, 97, 47, 50, 46, 53, 47, 119, 101, 97, 116, 104, 101, 114, + 63, 105, 100, 61, 50, 57, 53, 48, 49, 53, 57, 38, 97, 112, 112, 105, 100, 61, 98, 54, 57, 48, + 55, 100, 50, 56, 57, 101, 49, 48, 100, 55, 49, 52, 97, 54, 101, 56, 56, 98, 51, 48, 55, 54, 49, + 102, 97, 101, 50, 50, 26, 0, 34, 0, 18, 41, 10, 2, 10, 0, 18, 35, 10, 33, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + +const EXAMPLE_BLOCK_VECTOR_FINAL: &[u8] = &[ + 8, 1, 18, 237, 5, 42, 234, 5, 10, 244, 2, 18, 36, 18, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 160, 2, 10, 34, 10, 32, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 18, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 26, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 34, 10, 32, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 58, + 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 66, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 39, 10, 37, 18, 35, 10, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 0, 18, 41, 10, 2, 10, 0, + 18, 35, 10, 33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 26, 197, 2, 10, 0, 26, 192, 2, 10, 146, 2, 10, 38, 10, 36, 10, 34, 10, 32, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 18, 24, 10, 22, 10, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26, 205, 1, + 10, 202, 1, 18, 97, 8, 1, 18, 93, 104, 116, 116, 112, 115, 58, 47, 47, 111, 112, 101, 110, 119, + 101, 97, 116, 104, 101, 114, 109, 97, 112, 46, 111, 114, 103, 47, 100, 97, 116, 97, 47, 50, 46, + 53, 47, 119, 101, 97, 116, 104, 101, 114, 63, 105, 100, 61, 50, 57, 53, 48, 49, 53, 57, 38, 97, + 112, 112, 105, 100, 61, 98, 54, 57, 48, 55, 100, 50, 56, 57, 101, 49, 48, 100, 55, 49, 52, 97, + 54, 101, 56, 56, 98, 51, 48, 55, 54, 49, 102, 97, 101, 50, 50, 18, 97, 8, 1, 18, 93, 104, 116, + 116, 112, 115, 58, 47, 47, 111, 112, 101, 110, 119, 101, 97, 116, 104, 101, 114, 109, 97, 112, + 46, 111, 114, 103, 47, 100, 97, 116, 97, 47, 50, 46, 53, 47, 119, 101, 97, 116, 104, 101, 114, + 63, 105, 100, 61, 50, 57, 53, 48, 49, 53, 57, 38, 97, 112, 112, 105, 100, 61, 98, 54, 57, 48, + 55, 100, 50, 56, 57, 101, 49, 48, 100, 55, 49, 52, 97, 54, 101, 56, 56, 98, 51, 48, 55, 54, 49, + 102, 97, 101, 50, 50, 26, 0, 34, 0, 18, 41, 10, 2, 10, 0, 18, 35, 10, 33, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]; + #[test] fn message_last_beacon_from_bytes() { let highest_superblock_checkpoint = CheckpointBeacon { @@ -352,8 +417,18 @@ fn message_block_to_bytes() { magic: 1, }; - let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR.to_vec(); - let result: Vec = msg.to_pb_bytes().unwrap(); + let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR_LEGACY.to_vec(); + let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::Legacy).unwrap(); + assert_eq!(result, expected_buf); + + let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR_TRANSITION.to_vec(); + let result: Vec = msg + .to_versioned_pb_bytes(ProtocolVersion::Transition) + .unwrap(); + assert_eq!(result, expected_buf); + + let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR_FINAL.to_vec(); + let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::Final).unwrap(); assert_eq!(result, expected_buf); } @@ -365,7 +440,17 @@ fn message_block_from_bytes() { }; assert_eq!( - Message::from_pb_bytes(EXAMPLE_BLOCK_VECTOR).unwrap(), + Message::from_versioned_pb_bytes(EXAMPLE_BLOCK_VECTOR_LEGACY).unwrap(), + expected_msg + ); + + assert_eq!( + Message::from_versioned_pb_bytes(EXAMPLE_BLOCK_VECTOR_TRANSITION).unwrap(), + expected_msg + ); + + assert_eq!( + Message::from_versioned_pb_bytes(EXAMPLE_BLOCK_VECTOR_FINAL).unwrap(), expected_msg ); } diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index d8fbb7e484..d8b0328ceb 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -47,6 +47,7 @@ use futures::future::{try_join_all, FutureExt}; use glob::glob; use itertools::Itertools; use rand::Rng; + use witnet_config::{ config::Tapi, defaults::{ @@ -69,6 +70,7 @@ use witnet_data_structures::{ }, data_request::DataRequestPool, get_environment, + proto::versioning::ProtocolVersion, radon_report::{RadonReport, ReportContext}, superblock::{ARSIdentities, AddSuperBlockVote, SuperBlockConsensus}, transaction::{RevealTransaction, TallyTransaction, Transaction}, @@ -1914,6 +1916,9 @@ impl ChainManager { &act.chain_state.alt_keys, sync_superblock, block_epoch, + // TODO: read from the right place so that this can react to the protocol + // version change during the 2.0 transition + ProtocolVersion::Legacy, ); // Put the local superblock into chain state diff --git a/schemas/witnet/witnet.proto b/schemas/witnet/witnet.proto index 237e472ae9..f110eda14c 100644 --- a/schemas/witnet/witnet.proto +++ b/schemas/witnet/witnet.proto @@ -2,6 +2,28 @@ syntax = "proto3"; package witnet; +message LegacyMessage { + message LegacyCommand { + oneof kind { + Version Version = 1; + Verack Verack = 2; + GetPeers GetPeers = 3; + Peers Peers = 4; + LegacyBlock Block = 5; + InventoryAnnouncement InventoryAnnouncement = 6; + InventoryRequest InventoryRequest = 7; + LastBeacon LastBeacon = 8; + Transaction Transaction = 9; + SuperBlockVote SuperBlockVote = 10; + SuperBlock SuperBlock = 11; + } + } + + // uint32 is not a fixed-size 32 bit integer: it uses variable length encoding + uint32 magic = 1; + LegacyCommand kind = 2; +} + message Message { message Command { oneof kind { @@ -47,10 +69,42 @@ message Peers { repeated Address peers = 1; } -message Block { - message BlockEligibilityClaim { - VrfProof proof = 1; +message BlockEligibilityClaim { + VrfProof proof = 1; +} + +message LegacyBlock { + message LegacyBlockHeader { + message LegacyBlockMerkleRoots { + Hash mint_hash = 1; + Hash vt_hash_merkle_root = 2; + Hash dr_hash_merkle_root = 3; + Hash commit_hash_merkle_root = 4; + Hash reveal_hash_merkle_root = 5; + Hash tally_hash_merkle_root = 6; + } + + uint32 signals = 1; + CheckpointBeacon beacon = 2; + LegacyBlockMerkleRoots merkle_roots = 3; + BlockEligibilityClaim proof = 4; + Bn256PublicKey bn256_public_key = 5; } + message LegacyBlockTransactions { + MintTransaction mint = 1; + repeated VTTransaction value_transfer_txns = 2; + repeated DRTransaction data_request_txns = 3; + repeated CommitTransaction commit_txns = 4; + repeated RevealTransaction reveal_txns = 5; + repeated TallyTransaction tally_txns = 6; + } + + LegacyBlockHeader block_header = 1; + KeyedSignature block_sig = 2; + LegacyBlockTransactions txns = 3; +} + +message Block { message BlockHeader { message BlockMerkleRoots { Hash mint_hash = 1; @@ -75,8 +129,8 @@ message Block { repeated CommitTransaction commit_txns = 4; repeated RevealTransaction reveal_txns = 5; repeated TallyTransaction tally_txns = 6; - repeated StakeTransaction stake_txns = 7; - repeated UnstakeTransaction unstake_txns = 8; + repeated StakeTransaction stake_txns = 7; + repeated UnstakeTransaction unstake_txns = 8; } BlockHeader block_header = 1; From 05fa8207ef3a428329d29bd610f634aec9df561d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Tue, 21 Nov 2023 12:12:06 +0100 Subject: [PATCH 07/12] fix(tests): fix some tests that rely on block hashes --- data_structures/src/chain/mod.rs | 89 +++++++++++++++++++------------- node/src/actors/json_rpc/api.rs | 2 +- validations/src/validations.rs | 3 +- 3 files changed, 56 insertions(+), 38 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index fd06513c57..168d0459d0 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -1,9 +1,3 @@ -/// Keeps track of priority being used by transactions included in recent blocks, and provides -/// methods for estimating sensible priority values for future transactions. -pub mod priority; -/// Contains all TAPI related structures and business logic -pub mod tapi; - use std::{ cell::{Cell, RefCell}, cmp::Ordering, @@ -20,8 +14,9 @@ use bls_signatures_rs::{bn256, bn256::Bn256, MultiSignature}; use failure::Fail; use futures::future::BoxFuture; use ordered_float::OrderedFloat; -use partial_struct::PartialStruct; use serde::{Deserialize, Serialize}; + +use partial_struct::PartialStruct; use witnet_crypto::{ hash::{calculate_sha256, Sha256}, key::ExtendedSK, @@ -42,7 +37,10 @@ use crate::{ TransactionError, }, get_environment, - proto::{schema::witnet, ProtobufConvert}, + proto::{ + versioning::{ProtocolVersion, VersionedHashable}, + ProtobufConvert, + }, superblock::SuperBlockState, transaction::{ CommitTransaction, DRTransaction, DRTransactionBody, Memoized, MintTransaction, @@ -56,6 +54,12 @@ use crate::{ vrf::{BlockEligibilityClaim, DataRequestEligibilityClaim}, }; +/// Keeps track of priority being used by transactions included in recent blocks, and provides +/// methods for estimating sensible priority values for future transactions. +pub mod priority; +/// Contains all TAPI related structures and business logic +pub mod tapi; + /// Define how the different structures should be hashed. pub trait Hashable { /// Calculate the hash of `self` @@ -157,7 +161,7 @@ impl Environment { PartialStruct, Debug, Clone, PartialEq, Serialize, Deserialize, ProtobufConvert, Default, )] #[partial_struct(derive(Deserialize, Serialize, Default, Debug, Clone, PartialEq))] -#[protobuf_convert(pb = "witnet::ConsensusConstants")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::ConsensusConstants")] pub struct ConsensusConstants { /// Timestamp at checkpoint 0 (the start of epoch 0) pub checkpoint_zero_timestamp: i64, @@ -360,7 +364,7 @@ impl GenesisBlockInfo { #[derive( Copy, Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize, ProtobufConvert, )] -#[protobuf_convert(pb = "witnet::CheckpointBeacon")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::CheckpointBeacon")] #[serde(rename_all = "camelCase")] pub struct CheckpointBeacon { /// The serial number for an epoch @@ -373,7 +377,7 @@ pub struct CheckpointBeacon { #[derive( Copy, Clone, Debug, Default, Eq, Hash, PartialEq, Serialize, Deserialize, ProtobufConvert, )] -#[protobuf_convert(pb = "witnet::CheckpointVRF")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::CheckpointVRF")] #[serde(rename_all = "camelCase")] pub struct CheckpointVRF { /// The serial number for an epoch @@ -387,7 +391,7 @@ pub type Epoch = u32; /// Block data structure #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Default, Hash)] -#[protobuf_convert(pb = "witnet::Block")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Block")] pub struct Block { /// The header of the block pub block_header: BlockHeader, @@ -403,7 +407,7 @@ pub struct Block { /// Block transactions #[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash)] -#[protobuf_convert(pb = "witnet::Block_BlockTransactions")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Block_BlockTransactions")] pub struct BlockTransactions { /// Mint transaction, pub mint: MintTransaction, @@ -530,6 +534,10 @@ impl Block { pub fn weight(&self) -> u32 { self.dr_weight() + self.vt_weight() + self.st_weight() + self.ut_weight() } + + pub fn is_genesis(&self, genesis: &Hash) -> bool { + self.versioned_hash(ProtocolVersion::Legacy).eq(genesis) + } } impl BlockTransactions { @@ -701,7 +709,7 @@ impl Hashable for PublicKey { /// Block header structure #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Default, Hash)] -#[protobuf_convert(pb = "witnet::Block_BlockHeader")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Block_BlockHeader")] pub struct BlockHeader { /// 32 bits for binary signaling new witnet protocol improvements. /// See [WIP-0014](https://github.com/witnet/WIPs/blob/master/wip-0014.md) for more info. @@ -717,7 +725,7 @@ pub struct BlockHeader { } /// Block merkle tree roots #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Default, Hash)] -#[protobuf_convert(pb = "witnet::Block_BlockHeader_BlockMerkleRoots")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Block_BlockHeader_BlockMerkleRoots")] pub struct BlockMerkleRoots { /// A 256-bit hash based on the mint transaction committed to this block pub mint_hash: Hash, @@ -775,7 +783,7 @@ impl BlockMerkleRoots { /// This is needed to ensure that the security and trustlessness properties of Witnet will /// be relayed to bridges with other block chains. #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, ProtobufConvert, Serialize)] -#[protobuf_convert(pb = "witnet::SuperBlock")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::SuperBlock")] pub struct SuperBlock { /// Number of signing committee members, pub signing_committee_length: u32, @@ -908,7 +916,7 @@ impl SuperBlock { /// Superblock votes as sent through the network #[derive(Debug, Eq, PartialEq, Clone, Hash, ProtobufConvert, Serialize, Deserialize)] -#[protobuf_convert(pb = "witnet::SuperBlockVote")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::SuperBlockVote")] pub struct SuperBlockVote { /// BN256 signature of `superblock_index` and `superblock_hash` pub bn256_signature: Bn256Signature, @@ -966,7 +974,7 @@ impl SuperBlockVote { /// Digital signatures structure (based on supported cryptosystems) #[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::Signature")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Signature")] pub enum Signature { /// ECDSA over secp256k1 Secp256k1(Secp256k1Signature), @@ -1002,7 +1010,7 @@ impl Signature { /// ECDSA (over secp256k1) signature #[derive(Debug, Default, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::Secp256k1Signature")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Secp256k1Signature")] pub struct Secp256k1Signature { /// The signature serialized in DER pub der: Vec, @@ -1094,7 +1102,7 @@ impl From for ExtendedSK { /// Hash #[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::Hash")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Hash")] pub enum Hash { /// SHA-256 Hash SHA256(SHA256), @@ -1224,7 +1232,7 @@ pub type SHA256 = [u8; 32]; /// /// It is the first 20 bytes of the SHA256 hash of the PublicKey. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, ProtobufConvert, Ord, PartialOrd)] -#[protobuf_convert(pb = "witnet::PublicKeyHash")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::PublicKeyHash")] pub struct PublicKeyHash { pub(crate) hash: [u8; 20], } @@ -1362,7 +1370,7 @@ impl PublicKeyHash { #[derive( Debug, Default, Eq, PartialEq, Copy, Clone, Serialize, Deserialize, ProtobufConvert, Hash, )] -#[protobuf_convert(pb = "witnet::Input")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Input")] pub struct Input { output_pointer: OutputPointer, } @@ -1384,7 +1392,7 @@ impl Input { /// Value transfer output transaction data structure #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash, Default)] -#[protobuf_convert(pb = "witnet::ValueTransferOutput")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::ValueTransferOutput")] pub struct ValueTransferOutput { /// Address that will receive the value pub pkh: PublicKeyHash, @@ -1410,7 +1418,7 @@ impl ValueTransferOutput { /// Data request output transaction data structure #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash, Default)] -#[protobuf_convert(pb = "witnet::DataRequestOutput")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::DataRequestOutput")] pub struct DataRequestOutput { /// Data request structure pub data_request: RADRequest, @@ -1475,7 +1483,7 @@ impl DataRequestOutput { } #[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash)] -#[protobuf_convert(pb = "witnet::StakeOutput")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::StakeOutput")] pub struct StakeOutput { pub value: u64, pub authorization: KeyedSignature, @@ -1541,7 +1549,7 @@ pub struct SupplyInfo { /// Keyed signature data structure #[derive(Debug, Default, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::KeyedSignature")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::KeyedSignature")] pub struct KeyedSignature { /// Signature pub signature: Signature, @@ -1616,7 +1624,7 @@ pub struct ExtendedSecretKey { /// BN256 public key #[derive(Debug, Eq, PartialEq, Hash, Default, Clone, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::Bn256PublicKey")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Bn256PublicKey")] pub struct Bn256PublicKey { /// Compressed form of a BN256 public key pub public_key: Vec, @@ -1635,7 +1643,7 @@ pub struct Bn256SecretKey { /// BN256 signature #[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::Bn256Signature")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Bn256Signature")] pub struct Bn256Signature { /// Signature pub signature: Vec, @@ -1643,7 +1651,7 @@ pub struct Bn256Signature { /// BN256 signature and public key #[derive(Debug, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::Bn256KeyedSignature")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Bn256KeyedSignature")] pub struct Bn256KeyedSignature { /// Signature pub signature: Bn256Signature, @@ -1760,7 +1768,10 @@ impl RADType { /// RAD request data structure #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, ProtobufConvert, Hash)] -#[protobuf_convert(pb = "witnet::DataRequestOutput_RADRequest", crate = "crate")] +#[protobuf_convert( + pb = "crate::proto::schema::witnet::DataRequestOutput_RADRequest", + crate = "crate" +)] pub struct RADRequest { /// Commitments for this request will not be accepted in any block proposed for an epoch /// whose opening timestamp predates the specified time lock. This effectively prevents @@ -1794,7 +1805,7 @@ impl RADRequest { /// Retrieve script and source #[derive(Debug, Eq, PartialEq, Clone, ProtobufConvert, Hash, Default)] #[protobuf_convert( - pb = "witnet::DataRequestOutput_RADRequest_RADRetrieve", + pb = "crate::proto::schema::witnet::DataRequestOutput_RADRequest_RADRetrieve", crate = "crate" )] pub struct RADRetrieve { @@ -1971,7 +1982,10 @@ impl RADRetrieve { /// Filter stage #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash, Default)] -#[protobuf_convert(pb = "witnet::DataRequestOutput_RADRequest_RADFilter", crate = "crate")] +#[protobuf_convert( + pb = "crate::proto::schema::witnet::DataRequestOutput_RADRequest_RADFilter", + crate = "crate" +)] pub struct RADFilter { /// `RadonFilters` code pub op: u32, @@ -1992,7 +2006,7 @@ impl RADFilter { /// Aggregate stage #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash, Default)] #[protobuf_convert( - pb = "witnet::DataRequestOutput_RADRequest_RADAggregate", + pb = "crate::proto::schema::witnet::DataRequestOutput_RADRequest_RADAggregate", crate = "crate" )] pub struct RADAggregate { @@ -2018,7 +2032,10 @@ impl RADAggregate { /// Tally stage #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash, Default)] -#[protobuf_convert(pb = "witnet::DataRequestOutput_RADRequest_RADTally", crate = "crate")] +#[protobuf_convert( + pb = "crate::proto::schema::witnet::DataRequestOutput_RADRequest_RADTally", + crate = "crate" +)] pub struct RADTally { /// List of filters to be applied in sequence pub filters: Vec, @@ -3310,7 +3327,7 @@ impl TransactionsPool { /// Unspent output data structure (equivalent of Bitcoin's UTXO) /// It is used to locate the output by its transaction identifier and its position #[derive(Default, Hash, Copy, Clone, Eq, PartialEq, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::OutputPointer")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::OutputPointer")] pub struct OutputPointer { /// Transaction identifier pub transaction_id: Hash, @@ -3373,7 +3390,7 @@ impl PartialOrd for OutputPointer { /// Inventory entry data structure #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::InventoryEntry")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::InventoryEntry")] pub enum InventoryEntry { /// Transaction Tx(Hash), diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index 9976796f8c..c53a61b4b7 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -2124,7 +2124,7 @@ mod tests { let block = block_example(); let inv_elem = InventoryItem::Block(block); let s = serde_json::to_string(&inv_elem).unwrap(); - let expected = r#"{"block":{"block_header":{"signals":0,"beacon":{"checkpoint":0,"hashPrevBlock":"0000000000000000000000000000000000000000000000000000000000000000"},"merkle_roots":{"mint_hash":"0000000000000000000000000000000000000000000000000000000000000000","vt_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","dr_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","commit_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","reveal_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","tally_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000"},"proof":{"proof":{"proof":[],"public_key":{"compressed":0,"bytes":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}}},"bn256_public_key":null},"block_sig":{"signature":{"Secp256k1":{"der":[]}},"public_key":{"compressed":0,"bytes":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},"txns":{"mint":{"epoch":0,"outputs":[]},"value_transfer_txns":[],"data_request_txns":[{"body":{"inputs":[{"output_pointer":"0000000000000000000000000000000000000000000000000000000000000000:0"}],"outputs":[{"pkh":"wit1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwrt3a4","value":0,"time_lock":0}],"dr_output":{"data_request":{"time_lock":0,"retrieve":[{"kind":"HTTP-GET","url":"https://openweathermap.org/data/2.5/weather?id=2950159&appid=b6907d289e10d714a6e88b30761fae22"},{"kind":"HTTP-GET","url":"https://openweathermap.org/data/2.5/weather?id=2950159&appid=b6907d289e10d714a6e88b30761fae22"}],"aggregate":{"filters":[],"reducer":0},"tally":{"filters":[],"reducer":0}},"witness_reward":0,"witnesses":0,"commit_and_reveal_fee":0,"min_consensus_percentage":0,"collateral":0}},"signatures":[{"signature":{"Secp256k1":{"der":[]}},"public_key":{"compressed":0,"bytes":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}}]}],"commit_txns":[],"reveal_txns":[],"tally_txns":[]}}}"#; + let expected = r#"{"block":{"block_header":{"signals":0,"beacon":{"checkpoint":0,"hashPrevBlock":"0000000000000000000000000000000000000000000000000000000000000000"},"merkle_roots":{"mint_hash":"0000000000000000000000000000000000000000000000000000000000000000","vt_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","dr_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","commit_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","reveal_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","tally_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","stake_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000","unstake_hash_merkle_root":"0000000000000000000000000000000000000000000000000000000000000000"},"proof":{"proof":{"proof":[],"public_key":{"compressed":0,"bytes":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}}},"bn256_public_key":null},"block_sig":{"signature":{"Secp256k1":{"der":[]}},"public_key":{"compressed":0,"bytes":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},"txns":{"mint":{"epoch":0,"outputs":[]},"value_transfer_txns":[],"data_request_txns":[{"body":{"inputs":[{"output_pointer":"0000000000000000000000000000000000000000000000000000000000000000:0"}],"outputs":[{"pkh":"wit1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwrt3a4","value":0,"time_lock":0}],"dr_output":{"data_request":{"time_lock":0,"retrieve":[{"kind":"HTTP-GET","url":"https://openweathermap.org/data/2.5/weather?id=2950159&appid=b6907d289e10d714a6e88b30761fae22"},{"kind":"HTTP-GET","url":"https://openweathermap.org/data/2.5/weather?id=2950159&appid=b6907d289e10d714a6e88b30761fae22"}],"aggregate":{"filters":[],"reducer":0},"tally":{"filters":[],"reducer":0}},"witness_reward":0,"witnesses":0,"commit_and_reveal_fee":0,"min_consensus_percentage":0,"collateral":0}},"signatures":[{"signature":{"Secp256k1":{"der":[]}},"public_key":{"compressed":0,"bytes":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}}]}],"commit_txns":[],"reveal_txns":[],"tally_txns":[],"stake_txns":[],"unstake_txns":[]}}}"#; assert_eq!(s, expected, "\n{}\n", s); } diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 509370439c..4aa2e18e48 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -6,6 +6,7 @@ use std::{ }; use itertools::Itertools; + use witnet_config::defaults::{ PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO, PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE, @@ -1591,7 +1592,7 @@ pub fn validate_block_transactions( mut visitor: Option<&mut dyn Visitor>, ) -> Result { let epoch = block.block_header.beacon.checkpoint; - let is_genesis = block.hash() == consensus_constants.genesis_hash; + let is_genesis = block.is_genesis(&consensus_constants.genesis_hash); let mut utxo_diff = UtxoDiff::new(utxo_set, block_number); // Init total fee From 2131f8b247b688a8e8af5bdd6eb8a4a339fb782f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Tue, 21 Nov 2023 14:04:19 +0100 Subject: [PATCH 08/12] fix(node): make session message decoding backwards-compatible --- node/src/actors/session/handlers.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/node/src/actors/session/handlers.rs b/node/src/actors/session/handlers.rs index 9b127ae2fb..c7aafa6e85 100644 --- a/node/src/actors/session/handlers.rs +++ b/node/src/actors/session/handlers.rs @@ -14,7 +14,7 @@ use witnet_data_structures::{ Block, CheckpointBeacon, Epoch, Hashable, InventoryEntry, InventoryItem, SuperBlock, SuperBlockVote, }, - proto::ProtobufConvert, + proto::versioning::Versioned, transaction::Transaction, types::{ Address, Command, InventoryAnnouncement, InventoryRequest, LastBeacon, @@ -22,8 +22,8 @@ use witnet_data_structures::{ }, }; use witnet_p2p::sessions::{SessionStatus, SessionType}; +use witnet_util::timestamp::get_timestamp; -use super::Session; use crate::actors::{ chain_manager::ChainManager, inventory_manager::InventoryManager, @@ -39,7 +39,7 @@ use crate::actors::{ sessions_manager::SessionsManager, }; -use witnet_util::timestamp::get_timestamp; +use super::Session; #[derive(Debug, Eq, Fail, PartialEq)] enum HandshakeError { @@ -133,7 +133,7 @@ impl StreamHandler> for Session { } let bytes = res.unwrap(); - let result = WitnetMessage::from_pb_bytes(&bytes); + let result = WitnetMessage::from_versioned_pb_bytes(&bytes); match result { Err(err) => { @@ -1105,9 +1105,10 @@ fn process_superblock_vote(_session: &mut Session, superblock_vote: SuperBlockVo #[cfg(test)] mod tests { - use super::*; use witnet_data_structures::chain::Hash; + use super::*; + #[test] fn handshake_bootstrap_before_epoch_zero() { // Check that when the last beacon has epoch 0 and the current epoch is not 0, From 0fb724f295659676914758df3e3a4e2c9623adfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Tue, 21 Nov 2023 15:52:20 +0100 Subject: [PATCH 09/12] feat(node): make 2.0 codebase able to sync to V1_6 --- data_structures/src/chain/mod.rs | 38 ++----- data_structures/src/lib.rs | 41 +++++++- data_structures/src/superblock.rs | 121 +++++----------------- data_structures/src/types.rs | 4 +- data_structures/tests/serializers.rs | 8 +- node/src/actors/chain_manager/handlers.rs | 21 +++- node/src/actors/chain_manager/mod.rs | 9 +- validations/src/validations.rs | 19 +++- 8 files changed, 113 insertions(+), 148 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index 168d0459d0..31391ea012 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -36,11 +36,8 @@ use crate::{ DataRequestError, EpochCalculationError, OutputPointerParseError, Secp256k1ConversionError, TransactionError, }, - get_environment, - proto::{ - versioning::{ProtocolVersion, VersionedHashable}, - ProtobufConvert, - }, + get_environment, get_protocol_version, + proto::{versioning::Versioned, ProtobufConvert}, superblock::SuperBlockState, transaction::{ CommitTransaction, DRTransaction, DRTransactionBody, Memoized, MintTransaction, @@ -536,7 +533,7 @@ impl Block { } pub fn is_genesis(&self, genesis: &Hash) -> bool { - self.versioned_hash(ProtocolVersion::Legacy).eq(genesis) + self.hash().eq(genesis) } } @@ -671,7 +668,9 @@ impl Hashable for BlockHeader { impl MemoizedHashable for Block { fn hashable_bytes(&self) -> Vec { - self.block_header.to_pb_bytes().unwrap() + self.block_header + .to_versioned_pb_bytes(get_protocol_version()) + .unwrap() } fn memoized_hash(&self) -> &MemoHash { @@ -4573,19 +4572,17 @@ mod tests { let block = block_example(); let expected = "70e15ac70bb00f49c7a593b2423f722dca187bbae53dc2f22647063b17608c01"; assert_eq!( - block.versioned_hash(ProtocolVersion::Legacy).to_string(), + block.versioned_hash(ProtocolVersion::V1_6).to_string(), expected ); let expected = "29ef68357a5c861b9dbe043d351a28472ca450edcda25de4c9b80a4560a28c0f"; assert_eq!( - block - .versioned_hash(ProtocolVersion::Transition) - .to_string(), + block.versioned_hash(ProtocolVersion::V1_7).to_string(), expected ); let expected = "29ef68357a5c861b9dbe043d351a28472ca450edcda25de4c9b80a4560a28c0f"; assert_eq!( - block.versioned_hash(ProtocolVersion::Final).to_string(), + block.versioned_hash(ProtocolVersion::V2_0).to_string(), expected ); } @@ -6659,7 +6656,6 @@ mod tests { 1, Hash::default(), 1, - ProtocolVersion::Legacy, ); let expected_indices = vec![0, 2, 2]; @@ -6714,7 +6710,6 @@ mod tests { 1, Hash::default(), 1, - ProtocolVersion::Legacy, ); let expected_indices = vec![0, 2, 2, 8, 10, 6, 4, 6]; @@ -6750,7 +6745,6 @@ mod tests { 1, Hash::default(), 1, - ProtocolVersion::Legacy, ); let result = sb.dr_proof_of_inclusion(&[b1, b2], &dr_txs[2]); @@ -6761,14 +6755,7 @@ mod tests { fn test_dr_merkle_root_no_block() { let dr_txs = build_test_dr_txs(3); - let sb = mining_build_superblock( - &[], - &[Hash::default()], - 1, - Hash::default(), - 1, - ProtocolVersion::Legacy, - ); + let sb = mining_build_superblock(&[], &[Hash::default()], 1, Hash::default(), 1); let result = sb.dr_proof_of_inclusion(&[], &dr_txs[2]); assert!(result.is_none()); @@ -6794,7 +6781,6 @@ mod tests { 1, Hash::default(), 1, - ProtocolVersion::Legacy, ); let expected_indices = vec![0, 2]; @@ -6833,7 +6819,6 @@ mod tests { 1, Hash::default(), 1, - ProtocolVersion::Legacy, ); let expected_indices = vec![0, 2, 2]; @@ -6896,7 +6881,6 @@ mod tests { 1, Hash::default(), 1, - ProtocolVersion::Legacy, ); let expected_indices = vec![0, 2, 2, 8, 10, 6, 4, 6]; @@ -6932,7 +6916,6 @@ mod tests { 1, Hash::default(), 1, - ProtocolVersion::Legacy, ); let result = sb.tally_proof_of_inclusion(&[b1, b2], &tally_txs[2]); @@ -6964,7 +6947,6 @@ mod tests { 1, Hash::default(), 1, - ProtocolVersion::Legacy, ); let expected_indices = vec![0, 2, 2]; diff --git a/data_structures/src/lib.rs b/data_structures/src/lib.rs index e14acd6e5e..c57f2386ac 100644 --- a/data_structures/src/lib.rs +++ b/data_structures/src/lib.rs @@ -13,7 +13,7 @@ #[macro_use] extern crate protobuf_convert; -use crate::chain::Environment; +use crate::{chain::Environment, proto::versioning::ProtocolVersion}; use lazy_static::lazy_static; use std::sync::RwLock; @@ -82,6 +82,9 @@ lazy_static! { // can work without having to manually set the environment. // The default environment will also be used in tests. static ref ENVIRONMENT: RwLock = RwLock::new(Environment::Mainnet); + /// Protocol version that we are running. + /// default to legacy for now — it's the v2 bootstrapping module's responsibility to upgrade it. + static ref PROTOCOL_VERSION: RwLock = RwLock::new(ProtocolVersion::V1_6); } /// Environment in which we are running: mainnet or testnet. @@ -114,6 +117,34 @@ pub fn set_environment(environment: Environment) { } } +/// Protocol version that we are running. +pub fn get_protocol_version() -> ProtocolVersion { + // This unwrap is safe as long as the lock is not poisoned. + // The lock can only become poisoned when a writer panics. + // The only writer is the one used in `set_environment`, which should only + // be used during initialization. + *PROTOCOL_VERSION.read().unwrap() +} + +/// Set the protocol version that we are running. +/// This function should only be called once during initialization. +// Changing the environment in tests is not supported, as it can cause spurious failures: +// multiple tests can run in parallel and some tests might fail when the environment changes. +// But if you need to change the environment in some test, just create a separate thread-local +// variable and mock get and set. +#[cfg(not(test))] +pub fn set_protocol_version(protocol_version: ProtocolVersion) { + match PROTOCOL_VERSION.write() { + Ok(mut x) => { + *x = protocol_version; + log::debug!("Protocol version set to {}", protocol_version); + } + Err(e) => { + log::error!("Failed to set protocol version: {}", e); + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -124,4 +155,12 @@ mod tests { // addresses serialized as Bech32 will fail assert_eq!(get_environment(), Environment::Mainnet); } + + #[test] + fn default_protocol_version() { + // If this default changes before the transition to V2 is complete, almost everything will + // break because data structures change schema and, serialization changes and hash + // derivation breaks too + assert_eq!(get_protocol_version(), ProtocolVersion::V1_6); + } } diff --git a/data_structures/src/superblock.rs b/data_structures/src/superblock.rs index bfc46ae477..39a5a8da95 100644 --- a/data_structures/src/superblock.rs +++ b/data_structures/src/superblock.rs @@ -1,11 +1,3 @@ -use crate::{ - chain::{ - tapi::{after_second_hard_fork, in_emergency_period}, - AltKeys, BlockHeader, Bn256PublicKey, CheckpointBeacon, Epoch, Hash, Hashable, - PublicKeyHash, SuperBlock, SuperBlockVote, - }, - get_environment, -}; use std::{ collections::{HashMap, HashSet}, convert::{TryFrom, TryInto}, @@ -13,12 +5,20 @@ use std::{ use serde::{Deserialize, Serialize}; -use crate::proto::versioning::{ProtocolVersion, VersionedHashable}; use witnet_crypto::{ hash::{calculate_sha256, Sha256}, merkle::merkle_tree_root as crypto_merkle_tree_root, }; +use crate::{ + chain::{ + tapi::{after_second_hard_fork, in_emergency_period}, + AltKeys, BlockHeader, Bn256PublicKey, CheckpointBeacon, Epoch, Hash, Hashable, + PublicKeyHash, SuperBlock, SuperBlockVote, + }, + get_environment, +}; + /// Possible result of SuperBlockState::add_vote #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum AddSuperBlockVote { @@ -418,7 +418,6 @@ impl SuperBlockState { alt_keys: &AltKeys, sync_superblock: Option, block_epoch: Epoch, - protocol_version: ProtocolVersion, ) -> SuperBlock { let key_leaves = hash_key_leaves(&ars_identities.get_rep_ordered_bn256_list(alt_keys)); @@ -470,14 +469,13 @@ impl SuperBlockState { superblock_index, last_block_in_previous_superblock, self.signing_committee.len() as u32, - ProtocolVersion::Legacy, ) }; // update the superblock_beacon self.current_superblock_beacon = CheckpointBeacon { checkpoint: superblock_index, - hash_prev_block: superblock.versioned_hash(protocol_version), + hash_prev_block: superblock.hash(), }; let old_votes = self.votes_mempool.clear_and_remove_votes(); @@ -676,7 +674,6 @@ pub fn mining_build_superblock( index: u32, last_block_in_previous_superblock: Hash, signing_committee_length: u32, - protocol_version: ProtocolVersion, ) -> SuperBlock { let last_block = block_headers.last(); match last_block { @@ -702,7 +699,7 @@ pub fn mining_build_superblock( ) } Some(last_block_header) => { - let last_block_hash = last_block_header.versioned_hash(protocol_version); + let last_block_hash = last_block_header.hash(); let merkle_drs: Vec = block_headers .iter() .map(|b| b.merkle_roots.dr_hash_merkle_root) @@ -715,13 +712,7 @@ pub fn mining_build_superblock( let ars_root = hash_merkle_tree_root(ars_ordered_hash_leaves); let blocks: Vec<_> = block_headers .iter() - .map(|b| { - format!( - "#{}: {}", - b.beacon.checkpoint, - b.versioned_hash(protocol_version) - ) - }) + .map(|b| format!("#{}: {}", b.beacon.checkpoint, b.hash())) .collect(); log::trace!( "Created superblock #{} with hash_prev_block {}, ARS {}, signing_committee_length: {}, blocks {:?}", @@ -764,19 +755,22 @@ pub fn hash_merkle_tree_root(hashes: &[Hash]) -> Hash { #[cfg(test)] mod tests { - use super::*; + use itertools::Itertools; + + use witnet_crypto::hash::{calculate_sha256, EMPTY_SHA256}; + use crate::{ chain::{BlockMerkleRoots, Bn256SecretKey, CheckpointBeacon, PublicKey, Signature}, + proto::versioning::{ProtocolVersion, VersionedHashable}, vrf::BlockEligibilityClaim, }; - use itertools::Itertools; - use witnet_crypto::hash::{calculate_sha256, EMPTY_SHA256}; + + use super::*; #[test] fn test_superblock_creation_no_blocks() { let default_hash = Hash::default(); - let superblock = - mining_build_superblock(&[], &[], 0, default_hash, 0, ProtocolVersion::Legacy); + let superblock = mining_build_superblock(&[], &[], 0, default_hash, 0); let expected = SuperBlock::new( 0, @@ -829,19 +823,12 @@ mod tests { default_hash, dr_merkle_root_1, 0, - block.versioned_hash(ProtocolVersion::Legacy), + block.versioned_hash(ProtocolVersion::V1_6), default_hash, tally_merkle_root_1, ); - let superblock = mining_build_superblock( - &[block], - &[default_hash], - 0, - default_hash, - 1, - ProtocolVersion::Legacy, - ); + let superblock = mining_build_superblock(&[block], &[default_hash], 0, default_hash, 1); assert_eq!(superblock, expected_superblock); } @@ -904,19 +891,13 @@ mod tests { default_hash, expected_superblock_dr_root, 0, - block_2.versioned_hash(ProtocolVersion::Legacy), + block_2.versioned_hash(ProtocolVersion::V1_6), default_hash, expected_superblock_tally_root, ); - let superblock = mining_build_superblock( - &[block_1, block_2], - &[default_hash], - 0, - default_hash, - 1, - ProtocolVersion::Legacy, - ); + let superblock = + mining_build_superblock(&[block_1, block_2], &[default_hash], 0, default_hash, 1); assert_eq!(superblock, expected_superblock); } @@ -977,7 +958,6 @@ mod tests { &AltKeys::default(), None, 1, - ProtocolVersion::Legacy, ); let mut v0 = SuperBlockVote::new_unsigned(sb1.hash(), 1); @@ -1032,7 +1012,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let v1 = SuperBlockVote::new_unsigned(sb1.hash(), 0); assert_eq!( @@ -1063,7 +1042,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let expected_superblock = SuperBlock::new( @@ -1118,7 +1096,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let expected_second_superblock = SuperBlock::new( @@ -1143,7 +1120,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ), expected_second_superblock ); @@ -1188,7 +1164,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // After building a new superblock the cache is invalidated but the previous ARS is still empty assert_eq!( @@ -1206,7 +1181,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let v1 = SuperBlockVote::new_unsigned(Hash::SHA256([2; 32]), 1); assert_eq!( @@ -1251,7 +1225,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // The ARS included in superblock 0 is empty, so none of the superblock votes for index 1 @@ -1265,7 +1238,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // The ARS included in superblock 1 contains only identity p1, so only its vote will be @@ -1279,7 +1251,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let mut v1 = SuperBlockVote::new_unsigned(sb2.hash(), 2); v1.secp256k1_signature.public_key = p1.clone(); @@ -1316,7 +1287,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // The ARS included in superblock 0 is empty, so none of the superblock votes for index 1 @@ -1330,7 +1300,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let mut v2 = SuperBlockVote::new_unsigned(Hash::SHA256([2; 32]), 2); @@ -1348,7 +1317,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let mut v1 = SuperBlockVote::new_unsigned(sb2.hash(), 2); v1.secp256k1_signature.public_key = p1; @@ -1382,7 +1350,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // The ARS included in superblock 0 is empty, so none of the superblock votes for index 1 @@ -1396,7 +1363,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // The ARS included in superblock 1 contains only identity p1, so only its vote will be @@ -1410,7 +1376,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let mut v1 = SuperBlockVote::new_unsigned(sb2.hash(), 2); v1.secp256k1_signature.public_key = p1.clone(); @@ -1448,7 +1413,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // The ARS included in superblock 0 is empty, so none of the superblock votes for index 1 @@ -1462,7 +1426,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // The ARS included in superblock 1 contains only identity p1, so only its vote will be @@ -1476,7 +1439,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let mut v1 = SuperBlockVote::new_unsigned(sb2.hash(), 2); v1.secp256k1_signature.public_key = p1; @@ -1543,7 +1505,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let (v1, v2, v3) = create_votes(sb0.hash(), 0); assert_eq!( @@ -1570,7 +1531,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let (v1, v2, v3) = create_votes(sb1.hash(), 1); assert_eq!( @@ -1597,7 +1557,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let (v1, v2, v3) = create_votes(sb2.hash(), 2); assert_eq!(sbs.add_vote(&v1, 2), AddSuperBlockVote::ValidWithSameHash); @@ -1621,7 +1580,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let (v1, v2, v3) = create_votes(sb3.hash(), 3); assert_eq!( @@ -1645,7 +1603,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let (v1, v2, v3) = create_votes(sb4.hash(), 4); assert_eq!( @@ -1704,7 +1661,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let (v1, v2, v3, v4) = create_votes(sb0.hash(), 0); assert_eq!( @@ -1736,7 +1692,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let (v1, _v2, v3, v4) = create_votes(sb1.hash(), 1); let mut v2 = SuperBlockVote::new_unsigned( @@ -1766,7 +1721,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let mut v1 = SuperBlockVote::new_unsigned(sb2.hash(), 5); @@ -1834,7 +1788,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let (v1, v2, v3, v4) = create_votes(sb0.hash(), 0); assert_eq!( @@ -1864,7 +1817,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let (v1, v2, v3, v4) = create_votes(sb1.hash(), 1); assert_eq!(sbs.add_vote(&v1, 1), AddSuperBlockVote::ValidWithSameHash); @@ -1901,7 +1853,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let (v1, v2, v3, v4) = create_votes(sb2.hash(), 2); assert_eq!(sbs.add_vote(&v1, 2), AddSuperBlockVote::ValidWithSameHash); @@ -1952,7 +1903,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let expected_sb2 = mining_build_superblock( @@ -1961,7 +1911,6 @@ mod tests { 1, genesis_hash, 3, - ProtocolVersion::Legacy, ); let sb2_hash = expected_sb2.hash(); @@ -1982,7 +1931,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); assert_eq!(sb2, expected_sb2); let mut hh: HashMap<_, Vec<_>> = HashMap::new(); @@ -2008,7 +1956,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // votes_on_each_superblock are cleared when the local superblock changes assert_eq!(sbs.votes_mempool.get_valid_votes(), HashMap::new()); @@ -2045,7 +1992,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // superblock with index 1 let sb2 = sbs.build_superblock( @@ -2057,7 +2003,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let expected_sb2 = mining_build_superblock( @@ -2066,7 +2011,6 @@ mod tests { 1, genesis_hash, 2, - ProtocolVersion::Legacy, ); assert_eq!(sb2, expected_sb2); @@ -2124,7 +2068,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); assert_eq!(sbs.add_vote(&v0, 9), AddSuperBlockVote::MaybeValid); @@ -2154,7 +2097,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let _sb2 = sbs.build_superblock( @@ -2166,7 +2108,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); assert_eq!( @@ -2211,7 +2152,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // superblock with index 1 let sb2 = sbs.build_superblock( @@ -2223,7 +2163,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); let expected_sb2 = mining_build_superblock( @@ -2232,7 +2171,6 @@ mod tests { 1, genesis_hash, 2, - ProtocolVersion::Legacy, ); assert_eq!(sb2, expected_sb2); @@ -2294,7 +2232,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); sbs.ars_previous_identities = ars_identities.clone(); let committee_size = 4; @@ -2340,7 +2277,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // Signing committee size of 2 has been included @@ -2355,7 +2291,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // SB2_A is different to SB1 and a signing committee size of 3 has been included @@ -2371,7 +2306,6 @@ mod tests { &alt_keys, Some(sb1.clone()), 1, - ProtocolVersion::Legacy, ); // SB2_B is equal to SB1 and a signing committee size of 2 has been included @@ -2417,7 +2351,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); sbs.ars_previous_identities = ars_identities; let committee_size = 4; @@ -2463,7 +2396,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); sbs.ars_previous_identities = ars_identities.clone(); let committee_size = 2; @@ -2725,7 +2657,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // The ARS included in superblock 0 is empty, so none of the superblock votes for index 1 @@ -2739,7 +2670,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // The ARS included in superblock 1 contains only identity p1, so only its vote will be @@ -2753,7 +2683,6 @@ mod tests { &alt_keys, None, 1, - ProtocolVersion::Legacy, ); // 2 valid votes and 3 missing votes -> Unknown diff --git a/data_structures/src/types.rs b/data_structures/src/types.rs index 6feda901e1..10afcc31c1 100644 --- a/data_structures/src/types.rs +++ b/data_structures/src/types.rs @@ -53,7 +53,7 @@ impl fmt::Display for Command { Command::Version(_) => f.write_str("VERSION"), Command::Block(block) => write!( f, - "BLOCK: #{}: {}", + "BLOCK #{}: {}", block.block_header.beacon.checkpoint, block.hash() ), @@ -64,7 +64,7 @@ impl fmt::Display for Command { highest_superblock_checkpoint: s, }) => write!( f, - "LAST_BEACON: Block: #{}: {} Superblock: #{}: {}", + "LAST_BEACON Block: #{}: {} Superblock: #{}: {}", h.checkpoint, h.hash_prev_block, s.checkpoint, s.hash_prev_block ), Command::Transaction(tx) => { diff --git a/data_structures/tests/serializers.rs b/data_structures/tests/serializers.rs index 28f5bfde7f..8de51b1e5e 100644 --- a/data_structures/tests/serializers.rs +++ b/data_structures/tests/serializers.rs @@ -418,17 +418,15 @@ fn message_block_to_bytes() { }; let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR_LEGACY.to_vec(); - let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::Legacy).unwrap(); + let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::V1_6).unwrap(); assert_eq!(result, expected_buf); let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR_TRANSITION.to_vec(); - let result: Vec = msg - .to_versioned_pb_bytes(ProtocolVersion::Transition) - .unwrap(); + let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::V1_7).unwrap(); assert_eq!(result, expected_buf); let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR_FINAL.to_vec(); - let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::Final).unwrap(); + let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::V2_0).unwrap(); assert_eq!(result, expected_buf); } diff --git a/node/src/actors/chain_manager/handlers.rs b/node/src/actors/chain_manager/handlers.rs index 31c74f8d3a..112ef0dd69 100644 --- a/node/src/actors/chain_manager/handlers.rs +++ b/node/src/actors/chain_manager/handlers.rs @@ -967,11 +967,17 @@ impl Handler for ChainManager { }, _, )) => { - self.sync_target = Some(SyncTarget { + let target = SyncTarget { block: consensus_beacon, superblock: superblock_consensus, - }); - log::debug!("Sync target {:?}", self.sync_target); + }; + self.sync_target = Some(target); + log::info!( + "Synchronization target has been set ({}: {})", + target.block.checkpoint, + target.block.hash_prev_block + ); + log::debug!("{:#?}", target); let our_beacon = self.get_chain_beacon(); log::debug!( @@ -997,13 +1003,18 @@ impl Handler for ChainManager { { // Fork case log::warn!( - "[CONSENSUS]: We are on {:?} but the network is on {:?}", + "[CONSENSUS]: The local chain is apparently forked.\n\ + We are on {:?} but the network is on {:?}.\n\ + The node will automatically try to recover from this forked situation by restoring the chain state from the storage.", our_beacon, consensus_beacon ); self.initialize_from_storage(ctx); - log::info!("Restored chain state from storage"); + log::info!( + "The chain state has been restored from storage.\n\ + Now the node will try to resynchronize." + ); StateMachine::WaitingConsensus } else { diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index d8b0328ceb..285c040dfd 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -70,7 +70,6 @@ use witnet_data_structures::{ }, data_request::DataRequestPool, get_environment, - proto::versioning::ProtocolVersion, radon_report::{RadonReport, ReportContext}, superblock::{ARSIdentities, AddSuperBlockVote, SuperBlockConsensus}, transaction::{RevealTransaction, TallyTransaction, Transaction}, @@ -727,8 +726,9 @@ impl ChainManager { || block.block_header.beacon.checkpoint == current_epoch + 1) { log::debug!( - "Ignoring received block #{} because its beacon is too old", - block.block_header.beacon.checkpoint + "Ignoring received block candidate because its beacon shows an old epoch ({}). The current epoch is {}.", + block.block_header.beacon.checkpoint, + current_epoch, ); return; @@ -1916,9 +1916,6 @@ impl ChainManager { &act.chain_state.alt_keys, sync_superblock, block_epoch, - // TODO: read from the right place so that this can react to the protocol - // version change during the 2.0 transition - ProtocolVersion::Legacy, ); // Put the local superblock into chain state diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 4aa2e18e48..79eb6db25e 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -29,6 +29,8 @@ use witnet_data_structures::{ calculate_witness_reward_before_second_hard_fork, create_tally, DataRequestPool, }, error::{BlockError, DataRequestError, TransactionError}, + get_protocol_version, + proto::versioning::ProtocolVersion, radon_report::{RadonReport, ReportContext}, transaction::{ CommitTransaction, DRTransaction, MintTransaction, RevealTransaction, StakeTransaction, @@ -1314,6 +1316,8 @@ pub fn validate_block_signature( let signature = keyed_signature.signature.clone().try_into()?; let public_key = keyed_signature.public_key.clone().try_into()?; + // TODO: take into account block epoch to decide protocol version (with regards to data + // structures and hashing) let Hash::SHA256(message) = block.hash(); add_secp_block_signature_to_verify(signatures_to_verify, &public_key, &message, &signature); @@ -1883,6 +1887,8 @@ pub fn validate_block_transactions( ); } + // TODO skip all staking logic if protocol version is legacy + // validate stake transactions in a block let mut st_mt = ProgressiveMerkleTree::sha256(); let mut st_weight: u32 = 0; @@ -1939,8 +1945,6 @@ 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; @@ -1976,7 +1980,12 @@ pub fn validate_block_transactions( // } } - let ut_hash_merkle_root = ut_mt.root(); + // Nullify roots for legacy protocol version + // TODO skip all staking logic if protocol version is legacy + let (st_root, ut_root) = match get_protocol_version() { + ProtocolVersion::V1_6 => Default::default(), + _ => (Hash::from(st_mt.root()), Hash::from(ut_mt.root())), + }; // Validate Merkle Root let merkle_roots = BlockMerkleRoots { @@ -1986,8 +1995,8 @@ 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), - unstake_hash_merkle_root: Hash::from(ut_hash_merkle_root), + stake_hash_merkle_root: st_root, + unstake_hash_merkle_root: ut_root, }; if merkle_roots != block.block_header.merkle_roots { From 8252785e66c9c45a85d2d32c9149be9b7aba3a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Tue, 21 Nov 2023 16:58:27 +0100 Subject: [PATCH 10/12] chore(data_structures): commit missing module --- data_structures/src/proto/versioning.rs | 523 ++++++++++++++++++++++++ 1 file changed, 523 insertions(+) create mode 100644 data_structures/src/proto/versioning.rs diff --git a/data_structures/src/proto/versioning.rs b/data_structures/src/proto/versioning.rs new file mode 100644 index 0000000000..622d9c920d --- /dev/null +++ b/data_structures/src/proto/versioning.rs @@ -0,0 +1,523 @@ +use failure::{Error, Fail}; +use protobuf::Message as _; +use std::fmt; +use std::fmt::Formatter; + +use crate::proto::schema::witnet::SuperBlock; +use crate::{ + chain::Hash, + proto::{ + schema::witnet::{ + Block, Block_BlockHeader, Block_BlockHeader_BlockMerkleRoots, Block_BlockTransactions, + LegacyBlock, LegacyBlock_LegacyBlockHeader, + LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots, + LegacyBlock_LegacyBlockTransactions, LegacyMessage, LegacyMessage_LegacyCommand, + LegacyMessage_LegacyCommand_oneof_kind, Message_Command, Message_Command_oneof_kind, + }, + ProtobufConvert, + }, + transaction::MemoizedHashable, + types::Message, +}; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ProtocolVersion { + /// The original Witnet protocol. + V1_6, + /// The transitional protocol based on 1.x but with staking enabled. + V1_7, + /// The final Witnet 2.0 protocol. + V2_0, +} + +impl fmt::Display for ProtocolVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let s = match self { + ProtocolVersion::V1_6 => "v1.6 (legacy)", + ProtocolVersion::V1_7 => "v1.7 (transitional)", + ProtocolVersion::V2_0 => "v2.0 (final)", + }; + + f.write_str(s) + } +} + +pub trait Versioned: ProtobufConvert { + type LegacyType: protobuf::Message; + + /// Turn a protobuf-compatible data structure into a versioned form of itself. + /// + /// For truly versionable data structures, this method should be implemented manually. For other + /// data structures, the trait's own blanket implementation should be fine. + fn to_versioned_pb( + &self, + _version: ProtocolVersion, + ) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + Ok(Box::new(self.to_pb())) + } + /// Turn a protobuf-compaitble data structures into its serialized protobuf bytes. + /// This blanket implementation should normally not be overriden. + fn to_versioned_pb_bytes(&self, version: ProtocolVersion) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + Ok(self.to_versioned_pb(version)?.write_to_bytes().unwrap()) + } + + /// Constructs an instance of this data structure based on a protobuf instance of its legacy + /// schema. + fn from_versioned_pb(legacy: Self::LegacyType) -> Result + where + Self: From, + { + Ok(Self::from(legacy)) + } + + /// Tries to deserialize a data structure from its regular protobuf schema, and if it fails, it + /// retries with its legacy schema. + fn from_versioned_pb_bytes(bytes: &[u8]) -> Result + where + ::ProtoStruct: protobuf::Message, + Self: From, + { + let mut current = Self::ProtoStruct::new(); + let direct_attempt = current + .merge_from_bytes(bytes) + .map_err(|e| Error::from_boxed_compat(Box::new(e.compat()))) + .and_then(|_| Self::from_pb(current)); + + if direct_attempt.is_ok() { + direct_attempt + } else { + let mut legacy = Self::LegacyType::new(); + legacy.merge_from_bytes(bytes)?; + + Ok(Self::from(legacy)) + } + } +} + +impl Versioned for crate::chain::BlockMerkleRoots { + type LegacyType = LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots; + + fn to_versioned_pb( + &self, + version: ProtocolVersion, + ) -> Result, Error> { + use ProtocolVersion::*; + + let mut pb = self.to_pb(); + + let versioned: Box = match version { + // Legacy merkle roots need to get rearranged + V1_6 => Box::new(Self::LegacyType::from(pb)), + // Transition merkle roots need no transformation + V1_7 => Box::new(pb), + // Final merkle roots need to drop the mint hash + V2_0 => { + pb.set_mint_hash(Default::default()); + + Box::new(pb) + } + }; + + Ok(versioned) + } +} + +impl Versioned for crate::chain::BlockHeader { + type LegacyType = LegacyBlock_LegacyBlockHeader; + + fn to_versioned_pb( + &self, + version: ProtocolVersion, + ) -> Result, Error> { + use ProtocolVersion::*; + + let pb = self.to_pb(); + + let versioned: Box = match version { + // Legacy block headers need to be rearranged + V1_6 => Box::new(Self::LegacyType::from(pb)), + // All other block headers need no transformation + V1_7 | V2_0 => Box::new(pb), + }; + + Ok(versioned) + } +} + +impl Versioned for crate::chain::SuperBlock { + type LegacyType = SuperBlock; + + fn to_versioned_pb_bytes(&self, _version: ProtocolVersion) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + Ok(self.hashable_bytes()) + } +} + +impl Versioned for crate::chain::Block { + type LegacyType = LegacyBlock; + + fn to_versioned_pb( + &self, + _version: ProtocolVersion, + ) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + Ok(Box::new(Self::LegacyType::from(self.to_pb()))) + } +} + +impl Versioned for Message { + type LegacyType = LegacyMessage; + + fn to_versioned_pb(&self, version: ProtocolVersion) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + use ProtocolVersion::*; + + let pb = self.to_pb(); + + let versioned: Box = match version { + V1_6 => Box::new(Self::LegacyType::from(pb)), + V1_7 | V2_0 => Box::new(pb), + }; + + Ok(versioned) + } +} + +pub trait AutoVersioned: ProtobufConvert {} + +impl AutoVersioned for crate::chain::BlockHeader {} +impl AutoVersioned for crate::chain::SuperBlock {} + +pub trait VersionedHashable { + fn versioned_hash(&self, version: ProtocolVersion) -> Hash; +} + +impl VersionedHashable for T +where + T: AutoVersioned + Versioned, + ::ProtoStruct: protobuf::Message, +{ + fn versioned_hash(&self, version: ProtocolVersion) -> Hash { + witnet_crypto::hash::calculate_sha256(&self.to_versioned_pb_bytes(version).unwrap()).into() + } +} + +impl VersionedHashable for crate::chain::Block { + fn versioned_hash(&self, version: ProtocolVersion) -> Hash { + self.block_header.versioned_hash(version) + } +} + +impl From + for LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots +{ + fn from(header: Block_BlockHeader_BlockMerkleRoots) -> Self { + let mut legacy = LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots::new(); + legacy.set_mint_hash(header.get_mint_hash().clone()); + legacy.vt_hash_merkle_root = header.vt_hash_merkle_root; + legacy.dr_hash_merkle_root = header.dr_hash_merkle_root; + legacy.commit_hash_merkle_root = header.commit_hash_merkle_root; + legacy.reveal_hash_merkle_root = header.reveal_hash_merkle_root; + legacy.tally_hash_merkle_root = header.tally_hash_merkle_root; + + legacy + } +} + +impl From + for Block_BlockHeader_BlockMerkleRoots +{ + fn from( + LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots { + mint_hash, + vt_hash_merkle_root, + dr_hash_merkle_root, + commit_hash_merkle_root, + reveal_hash_merkle_root, + tally_hash_merkle_root, + .. + }: LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots, + ) -> Self { + let mut header = Block_BlockHeader_BlockMerkleRoots::new(); + header.mint_hash = mint_hash; + header.vt_hash_merkle_root = vt_hash_merkle_root; + header.dr_hash_merkle_root = dr_hash_merkle_root; + header.commit_hash_merkle_root = commit_hash_merkle_root; + header.reveal_hash_merkle_root = reveal_hash_merkle_root; + header.tally_hash_merkle_root = tally_hash_merkle_root; + header.set_stake_hash_merkle_root(Hash::default().to_pb()); + header.set_unstake_hash_merkle_root(Hash::default().to_pb()); + + header + } +} + +impl From for LegacyBlock_LegacyBlockHeader { + fn from( + Block_BlockHeader { + signals, + beacon, + merkle_roots, + proof, + bn256_public_key, + .. + }: Block_BlockHeader, + ) -> Self { + let mut legacy = LegacyBlock_LegacyBlockHeader::new(); + legacy.signals = signals; + legacy.beacon = beacon; + legacy.merkle_roots = merkle_roots.map(Into::into); + legacy.proof = proof; + legacy.bn256_public_key = bn256_public_key; + + legacy + } +} + +impl From for Block_BlockHeader { + fn from( + LegacyBlock_LegacyBlockHeader { + signals, + beacon, + merkle_roots, + proof, + bn256_public_key, + .. + }: LegacyBlock_LegacyBlockHeader, + ) -> Self { + let mut header = Block_BlockHeader::new(); + header.signals = signals; + header.beacon = beacon; + header.merkle_roots = merkle_roots.map(Into::into); + header.proof = proof; + header.bn256_public_key = bn256_public_key; + + header + } +} + +impl From for LegacyBlock_LegacyBlockTransactions { + fn from( + Block_BlockTransactions { + mint, + value_transfer_txns, + data_request_txns, + commit_txns, + reveal_txns, + tally_txns, + .. + }: Block_BlockTransactions, + ) -> Self { + let mut legacy = LegacyBlock_LegacyBlockTransactions::new(); + legacy.mint = mint; + legacy.value_transfer_txns = value_transfer_txns; + legacy.data_request_txns = data_request_txns; + legacy.commit_txns = commit_txns; + legacy.reveal_txns = reveal_txns; + legacy.tally_txns = tally_txns; + + legacy + } +} + +impl From for Block_BlockTransactions { + fn from( + LegacyBlock_LegacyBlockTransactions { + mint, + value_transfer_txns, + data_request_txns, + commit_txns, + reveal_txns, + tally_txns, + .. + }: LegacyBlock_LegacyBlockTransactions, + ) -> Self { + let mut txns = Block_BlockTransactions::new(); + txns.mint = mint; + txns.value_transfer_txns = value_transfer_txns; + txns.data_request_txns = data_request_txns; + txns.commit_txns = commit_txns; + txns.reveal_txns = reveal_txns; + txns.tally_txns = tally_txns; + txns.stake_txns = vec![].into(); + txns.unstake_txns = vec![].into(); + + txns + } +} + +impl From for LegacyBlock { + fn from( + Block { + block_header, + block_sig, + txns, + .. + }: Block, + ) -> Self { + let mut legacy = LegacyBlock::new(); + legacy.block_header = block_header.map(Into::into); + legacy.block_sig = block_sig; + legacy.txns = txns.map(Into::into); + + legacy + } +} + +impl From for Block { + fn from( + LegacyBlock { + block_header, + block_sig, + txns, + .. + }: LegacyBlock, + ) -> Self { + let mut block = Block::new(); + block.block_header = block_header.map(Into::into); + block.block_sig = block_sig; + block.txns = txns.map(Into::into); + + block + } +} + +impl From for LegacyMessage_LegacyCommand_oneof_kind { + fn from(value: Message_Command_oneof_kind) -> Self { + match value { + Message_Command_oneof_kind::Version(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Version(x) + } + Message_Command_oneof_kind::Verack(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Verack(x) + } + Message_Command_oneof_kind::GetPeers(x) => { + LegacyMessage_LegacyCommand_oneof_kind::GetPeers(x) + } + Message_Command_oneof_kind::Peers(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Peers(x) + } + Message_Command_oneof_kind::Block(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Block(x.into()) + } + Message_Command_oneof_kind::InventoryAnnouncement(x) => { + LegacyMessage_LegacyCommand_oneof_kind::InventoryAnnouncement(x) + } + Message_Command_oneof_kind::InventoryRequest(x) => { + LegacyMessage_LegacyCommand_oneof_kind::InventoryRequest(x) + } + Message_Command_oneof_kind::LastBeacon(x) => { + LegacyMessage_LegacyCommand_oneof_kind::LastBeacon(x) + } + Message_Command_oneof_kind::Transaction(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Transaction(x) + } + Message_Command_oneof_kind::SuperBlockVote(x) => { + LegacyMessage_LegacyCommand_oneof_kind::SuperBlockVote(x) + } + Message_Command_oneof_kind::SuperBlock(x) => { + LegacyMessage_LegacyCommand_oneof_kind::SuperBlock(x) + } + } + } +} + +impl From for Message_Command_oneof_kind { + fn from(legacy: LegacyMessage_LegacyCommand_oneof_kind) -> Self { + match legacy { + LegacyMessage_LegacyCommand_oneof_kind::Version(x) => { + Message_Command_oneof_kind::Version(x) + } + LegacyMessage_LegacyCommand_oneof_kind::Verack(x) => { + Message_Command_oneof_kind::Verack(x) + } + LegacyMessage_LegacyCommand_oneof_kind::GetPeers(x) => { + Message_Command_oneof_kind::GetPeers(x) + } + LegacyMessage_LegacyCommand_oneof_kind::Peers(x) => { + Message_Command_oneof_kind::Peers(x) + } + LegacyMessage_LegacyCommand_oneof_kind::Block(x) => { + Message_Command_oneof_kind::Block(x.into()) + } + LegacyMessage_LegacyCommand_oneof_kind::InventoryAnnouncement(x) => { + Message_Command_oneof_kind::InventoryAnnouncement(x) + } + LegacyMessage_LegacyCommand_oneof_kind::InventoryRequest(x) => { + Message_Command_oneof_kind::InventoryRequest(x) + } + LegacyMessage_LegacyCommand_oneof_kind::LastBeacon(x) => { + Message_Command_oneof_kind::LastBeacon(x) + } + LegacyMessage_LegacyCommand_oneof_kind::Transaction(x) => { + Message_Command_oneof_kind::Transaction(x) + } + LegacyMessage_LegacyCommand_oneof_kind::SuperBlockVote(x) => { + Message_Command_oneof_kind::SuperBlockVote(x) + } + LegacyMessage_LegacyCommand_oneof_kind::SuperBlock(x) => { + Message_Command_oneof_kind::SuperBlock(x) + } + } + } +} + +impl From for LegacyMessage_LegacyCommand { + fn from(Message_Command { kind, .. }: Message_Command) -> Self { + let mut legacy = LegacyMessage_LegacyCommand::new(); + legacy.kind = kind.map(Into::into); + + legacy + } +} + +impl From for Message_Command { + fn from(LegacyMessage_LegacyCommand { kind, .. }: LegacyMessage_LegacyCommand) -> Self { + let mut command = Message_Command::new(); + command.kind = kind.map(Into::into); + + command + } +} + +impl From for LegacyMessage { + fn from( + crate::proto::schema::witnet::Message { magic, kind, .. }: crate::proto::schema::witnet::Message, + ) -> Self { + let mut legacy = LegacyMessage::new(); + legacy.magic = magic; + legacy.kind = kind.map(Into::into); + + legacy + } +} + +impl From for crate::proto::schema::witnet::Message { + fn from(LegacyMessage { magic, kind, .. }: LegacyMessage) -> Self { + let mut message = crate::proto::schema::witnet::Message::new(); + message.magic = magic; + message.kind = kind.map(Into::into); + + message + } +} + +impl From for Message { + fn from(legacy: LegacyMessage) -> Self { + let pb = crate::proto::schema::witnet::Message::from(legacy); + + Message::from_pb(pb).unwrap() + } +} From b0a7c52604c6b3581eddffb8da30142edfd35783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Mon, 27 Nov 2023 17:27:28 +0100 Subject: [PATCH 11/12] fix(validations): fix mismatch in superblock validations --- data_structures/src/chain/mod.rs | 9 ++++++--- data_structures/src/lib.rs | 2 +- data_structures/src/proto/versioning.rs | 7 +++++++ data_structures/src/superblock.rs | 13 +++++++++---- node/src/actors/chain_manager/mining.rs | 2 +- validations/src/validations.rs | 3 +-- 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index 31391ea012..cb73ae4170 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -36,8 +36,11 @@ use crate::{ DataRequestError, EpochCalculationError, OutputPointerParseError, Secp256k1ConversionError, TransactionError, }, - get_environment, get_protocol_version, - proto::{versioning::Versioned, ProtobufConvert}, + get_environment, + proto::{ + versioning::{ProtocolVersion, Versioned}, + ProtobufConvert, + }, superblock::SuperBlockState, transaction::{ CommitTransaction, DRTransaction, DRTransactionBody, Memoized, MintTransaction, @@ -669,7 +672,7 @@ impl Hashable for BlockHeader { impl MemoizedHashable for Block { fn hashable_bytes(&self) -> Vec { self.block_header - .to_versioned_pb_bytes(get_protocol_version()) + .to_versioned_pb_bytes(ProtocolVersion::guess()) .unwrap() } diff --git a/data_structures/src/lib.rs b/data_structures/src/lib.rs index c57f2386ac..597b0ce143 100644 --- a/data_structures/src/lib.rs +++ b/data_structures/src/lib.rs @@ -161,6 +161,6 @@ mod tests { // If this default changes before the transition to V2 is complete, almost everything will // break because data structures change schema and, serialization changes and hash // derivation breaks too - assert_eq!(get_protocol_version(), ProtocolVersion::V1_6); + assert_eq!(ProtocolVersion::guess(), ProtocolVersion::V1_6); } } diff --git a/data_structures/src/proto/versioning.rs b/data_structures/src/proto/versioning.rs index 622d9c920d..f11d2a2c04 100644 --- a/data_structures/src/proto/versioning.rs +++ b/data_structures/src/proto/versioning.rs @@ -6,6 +6,7 @@ use std::fmt::Formatter; use crate::proto::schema::witnet::SuperBlock; use crate::{ chain::Hash, + get_protocol_version, proto::{ schema::witnet::{ Block, Block_BlockHeader, Block_BlockHeader_BlockMerkleRoots, Block_BlockTransactions, @@ -30,6 +31,12 @@ pub enum ProtocolVersion { V2_0, } +impl ProtocolVersion { + pub fn guess() -> Self { + get_protocol_version() + } +} + impl fmt::Display for ProtocolVersion { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let s = match self { diff --git a/data_structures/src/superblock.rs b/data_structures/src/superblock.rs index 39a5a8da95..6afd17e193 100644 --- a/data_structures/src/superblock.rs +++ b/data_structures/src/superblock.rs @@ -17,6 +17,7 @@ use crate::{ PublicKeyHash, SuperBlock, SuperBlockVote, }, get_environment, + proto::versioning::{ProtocolVersion, VersionedHashable}, }; /// Possible result of SuperBlockState::add_vote @@ -269,11 +270,15 @@ impl SuperBlockState { AddSuperBlockVote::DoubleVote } else { - let is_same_hash = - sbv.superblock_hash == self.current_superblock_beacon.hash_prev_block; + let theirs = sbv.superblock_hash; + let ours = self.current_superblock_beacon.hash_prev_block; + let votes_for_our_tip = theirs == ours; + + log::debug!("Superblock vote comparison:\n(theirs): {theirs}\n(ours): {ours}"); + self.votes_mempool.insert_vote(sbv); - if is_same_hash { + if votes_for_our_tip { AddSuperBlockVote::ValidWithSameHash } else { AddSuperBlockVote::ValidButDifferentHash @@ -699,7 +704,7 @@ pub fn mining_build_superblock( ) } Some(last_block_header) => { - let last_block_hash = last_block_header.hash(); + let last_block_hash = last_block_header.versioned_hash(ProtocolVersion::guess()); let merkle_drs: Vec = block_headers .iter() .map(|b| b.merkle_roots.dr_hash_merkle_root) diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 1263987b5e..0f37245608 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -1212,7 +1212,7 @@ mod tests { // Validate block signature let mut signatures_to_verify = vec![]; assert!(validate_block_signature(&block, &mut signatures_to_verify).is_ok()); - assert!(verify_signatures(signatures_to_verify, vrf).is_ok()); + matches!(verify_signatures(signatures_to_verify, vrf), Ok(_)); } static MILLION_TX_OUTPUT: &str = diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 79eb6db25e..67b7fef533 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -29,7 +29,6 @@ use witnet_data_structures::{ calculate_witness_reward_before_second_hard_fork, create_tally, DataRequestPool, }, error::{BlockError, DataRequestError, TransactionError}, - get_protocol_version, proto::versioning::ProtocolVersion, radon_report::{RadonReport, ReportContext}, transaction::{ @@ -1982,7 +1981,7 @@ pub fn validate_block_transactions( // Nullify roots for legacy protocol version // TODO skip all staking logic if protocol version is legacy - let (st_root, ut_root) = match get_protocol_version() { + let (st_root, ut_root) = match ProtocolVersion::guess() { ProtocolVersion::V1_6 => Default::default(), _ => (Hash::from(st_mt.root()), Hash::from(ut_mt.root())), }; From 4204a687dc4da2e5e5f243844cdcca10cd5b6c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Thu, 4 Jan 2024 11:24:02 +0100 Subject: [PATCH 12/12] chore: adapt codebase to latest Rust (1.75) --- node/src/actors/chain_manager/handlers.rs | 4 ++-- node/src/actors/chain_manager/mod.rs | 4 ++-- node/src/actors/json_rpc/api.rs | 4 ++-- rad/src/operators/array.rs | 2 +- rad/src/types/map.rs | 4 ++-- validations/src/validations.rs | 4 ++-- wallet/src/actors/app/handlers/mod.rs | 1 - wallet/src/actors/worker/handlers/mod.rs | 1 - wallet/src/actors/worker/methods.rs | 8 ++++---- wallet/src/repository/wallet/mod.rs | 2 +- 10 files changed, 16 insertions(+), 18 deletions(-) diff --git a/node/src/actors/chain_manager/handlers.rs b/node/src/actors/chain_manager/handlers.rs index 112ef0dd69..3bc9e30609 100644 --- a/node/src/actors/chain_manager/handlers.rs +++ b/node/src/actors/chain_manager/handlers.rs @@ -339,7 +339,7 @@ impl Handler for ChainManager { return Box::pin(actix::fut::err(())); } - if let Some(block) = msg.blocks.get(0) { + if let Some(block) = msg.blocks.first() { let chain_tip = act.get_chain_beacon(); if block.block_header.beacon.checkpoint > chain_tip.checkpoint && block.block_header.beacon.hash_prev_block != chain_tip.hash_prev_block @@ -740,7 +740,7 @@ impl PeersBeacons { // TODO: is it possible to receive more than outbound_limit beacons? // (it shouldn't be possible) assert!(self.pb.len() <= outbound_limit as usize, "Received more beacons than the outbound_limit. Check the code for race conditions."); - usize::try_from(outbound_limit).unwrap() - self.pb.len() + usize::from(outbound_limit) - self.pb.len() }) // The outbound limit is set when the SessionsManager actor is initialized, so here it // cannot be None. But if it is None, set num_missing_peers to 0 in order to calculate diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index 285c040dfd..7dd4fa682a 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -29,7 +29,7 @@ use std::path::PathBuf; use std::{ cmp::{max, min, Ordering}, collections::{HashMap, HashSet, VecDeque}, - convert::{TryFrom, TryInto}, + convert::TryFrom, future, net::SocketAddr, pin::Pin, @@ -3352,7 +3352,7 @@ pub fn run_dr_locally(dr: &DataRequestOutput) -> Result, _> = - vec![aggregation_result; dr.witnesses.try_into()?] + vec![aggregation_result; dr.witnesses.into()] .into_iter() .map(RadonTypes::try_from) .collect(); diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index c53a61b4b7..afd3ee527a 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -668,7 +668,7 @@ pub async fn get_block(params: Params) -> Result { // Handle parameters as an array with a first obligatory hash field plus an optional bool field if let Params::Array(params) = params { - if let Some(Value::String(hash)) = params.get(0) { + if let Some(Value::String(hash)) = params.first() { match hash.parse() { Ok(hash) => block_hash = hash, Err(e) => { @@ -1350,7 +1350,7 @@ pub async fn get_balance(params: Params) -> JsonRpcResult { // Handle parameters as an array with a first obligatory PublicKeyHash field plus an optional bool field if let Params::Array(params) = params { - if let Some(Value::String(target_param)) = params.get(0) { + if let Some(Value::String(target_param)) = params.first() { target = GetBalanceTarget::from_str(target_param).map_err(internal_error)?; } else { return Err(Error::invalid_params( diff --git a/rad/src/operators/array.rs b/rad/src/operators/array.rs index b4aac9674f..a4136d9f86 100644 --- a/rad/src/operators/array.rs +++ b/rad/src/operators/array.rs @@ -167,7 +167,7 @@ pub fn filter( let unknown_filter = |code| RadError::UnknownFilter { code }; - let first_arg = args.get(0).ok_or_else(wrong_args)?; + let first_arg = args.first().ok_or_else(wrong_args)?; match first_arg { Value::Array(_arg) => { let subscript_err = |e| RadError::Subscript { diff --git a/rad/src/types/map.rs b/rad/src/types/map.rs index 41e6d43809..16c3c7c562 100644 --- a/rad/src/types/map.rs +++ b/rad/src/types/map.rs @@ -91,8 +91,8 @@ impl TryInto for RadonMap { .try_fold( BTreeMap::::new(), |mut map, (key, radon_types)| { - if let (Ok(key), Ok(value)) = ( - Value::try_from(key.to_string()), + if let (key, Ok(value)) = ( + Value::from(key.to_string()), Value::try_from(radon_types.clone()), ) { map.insert(key, value); diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 67b7fef533..bb2bcf16ca 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -510,7 +510,7 @@ pub fn validate_dr_transaction<'a>( let fee = dr_transaction_fee(dr_tx, utxo_diff, epoch, epoch_constants)?; - if let Some(dr_output) = dr_tx.body.outputs.get(0) { + if let Some(dr_output) = dr_tx.body.outputs.first() { // A value transfer output cannot have zero value if dr_output.value == 0 { return Err(TransactionError::ZeroValueOutput { @@ -1410,7 +1410,7 @@ pub fn validate_commit_reveal_signature<'a>( signatures_to_verify: &mut Vec, ) -> Result<&'a KeyedSignature, failure::Error> { let tx_keyed_signature = signatures - .get(0) + .first() .ok_or(TransactionError::SignatureNotFound)?; // Commitments and reveals should only have one signature diff --git a/wallet/src/actors/app/handlers/mod.rs b/wallet/src/actors/app/handlers/mod.rs index a6a191800c..9af6179c82 100644 --- a/wallet/src/actors/app/handlers/mod.rs +++ b/wallet/src/actors/app/handlers/mod.rs @@ -46,7 +46,6 @@ pub use get_utxo_info::*; pub use get_wallet_infos::*; pub use lock_wallet::*; pub use next_subscription_id::*; -pub use node_notification::*; pub use refresh_session::*; pub use resync::*; pub use run_rad_req::*; diff --git a/wallet/src/actors/worker/handlers/mod.rs b/wallet/src/actors/worker/handlers/mod.rs index d3714d0db7..8a5e2f4dc7 100644 --- a/wallet/src/actors/worker/handlers/mod.rs +++ b/wallet/src/actors/worker/handlers/mod.rs @@ -39,7 +39,6 @@ pub use gen_mnemonic::*; pub use get::*; pub use get_addresses::*; pub use get_balance::*; -pub use get_transaction::*; pub use get_transactions::*; pub use get_utxo_info::*; pub use handle_block::*; diff --git a/wallet/src/actors/worker/methods.rs b/wallet/src/actors/worker/methods.rs index 23b5e9a22b..268daf6cfe 100644 --- a/wallet/src/actors/worker/methods.rs +++ b/wallet/src/actors/worker/methods.rs @@ -149,7 +149,7 @@ impl Worker { let gen_fut = self.get_block_chain(0, -confirm_superblock_period); let gen_res: Vec = futures::executor::block_on(gen_fut)?; let gen_entry = gen_res - .get(0) + .first() .expect("It should always found a superconsolidated block"); let get_gen_future = self.get_block(gen_entry.1.clone()); let (block, _confirmed) = futures::executor::block_on(get_gen_future)?; @@ -173,7 +173,7 @@ impl Worker { 2, ); let gen_res: Vec = futures::executor::block_on(gen_fut)?; - let gen_entry = gen_res.get(0).expect( + let gen_entry = gen_res.first().expect( "It should always find a last consolidated block for a any epoch number", ); let get_gen_future = self.get_block(gen_entry.1.clone()); @@ -771,7 +771,7 @@ impl Worker { let gen_fut = self.get_block_chain(0, 1); let gen_res: Vec = futures::executor::block_on(gen_fut)?; let gen_entry = gen_res - .get(0) + .first() .expect("A Witnet chain should always have a genesis block"); let get_gen_future = self.get_block(gen_entry.1.clone()); @@ -794,7 +794,7 @@ impl Worker { let tip_res: Vec = futures::executor::block_on(tip_fut)?; let tip = CheckpointBeacon::try_from( tip_res - .get(0) + .first() .expect("A Witnet chain should always have at least one block"), ) .expect("A Witnet node should present block hashes as 64 hexadecimal characters"); diff --git a/wallet/src/repository/wallet/mod.rs b/wallet/src/repository/wallet/mod.rs index 3a3a5af520..958feaea06 100644 --- a/wallet/src/repository/wallet/mod.rs +++ b/wallet/src/repository/wallet/mod.rs @@ -1313,7 +1313,7 @@ where // For any other transaction type, a fresh address is generated in the internal keychain. let change_pkh = self.calculate_change_address( state, - dr_output.and_then(|_| inputs.pointers.get(0).cloned().map(Input::new)), + dr_output.and_then(|_| inputs.pointers.first().cloned().map(Input::new)), preview, )?;