From bca44015e92292239a449151961efa046cbf492b 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/32] 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 adb1f0574a4d42fcd9b784e35464e5b62718816b 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/32] 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 264649bcdc..29e57caeb2 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 22c66723de8bd5579db10cc59486bceef3be1619 Mon Sep 17 00:00:00 2001 From: tommytrg Date: Mon, 9 Oct 2023 18:29:38 +0200 Subject: [PATCH 03/32] 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 8d309b31eb..05e14a689a 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -1722,6 +1722,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.10" @@ -5091,7 +5100,7 @@ dependencies = [ "failure", "futures 0.3.30", "hex", - "itertools", + "itertools 0.8.2", "lazy_static", "log 0.4.20", "num-format", @@ -5207,7 +5216,7 @@ dependencies = [ "failure", "futures 0.3.30", "hex", - "itertools", + "itertools 0.8.2", "lazy_static", "log 0.4.20", "num-traits", @@ -5273,7 +5282,7 @@ dependencies = [ "futures-util", "glob", "hex", - "itertools", + "itertools 0.8.2", "jsonrpc-core 18.0.0", "jsonrpc-pubsub 18.0.0", "log 0.4.20", @@ -5396,7 +5405,7 @@ dependencies = [ "bencher", "failure", "hex", - "itertools", + "itertools 0.11.0", "log 0.4.20", "url", "witnet_config", @@ -5420,7 +5429,7 @@ dependencies = [ "futures 0.3.30", "futures-util", "hex", - "itertools", + "itertools 0.8.2", "jsonrpc-core 15.1.0", "jsonrpc-pubsub 15.1.0", "log 0.4.20", 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 2ed59872c0..28ab200292 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) @@ -375,8 +404,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(), @@ -1129,6 +1156,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, @@ -1718,6 +1798,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(), @@ -1726,6 +1864,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 { @@ -1894,6 +2033,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()), } } @@ -2166,6 +2313,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 451586ad04..20e9f826a6 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 fca76af932f7ec002c5845c6c6b682c5e99448f6 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/32] 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 a408bc48d8..95992fa25a 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 28ab200292..4d590a7605 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()) @@ -1156,6 +1151,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, @@ -1163,23 +1167,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( @@ -1192,10 +1186,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)?; @@ -1840,7 +1833,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 20e9f826a6..b2e469e5a0 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 824166ae7823a3f7b35a45747534313c3789c972 Mon Sep 17 00:00:00 2001 From: tommytrg Date: Fri, 20 Oct 2023 12:18:54 +0200 Subject: [PATCH 05/32] 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 4d590a7605..aae6c7afdc 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) @@ -1202,6 +1221,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, @@ -1849,6 +1944,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(), @@ -1858,6 +1990,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 { @@ -2307,6 +2440,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 b2e469e5a0..958feaea06 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 10472e5ddeb1fb1372333ef6c456065614f1929b 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/32] 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 bc2c486022..f9b0e7c2dc 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 6009e9b20c5410ddb42d53b03d2ee8266adc076d 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/32] 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 95992fa25a..afd3ee527a 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 aae6c7afdc..508532aa4e 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, @@ -1595,7 +1596,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 25e1f105e4f6b104208632ac219b69cad4ad4f16 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/32] 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 188f4627bfb46b2063b933e93fb9926c257b71e7 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/32] 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 e45a46bcb3..3bc9e30609 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 f9b0e7c2dc..47e7379f4e 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 508532aa4e..14c49c1155 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, @@ -1318,6 +1320,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); @@ -1887,6 +1891,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; @@ -1943,8 +1949,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; @@ -1980,7 +1984,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 { @@ -1990,8 +1999,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 d307d68a21ecc42c5bd5a0ac056d1547d950f25c 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/32] 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 ced013326638109a11ad9c2e864424509a683ec0 Mon Sep 17 00:00:00 2001 From: tommytrg Date: Tue, 21 Nov 2023 18:43:36 +0100 Subject: [PATCH 11/32] feat(jsonrpc): implement method for staking --- node/src/actors/chain_manager/handlers.rs | 86 ++++++++++++++++++++--- node/src/actors/json_rpc/api.rs | 42 ++++++++++- node/src/actors/messages.rs | 27 ++++++- 3 files changed, 141 insertions(+), 14 deletions(-) diff --git a/node/src/actors/chain_manager/handlers.rs b/node/src/actors/chain_manager/handlers.rs index 3bc9e30609..95d19597de 100644 --- a/node/src/actors/chain_manager/handlers.rs +++ b/node/src/actors/chain_manager/handlers.rs @@ -9,14 +9,17 @@ use std::{ use actix::{prelude::*, ActorFutureExt, WrapFuture}; use futures::future::Either; -use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE; +use witnet_config::defaults::{ + PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT, + PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE, +}; use witnet_data_structures::{ chain::{ tapi::ActiveWips, Block, ChainState, CheckpointBeacon, DataRequestInfo, Epoch, Hash, Hashable, NodeStats, PublicKeyHash, SuperBlockVote, SupplyInfo, }, error::{ChainInfoError, TransactionError::DataRequestNotFound}, - transaction::{DRTransaction, Transaction, VTTransaction}, + transaction::{DRTransaction, StakeTransaction, Transaction, VTTransaction}, transaction_factory::{self, NodeBalance}, types::LastBeacon, utxo_pool::{get_utxo_info, UtxoInfo}, @@ -29,13 +32,14 @@ use crate::{ chain_manager::{handlers::BlockBatches::*, BlockCandidate}, messages::{ AddBlocks, AddCandidates, AddCommitReveal, AddSuperBlock, AddSuperBlockVote, - AddTransaction, Broadcast, BuildDrt, BuildVtt, EpochNotification, EstimatePriority, - GetBalance, GetBalanceTarget, GetBlocksEpochRange, GetDataRequestInfo, - GetHighestCheckpointBeacon, GetMemoryTransaction, GetMempool, GetMempoolResult, - GetNodeStats, GetReputation, GetReputationResult, GetSignalingInfo, GetState, - GetSuperBlockVotes, GetSupplyInfo, GetUtxoInfo, IsConfirmedBlock, PeersBeacons, - ReputationStats, Rewind, SendLastBeacon, SessionUnitResult, SetLastBeacon, - SetPeersLimits, SignalingInfo, SnapshotExport, SnapshotImport, TryMineBlock, + AddTransaction, Broadcast, BuildDrt, BuildStake, BuildVtt, EpochNotification, + EstimatePriority, GetBalance, GetBalanceTarget, GetBlocksEpochRange, + GetDataRequestInfo, GetHighestCheckpointBeacon, GetMemoryTransaction, GetMempool, + GetMempoolResult, GetNodeStats, GetReputation, GetReputationResult, GetSignalingInfo, + GetState, GetSuperBlockVotes, GetSupplyInfo, GetUtxoInfo, IsConfirmedBlock, + PeersBeacons, ReputationStats, Rewind, SendLastBeacon, SessionUnitResult, + SetLastBeacon, SetPeersLimits, SignalingInfo, SnapshotExport, SnapshotImport, + TryMineBlock, }, sessions_manager::SessionsManager, }, @@ -1288,6 +1292,70 @@ impl Handler for ChainManager { } } +impl Handler for ChainManager { + type Result = ResponseActFuture::Result>; + + fn handle(&mut self, msg: BuildStake, _ctx: &mut Self::Context) -> Self::Result { + if self.sm_state != StateMachine::Synced { + return Box::pin(actix::fut::err( + ChainManagerError::NotSynced { + current_state: self.sm_state, + } + .into(), + )); + } + let timestamp = u64::try_from(get_timestamp()).unwrap(); + match transaction_factory::build_st( + msg.stake_output, + msg.fee, + &mut self.chain_state.own_utxos, + self.own_pkh.unwrap(), + &self.chain_state.unspent_outputs_pool, + timestamp, + self.tx_pending_timeout, + &msg.utxo_strategy, + PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT, + msg.dry_run, + ) { + Err(e) => { + log::error!("Error when building stake transaction: {}", e); + Box::pin(actix::fut::err(e.into())) + } + Ok(st) => { + let fut = signature_mngr::sign_transaction(&st, st.inputs.len()) + .into_actor(self) + .then(move |s, act, _ctx| match s { + Ok(signatures) => { + let st = StakeTransaction::new(st, signatures); + + if msg.dry_run { + Either::Right(actix::fut::result(Ok(st))) + } else { + let transaction = Transaction::Stake(st.clone()); + Either::Left( + act.add_transaction( + AddTransaction { + transaction, + broadcast_flag: true, + }, + get_timestamp(), + ) + .map_ok(move |_, _, _| st), + ) + } + } + Err(e) => { + log::error!("Failed to sign stake transaction: {}", e); + Either::Right(actix::fut::result(Err(e))) + } + }); + + Box::pin(fut) + } + } + } +} + impl Handler for ChainManager { type Result = ResponseActFuture>; diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index afd3ee527a..a31030b195 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -37,8 +37,8 @@ use crate::{ inventory_manager::{InventoryManager, InventoryManagerError}, json_rpc::Subscriptions, messages::{ - AddCandidates, AddPeers, AddTransaction, BuildDrt, BuildVtt, ClearPeers, DropAllPeers, - EstimatePriority, GetBalance, GetBalanceTarget, GetBlocksEpochRange, + AddCandidates, AddPeers, AddTransaction, BuildDrt, BuildStake, BuildVtt, ClearPeers, + DropAllPeers, EstimatePriority, GetBalance, GetBalanceTarget, GetBlocksEpochRange, GetConsolidatedPeers, GetDataRequestInfo, GetEpoch, GetHighestCheckpointBeacon, GetItemBlock, GetItemSuperblock, GetItemTransaction, GetKnownPeers, GetMemoryTransaction, GetMempool, GetNodeStats, GetReputation, GetSignalingInfo, @@ -266,6 +266,14 @@ pub fn attach_sensitive_methods( |params| snapshot_import(params.parse()), )) }); + server.add_actix_method(system, "stake", move |params| { + Box::pin(if_authorized( + enable_sensitive_methods, + "stake", + params, + |params| stake(params.parse()), + )) + }); } fn extract_topic_and_params(params: Params) -> Result<(String, Value), Error> { @@ -1921,6 +1929,36 @@ pub async fn snapshot_import(params: Result) -> Jso // Write the response back (the path to the snapshot file) serde_json::to_value(response).map_err(internal_error_s) } +/// Build a stake transaction +pub async fn stake(params: Result) -> JsonRpcResult { + log::debug!("Creating stake transaction from JSON-RPC."); + + match params { + Ok(msg) => { + ChainManager::from_registry() + .send(msg) + .map(|res| match res { + Ok(Ok(hash)) => match serde_json::to_value(hash) { + Ok(x) => Ok(x), + Err(e) => { + let err = internal_error_s(e); + Err(err) + } + }, + Ok(Err(e)) => { + let err = internal_error_s(e); + Err(err) + } + Err(e) => { + let err = internal_error_s(e); + Err(err) + } + }) + .await + } + Err(err) => Err(err), + } +} #[cfg(test)] mod mock_actix { diff --git a/node/src/actors/messages.rs b/node/src/actors/messages.rs index d11776e2bd..1c2a6ab21c 100644 --- a/node/src/actors/messages.rs +++ b/node/src/actors/messages.rs @@ -22,13 +22,14 @@ use witnet_data_structures::{ tapi::{ActiveWips, BitVotesCounter}, Block, CheckpointBeacon, DataRequestInfo, DataRequestOutput, Epoch, EpochConstants, Hash, InventoryEntry, InventoryItem, NodeStats, PointerToBlock, PublicKeyHash, - PublicKeyHashParseError, RADRequest, RADTally, Reputation, StateMachine, SuperBlock, - SuperBlockVote, SupplyInfo, ValueTransferOutput, + PublicKeyHashParseError, RADRequest, RADTally, Reputation, StakeOutput, StateMachine, + SuperBlock, SuperBlockVote, SupplyInfo, ValueTransferOutput, }, fee::{deserialize_fee_backwards_compatible, Fee}, radon_report::RadonReport, transaction::{ - CommitTransaction, DRTransaction, RevealTransaction, Transaction, VTTransaction, + CommitTransaction, DRTransaction, RevealTransaction, StakeTransaction, Transaction, + VTTransaction, }, transaction_factory::NodeBalance, types::LastBeacon, @@ -220,6 +221,26 @@ impl Message for BuildVtt { type Result = Result; } +/// Builds a `StakeTransaction` from a list of `ValueTransferOutput`s +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct BuildStake { + /// List of `ValueTransferOutput`s + pub stake_output: StakeOutput, + /// Fee + #[serde(default)] + pub fee: Fee, + /// Strategy to sort the unspent outputs pool + #[serde(default)] + pub utxo_strategy: UtxoSelectionStrategy, + /// Construct the transaction but do not broadcast it + #[serde(default)] + pub dry_run: bool, +} + +impl Message for BuildStake { + type Result = Result; +} + /// Builds a `DataRequestTransaction` from a `DataRequestOutput` #[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct BuildDrt { From f5d85a1d1fef7b62796645b9cfd2cf1d1eaa1a51 Mon Sep 17 00:00:00 2001 From: tommytrg Date: Thu, 23 Nov 2023 15:23:05 +0100 Subject: [PATCH 12/32] feat(node): staking CLI client --- Cargo.lock | 56 +++++++++- Cargo.toml | 1 + data_structures/src/chain/mod.rs | 9 ++ node/src/actors/json_rpc/api.rs | 141 +++++++++++++++++++++--- node/src/actors/messages.rs | 63 +++++++++++ src/cli/node/json_rpc_client.rs | 183 +++++++++++++++++++++++++++++-- src/cli/node/with_node.rs | 46 ++++++++ 7 files changed, 475 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05e14a689a..f4af2675a1 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -420,6 +420,12 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + [[package]] name = "byteorder" version = "1.5.0" @@ -505,6 +511,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "checked_int_cast" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" + [[package]] name = "chrono" version = "0.4.34" @@ -563,6 +575,12 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.6" @@ -1591,6 +1609,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-iter", + "num-rational 0.3.2", + "num-traits", +] + [[package]] name = "impl-codec" version = "0.5.1" @@ -2337,7 +2369,7 @@ dependencies = [ "num-complex", "num-integer", "num-iter", - "num-rational", + "num-rational 0.4.1", "num-traits", ] @@ -2397,6 +2429,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -2998,6 +3041,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "qrcode" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" +dependencies = [ + "checked_int_cast", + "image", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -5105,6 +5158,7 @@ dependencies = [ "log 0.4.20", "num-format", "prettytable-rs", + "qrcode", "sentry", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 216d3f6aee..826fd49c8f 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ lazy_static = "1.4.0" log = "0.4.8" num-format = "0.4.0" prettytable-rs = { version = "0.10.0", default-features = false } +qrcode = "0.12" sentry = { version = "0.29.3", features = ["log"], optional = true } serde_json = "1.0.47" structopt = "0.3.9" diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index 31391ea012..6d752a6a01 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -1597,6 +1597,15 @@ impl PublicKey { } } + pub fn from_str(serialized: &str) -> Self { + let mut pk = hex::decode(serialized).unwrap(); + pk.resize(33, 0); + let mut array_bytes = [0u8; 33]; + array_bytes.copy_from_slice(&pk[..33]); + + Self::from_bytes(array_bytes) + } + /// Returns the PublicKeyHash related to the PublicKey pub fn pkh(&self) -> PublicKeyHash { PublicKeyHash::from_public_key(self) diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index a31030b195..4d8d9982a8 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -19,12 +19,11 @@ use itertools::Itertools; use jsonrpc_core::{BoxFuture, Error, Params, Value}; use jsonrpc_pubsub::{Subscriber, SubscriptionId}; use serde::{Deserialize, Serialize}; - -use witnet_crypto::key::KeyPath; +use witnet_crypto::{key::KeyPath, secp256k1::ecdsa::Signature}; use witnet_data_structures::{ chain::{ - tapi::ActiveWips, Block, DataRequestOutput, Epoch, Hash, Hashable, PublicKeyHash, RADType, - StateMachine, SyncStatus, + tapi::ActiveWips, Block, DataRequestOutput, Environment, Epoch, Hash, Hashable, + KeyedSignature, PublicKey, PublicKeyHash, RADType, StakeOutput, StateMachine, SyncStatus, }, transaction::Transaction, vrf::VrfMessage, @@ -37,13 +36,14 @@ use crate::{ inventory_manager::{InventoryManager, InventoryManagerError}, json_rpc::Subscriptions, messages::{ - AddCandidates, AddPeers, AddTransaction, BuildDrt, BuildStake, BuildVtt, ClearPeers, - DropAllPeers, EstimatePriority, GetBalance, GetBalanceTarget, GetBlocksEpochRange, - GetConsolidatedPeers, GetDataRequestInfo, GetEpoch, GetHighestCheckpointBeacon, - GetItemBlock, GetItemSuperblock, GetItemTransaction, GetKnownPeers, - GetMemoryTransaction, GetMempool, GetNodeStats, GetReputation, GetSignalingInfo, - GetState, GetSupplyInfo, GetUtxoInfo, InitializePeers, IsConfirmedBlock, Rewind, - SnapshotExport, SnapshotImport, + AddCandidates, AddPeers, AddTransaction, AuthorizationParams, AuthorizeStake, BuildDrt, + BuildStake, BuildStakeParams, BuildVtt, ClearPeers, DropAllPeers, EstimatePriority, + GetBalance, GetBalanceTarget, GetBlocksEpochRange, GetConsolidatedPeers, + GetDataRequestInfo, GetEpoch, GetHighestCheckpointBeacon, GetItemBlock, + GetItemSuperblock, GetItemTransaction, GetKnownPeers, GetMemoryTransaction, GetMempool, + GetNodeStats, GetReputation, GetSignalingInfo, GetState, GetSupplyInfo, GetUtxoInfo, + InitializePeers, IsConfirmedBlock, Rewind, SnapshotExport, SnapshotImport, + StakeAuthorization, }, peers_manager::PeersManager, sessions_manager::SessionsManager, @@ -274,6 +274,15 @@ pub fn attach_sensitive_methods( |params| stake(params.parse()), )) }); + + server.add_actix_method(system, "authorizeStake", move |params: Params| { + Box::pin(if_authorized( + enable_sensitive_methods, + "authorizeStake", + params, + |params| authorize_stake(params.parse()), + )) + }); } fn extract_topic_and_params(params: Params) -> Result<(String, Value), Error> { @@ -1930,13 +1939,63 @@ pub async fn snapshot_import(params: Result) -> Jso serde_json::to_value(response).map_err(internal_error_s) } /// Build a stake transaction -pub async fn stake(params: Result) -> JsonRpcResult { +pub async fn stake(params: Result) -> JsonRpcResult { log::debug!("Creating stake transaction from JSON-RPC."); match params { Ok(msg) => { + let withdrawer = match msg.withdrawer { + Some(withdrawer) => withdrawer, + None => { + let pk = signature_mngr::public_key() + .map(|res| { + res.map_err(internal_error) + .map(|pk| pk.pkh().bech32(Environment::Mainnet)) + }) + .await; + + pk.unwrap() + } + }; + + let authorization: AuthorizationParams = match msg.authorization { + Some(authorization) => authorization, + None => { + let mut data = [0u8; witnet_crypto::secp256k1::constants::MESSAGE_SIZE]; + data[0..20].clone_from_slice(withdrawer.as_ref()); + + let keyed_signature = signature_mngr::sign_data(data) + .map(|res| res.map_err(internal_error)) + .await + .unwrap(); + + AuthorizationParams { + authorization: hex::encode(keyed_signature.signature.to_bytes().unwrap()), + public_key: hex::encode(keyed_signature.public_key.to_bytes()), + } + } + }; + + let signature = Signature::from_str(&authorization.authorization).unwrap(); + let authorization = KeyedSignature { + signature: signature.into(), + // TODO: https://docs.rs/secp256k1/0.22.2/secp256k1/struct.Secp256k1.html#method.recover_ecdsa + // public_key: signature.recover_ecdsa(withdrawer, signature) + public_key: PublicKey::from_str(&authorization.public_key), + }; + + let build_stake = BuildStake { + dry_run: msg.dry_run, + fee: msg.fee, + utxo_strategy: msg.utxo_strategy, + stake_output: StakeOutput { + authorization, + value: msg.value, + }, + }; + ChainManager::from_registry() - .send(msg) + .send(build_stake) .map(|res| match res { Ok(Ok(hash)) => match serde_json::to_value(hash) { Ok(x) => Ok(x), @@ -1960,6 +2019,58 @@ pub async fn stake(params: Result) -> JsonRpcResult { } } +/// Create a stake authorization for the given address. +/// +/// The output of this method is a required argument to call the Stake method. +/* test +{"jsonrpc": "2.0","method": "authorizeStake", "params": {"withdrawer":"wit1lkzl4a365fvrr604pwqzykxugpglkrp5ekj0k0"}, "id": "1"} +*/ +pub async fn authorize_stake(params: Result) -> JsonRpcResult { + print!("Inside authorize_stake"); + log::debug!("Creating an authorization stake from JSON-RPC."); + match params { + Ok(msg) => { + let mut withdrawer = msg.withdrawer; + if withdrawer.is_none() { + let pk = signature_mngr::public_key() + .map(|res| { + res.map_err(internal_error) + .map(|pk| pk.pkh().bech32(Environment::Mainnet)) + }) + .await; + withdrawer = Some(pk.unwrap()); + } + // FIXME: we could use directly the pk calculated above in one case + let pkh = + PublicKeyHash::from_bech32(Environment::Mainnet, &withdrawer.clone().unwrap()) + .unwrap(); + let mut data = [0u8; witnet_crypto::secp256k1::constants::MESSAGE_SIZE]; + data[0..20].clone_from_slice(pkh.as_ref()); + + signature_mngr::sign_data(data) + .map(|res| { + res.map_err(internal_error).and_then(|ks| { + let a = StakeAuthorization { + withdrawer: withdrawer.unwrap(), + signature: hex::encode(ks.signature.to_bytes().unwrap()), + public_key: hex::encode(ks.public_key.to_bytes()), + }; + + match serde_json::to_value(a) { + Ok(value) => Ok(value), + Err(e) => { + let err = internal_error_s(e); + Err(err) + } + } + }) + }) + .await + } + Err(err) => Err(err), + } +} + #[cfg(test)] mod mock_actix { use actix::{MailboxError, Message}; @@ -2264,6 +2375,7 @@ mod tests { all_methods_vec, vec![ "addPeers", + "authorizeStake", "chainExport", "chainImport", "clearPeers", @@ -2294,6 +2406,7 @@ mod tests { "sendValue", "sign", "signalingInfo", + "stake", "syncStatus", "tryRequest", "witnet_subscribe", @@ -2312,6 +2425,7 @@ mod tests { let expected_sensitive_methods = vec![ "addPeers", + "authorizeStake", "clearPeers", "createVRF", "getPkh", @@ -2324,6 +2438,7 @@ mod tests { "sendValue", "sign", "tryRequest", + "stake", ]; for method_name in expected_sensitive_methods { diff --git a/node/src/actors/messages.rs b/node/src/actors/messages.rs index 1c2a6ab21c..fa78be1827 100644 --- a/node/src/actors/messages.rs +++ b/node/src/actors/messages.rs @@ -241,6 +241,69 @@ impl Message for BuildStake { type Result = Result; } +/// Authorization formatted as strings +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct AuthorizationParams { + /// Authorization public key + pub public_key: String, + /// Authorization signature + pub authorization: String, +} + +/// Builds a `StakeTransaction` from a list of `ValueTransferOutput`s +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct BuildStakeParams { + /// Authorization signature and public key + #[serde(default)] + pub authorization: Option, + /// List of `ValueTransferOutput`s + #[serde(default)] + pub value: u64, + /// Withdrawer + #[serde(default)] + pub withdrawer: Option, + /// Fee + #[serde(default)] + pub fee: Fee, + /// Strategy to sort the unspent outputs pool + #[serde(default)] + pub utxo_strategy: UtxoSelectionStrategy, + /// Construct the transaction but do not broadcast it + #[serde(default)] + pub dry_run: bool, +} + +// impl Message for BuildStake { +// type Result = Result; +// } + +/// Builds an `AuthorizeStake` +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct AuthorizeStake { + /// Address that can withdraw the stake + #[serde(default)] + pub withdrawer: Option, +} + +impl Message for AuthorizeStake { + type Result = Result; +} + +/// Builds an `StakeAuthorization` +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct StakeAuthorization { + /// Address that can withdraw the stake + pub withdrawer: String, + /// Signature of the withdrawer + pub signature: String, + /// Public key related with signature + pub public_key: String, +} + +impl Message for StakeAuthorization { + type Result = Result; +} + /// Builds a `DataRequestTransaction` from a `DataRequestOutput` #[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct BuildDrt { diff --git a/src/cli/node/json_rpc_client.rs b/src/cli/node/json_rpc_client.rs index c1a9ee6c92..2a8b6ae3f3 100644 --- a/src/cli/node/json_rpc_client.rs +++ b/src/cli/node/json_rpc_client.rs @@ -1,3 +1,10 @@ +use ansi_term::Color::{Purple, Red, White, Yellow}; +use failure::{bail, Fail}; +use itertools::Itertools; +use num_format::{Locale, ToFormattedString}; +use prettytable::{row, Table}; +use qrcode::render::unicode; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ collections::HashMap, convert::TryFrom, @@ -8,13 +15,6 @@ use std::{ path::Path, str::FromStr, }; - -use ansi_term::Color::{Purple, Red, White, Yellow}; -use failure::{bail, Fail}; -use itertools::Itertools; -use num_format::{Locale, ToFormattedString}; -use prettytable::{row, Table}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; use witnet_crypto::{ hash::calculate_sha256, @@ -30,7 +30,7 @@ use witnet_data_structures::{ }, fee::Fee, proto::ProtobufConvert, - transaction::{DRTransaction, Transaction, VTTransaction}, + transaction::{DRTransaction, StakeTransaction, Transaction, VTTransaction}, transaction_factory::NodeBalance, types::SequentialId, utxo_pool::{UtxoInfo, UtxoSelectionStrategy}, @@ -39,7 +39,10 @@ use witnet_data_structures::{ use witnet_node::actors::{ chain_manager::run_dr_locally, json_rpc::api::{AddrType, GetBlockChainParams, GetTransactionOutput, PeersResult}, - messages::{BuildDrt, BuildVtt, GetBalanceTarget, GetReputationResult, SignalingInfo}, + messages::{ + AuthorizationParams, AuthorizeStake, BuildDrt, BuildStakeParams, BuildVtt, + GetBalanceTarget, GetReputationResult, SignalingInfo, StakeAuthorization, + }, }; use witnet_rad::types::RadonTypes; use witnet_util::{files::create_private_file, timestamp::pretty_print}; @@ -855,6 +858,167 @@ pub fn send_dr( Ok(()) } +pub fn send_st( + addr: SocketAddr, + value: u64, + withdrawer: Option, + fee: Option, + sorted_bigger: Option, + dry_run: bool, +) -> Result<(), failure::Error> { + let mut stream = start_client(addr)?; + let mut id = SequentialId::initialize(1u8); + + let authorize_stake_params = AuthorizeStake { withdrawer }; + + let (stake_authorization, (_, _response)): (StakeAuthorization, _) = issue_method( + "authorizeStake", + Some(authorize_stake_params), + &mut stream, + id.next(), + )?; + + // Prepare for fee estimation if no fee value was specified + let (fee, estimate) = unwrap_fee_or_estimate_priority(fee, &mut stream, &mut id)?; + + let utxo_strategy = match sorted_bigger { + Some(true) => UtxoSelectionStrategy::BigFirst { from: None }, + Some(false) => UtxoSelectionStrategy::SmallFirst { from: None }, + None => UtxoSelectionStrategy::Random { from: None }, + }; + + let mut build_stake_params = BuildStakeParams { + authorization: Some(AuthorizationParams { + authorization: stake_authorization.signature, + public_key: stake_authorization.public_key, + }), + withdrawer: Some(stake_authorization.withdrawer), + value, + fee, + utxo_strategy, + dry_run, + }; + + // If no fee was specified, we first need to do a dry run for each of the priority tiers to + // find out the actual transaction weight (as different priorities will affect the number + // of inputs being used, and thus also the weight). + if let Some(PrioritiesEstimate { + vtt_stinky, + vtt_low, + vtt_medium, + vtt_high, + vtt_opulent, + .. + }) = estimate + { + let priorities = vec![ + (vtt_stinky, "Stinky"), + (vtt_low, "Low"), + (vtt_medium, "Medium"), + (vtt_high, "High"), + (vtt_opulent, "Opulent"), + ]; + let mut estimates = vec![]; + let mut fee; + + // Iterative algorithm for transaction weight discovery. It calculates the fees for this + // transaction assuming that it has the minimum weight, and then repeats the estimation + // using the actual weight of the latest created transaction, until the weight stabilizes + // or after 5 rounds. + for ( + PriorityEstimate { + priority, + time_to_block, + }, + label, + ) in priorities + { + // The minimum ST size is N*133+M*36+105` where `N` is the number of `inputs`, and `M` + // is 0 or 1 depending on whether a `change` output is used + let mut weight = 238u32; + let mut rounds = 0u8; + // Iterative algorithm for weight discovery + loop { + // Calculate fee for current priority and weight + fee = Fee::absolute_from_wit(priority.derive_fee_wit(weight)); + + // Create and dry run a Stake transaction using that fee + let dry_params = BuildStakeParams { + fee, + dry_run: true, + ..build_stake_params.clone() + }; + let (dry_st, ..): (StakeTransaction, _) = + issue_method("stake", Some(dry_params), &mut stream, id.next())?; + let dry_weight = dry_st.weight(); + + // We retry up to 5 times, or until the weight is stable + if rounds > 5 || dry_weight == weight { + break; + } + + weight = dry_weight; + rounds += 1; + } + + estimates.push((label, priority, fee, time_to_block)); + } + + // We are ready to compose the params for the actual transaction. + build_stake_params.fee = prompt_user_for_priority_selection(estimates)?; + } + + // Finally ask the node to create the transaction with the chosen fee. + let (_st, (request, response)): (StakeTransaction, _) = + issue_method("stake", Some(build_stake_params), &mut stream, id.next())?; + + // On dry run mode, print the request, otherwise, print the response. + // This is kept like this strictly for backwards compatibility. + // TODO: wouldn't it be better to always print the response or both? + if dry_run { + println!("{}", request); + } else { + println!("{}", response); + } + + Ok(()) +} + +pub fn authorize_st(addr: SocketAddr, withdrawer: Option) -> Result<(), failure::Error> { + if withdrawer.is_some() { + // validate withdrawer + PublicKeyHash::from_bech32(Environment::Mainnet, &withdrawer.clone().unwrap())?; + } + + let mut stream = start_client(addr)?; + let mut id = SequentialId::initialize(1u8); + + let params = AuthorizeStake { withdrawer }; + let (authorization, (_, response)): (StakeAuthorization, _) = + issue_method("authorizeStake", Some(params), &mut stream, id.next())?; + + println!("{}", response); + + let mut str = authorization.signature.clone(); + str.push(':'); + str.push_str(&authorization.public_key); + + let auth_qr = qrcode::QrCode::new(str).unwrap(); + let auth_ascii = auth_qr + .render::() + .quiet_zone(true) + .dark_color(unicode::Dense1x2::Light) + .light_color(unicode::Dense1x2::Dark) + .build(); + + println!( + "Authorization code:\n{}\nPublicKey code:\n{}\nQR code for myWitWallet:\n{}", + authorization.signature, authorization.public_key, auth_ascii + ); + + Ok(()) +} + pub fn master_key_export( addr: SocketAddr, write_to_path: Option<&Path>, @@ -1849,7 +2013,6 @@ where id.unwrap_or(1) ); let response = send_request(stream, &request)?; - parse_response::(&response).map(|output| (output, (request, response))) } diff --git a/src/cli/node/with_node.rs b/src/cli/node/with_node.rs index 41321f29c7..408ef70a02 100644 --- a/src/cli/node/with_node.rs +++ b/src/cli/node/with_node.rs @@ -269,6 +269,23 @@ pub fn exec_cmd( Command::Rewind { node, epoch } => rpc::rewind(node.unwrap_or(default_jsonrpc), epoch), Command::SignalingInfo { node } => rpc::signaling_info(node.unwrap_or(default_jsonrpc)), Command::Priority { node, json } => rpc::priority(node.unwrap_or(default_jsonrpc), json), + Command::Stake { + node, + value, + withdrawer, + fee, + dry_run, + } => rpc::send_st( + node.unwrap_or(default_jsonrpc), + value, + withdrawer, + fee.map(Fee::absolute_from_nanowits), + None, + dry_run, + ), + Command::AuthorizeStake { node, withdrawer } => { + rpc::authorize_st(node.unwrap_or(default_jsonrpc), withdrawer) + } } } @@ -730,6 +747,35 @@ pub enum Command { #[structopt(long = "json", help = "Show output in JSON format")] json: bool, }, + #[structopt(name = "stake", about = "Create a stake transaction")] + Stake { + /// Socket address of the Witnet node to query + #[structopt(short = "n", long = "node")] + node: Option, + /// Value + #[structopt(long = "value")] + value: u64, + /// Withdrawer + #[structopt(long = "withdrawer")] + // make it also optional in jsonrcp + // see get balance in main + withdrawer: Option, + /// Fee + #[structopt(long = "fee")] + fee: Option, + /// Print the request that would be sent to the node and exit without doing anything + #[structopt(long = "dry-run")] + dry_run: bool, + }, + #[structopt(name = "authorizeStake", about = "Create an stake authorization")] + AuthorizeStake { + /// Socket address of the Witnet node to query + #[structopt(short = "n", long = "node")] + node: Option, + /// Withdrawer address + #[structopt(long = "withdrawer")] + withdrawer: Option, + }, } #[derive(Debug, StructOpt)] From 33ab7fb963a633d7ccd47764554c06e716746637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Thu, 4 Jan 2024 21:10:55 +0100 Subject: [PATCH 13/32] feat(node): perform further integration of staking methods --- crypto/Cargo.toml | 2 +- data_structures/src/chain/mod.rs | 68 ++++++++-- node/src/actors/json_rpc/api.rs | 213 +++++++++++++------------------ node/src/actors/messages.rs | 73 ++++++----- src/cli/node/json_rpc_client.rs | 30 ++--- 5 files changed, 195 insertions(+), 191 deletions(-) diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index a1831753ab..2b65b8da2d 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -22,7 +22,7 @@ hmac = "0.7.1" memzero = "0.1.0" rand = "0.7.3" ring = "0.16.11" -secp256k1 = { version = "0.22.2", features = ["global-context"] } +secp256k1 = { version = "0.22.2", features = ["global-context", "recovery"] } serde = { version = "1.0.104", optional = true } sha2 = "0.8.1" tiny-bip39 = "0.7.0" diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index 6d752a6a01..1a69aa3f02 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -22,7 +22,12 @@ use witnet_crypto::{ key::ExtendedSK, merkle::merkle_tree_root as crypto_merkle_tree_root, secp256k1::{ - ecdsa::Signature as Secp256k1_Signature, PublicKey as Secp256k1_PublicKey, + self, + PublicKey as Secp256k1_PublicKey, + ecdsa::{ + RecoverableSignature, RecoveryId, + Signature as Secp256k1_Signature, + }, SecretKey as Secp256k1_SecretKey, }, }; @@ -30,7 +35,7 @@ use witnet_protected::Protected; use witnet_reputation::{ActiveReputationSet, TotalReputationSet}; use crate::{ - chain::{tapi::TapiEngine, Signature::Secp256k1}, + chain::{Signature::Secp256k1, tapi::TapiEngine}, data_request::{calculate_reward_collateral_ratio, DataRequestPool}, error::{ DataRequestError, EpochCalculationError, OutputPointerParseError, Secp256k1ConversionError, @@ -45,7 +50,7 @@ use crate::{ UnstakeTransaction, VTTransaction, }, transaction::{ - MemoHash, MemoizedHashable, BETA, COMMIT_WEIGHT, OUTPUT_SIZE, REVEAL_WEIGHT, TALLY_WEIGHT, + BETA, COMMIT_WEIGHT, MemoHash, MemoizedHashable, OUTPUT_SIZE, REVEAL_WEIGHT, TALLY_WEIGHT, }, utxo_pool::{OldUnspentOutputsPool, OwnUnspentOutputsPool, UnspentOutputsPool}, vrf::{BlockEligibilityClaim, DataRequestEligibilityClaim}, @@ -1236,6 +1241,15 @@ pub struct PublicKeyHash { pub(crate) hash: [u8; 20], } +impl PublicKeyHash { + pub fn as_secp256k1_msg(&self) -> [u8; secp256k1::constants::MESSAGE_SIZE] { + let mut msg = [0u8; secp256k1::constants::MESSAGE_SIZE]; + msg[0..20].clone_from_slice(self.as_ref()); + + msg + } +} + impl AsRef<[u8]> for PublicKeyHash { fn as_ref(&self) -> &[u8] { self.hash.as_ref() @@ -1556,6 +1570,33 @@ pub struct KeyedSignature { pub public_key: PublicKey, } +impl KeyedSignature { + pub fn from_recoverable_hex(string: &str, msg: &[u8]) -> Self { + let bytes = hex::decode(string).unwrap(); + + Self::from_recoverable_slice(&bytes, msg) + } + pub fn from_recoverable(recoverable: &RecoverableSignature, message: &[u8]) -> Self { + let msg = secp256k1::Message::from_slice(message).unwrap(); + let signature = recoverable.to_standard(); + let public_key = recoverable.recover(&msg).unwrap(); + + KeyedSignature { + signature: signature.into(), + public_key: public_key.into(), + } + } + + // Recovers a keyed signature from its serialized form and a known message. + pub fn from_recoverable_slice(compact: &[u8], message: &[u8]) -> Self { + let recid = RecoveryId::from_i32(0).unwrap(); + let recoverable = + secp256k1::ecdsa::RecoverableSignature::from_compact(compact, recid).unwrap(); + + Self::from_recoverable(&recoverable, message) + } +} + /// Public Key data structure #[derive(Debug, Default, Eq, PartialEq, Clone, Hash, Serialize, Deserialize)] pub struct PublicKey { @@ -1597,21 +1638,20 @@ impl PublicKey { } } - pub fn from_str(serialized: &str) -> Self { - let mut pk = hex::decode(serialized).unwrap(); - pk.resize(33, 0); - let mut array_bytes = [0u8; 33]; - array_bytes.copy_from_slice(&pk[..33]); - - Self::from_bytes(array_bytes) - } - /// Returns the PublicKeyHash related to the PublicKey pub fn pkh(&self) -> PublicKeyHash { PublicKeyHash::from_public_key(self) } } +impl std::str::FromStr for PublicKey { + type Err = Secp256k1ConversionError; + + fn from_str(s: &str) -> Result { + Self::try_from_slice(s.as_bytes()) + } +} + /// Secret Key data structure #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] pub struct SecretKey { @@ -4465,14 +4505,14 @@ pub fn block_example() -> Block { #[cfg(test)] mod tests { use witnet_crypto::{ - merkle::{merkle_tree_root, InclusionProof}, + merkle::{InclusionProof, merkle_tree_root}, secp256k1::{PublicKey as Secp256k1_PublicKey, SecretKey as Secp256k1_SecretKey}, signature::sign, }; use crate::{ proto::versioning::{ProtocolVersion, VersionedHashable}, - superblock::{mining_build_superblock, ARSIdentities}, + superblock::{ARSIdentities, mining_build_superblock}, transaction::{CommitTransactionBody, RevealTransactionBody, VTTransactionBody}, }; diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index 4d8d9982a8..7817931e19 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -19,12 +19,13 @@ use itertools::Itertools; use jsonrpc_core::{BoxFuture, Error, Params, Value}; use jsonrpc_pubsub::{Subscriber, SubscriptionId}; use serde::{Deserialize, Serialize}; -use witnet_crypto::{key::KeyPath, secp256k1::ecdsa::Signature}; +use witnet_crypto::key::KeyPath; use witnet_data_structures::{ chain::{ - tapi::ActiveWips, Block, DataRequestOutput, Environment, Epoch, Hash, Hashable, - KeyedSignature, PublicKey, PublicKeyHash, RADType, StakeOutput, StateMachine, SyncStatus, + tapi::ActiveWips, Block, DataRequestOutput, Epoch, Hash, Hashable, KeyedSignature, + PublicKeyHash, RADType, StakeOutput, StateMachine, SyncStatus, }, + get_environment, transaction::Transaction, vrf::VrfMessage, }; @@ -36,14 +37,13 @@ use crate::{ inventory_manager::{InventoryManager, InventoryManagerError}, json_rpc::Subscriptions, messages::{ - AddCandidates, AddPeers, AddTransaction, AuthorizationParams, AuthorizeStake, BuildDrt, - BuildStake, BuildStakeParams, BuildVtt, ClearPeers, DropAllPeers, EstimatePriority, - GetBalance, GetBalanceTarget, GetBlocksEpochRange, GetConsolidatedPeers, - GetDataRequestInfo, GetEpoch, GetHighestCheckpointBeacon, GetItemBlock, - GetItemSuperblock, GetItemTransaction, GetKnownPeers, GetMemoryTransaction, GetMempool, - GetNodeStats, GetReputation, GetSignalingInfo, GetState, GetSupplyInfo, GetUtxoInfo, - InitializePeers, IsConfirmedBlock, Rewind, SnapshotExport, SnapshotImport, - StakeAuthorization, + AddCandidates, AddPeers, AddTransaction, AuthorizeStake, BuildDrt, BuildStake, + BuildStakeParams, BuildVtt, ClearPeers, DropAllPeers, EstimatePriority, GetBalance, + GetBalanceTarget, GetBlocksEpochRange, GetConsolidatedPeers, GetDataRequestInfo, + GetEpoch, GetHighestCheckpointBeacon, GetItemBlock, GetItemSuperblock, + GetItemTransaction, GetKnownPeers, GetMemoryTransaction, GetMempool, GetNodeStats, + GetReputation, GetSignalingInfo, GetState, GetSupplyInfo, GetUtxoInfo, InitializePeers, + IsConfirmedBlock, Rewind, SnapshotExport, SnapshotImport, StakeAuthorization, }, peers_manager::PeersManager, sessions_manager::SessionsManager, @@ -1940,83 +1940,62 @@ pub async fn snapshot_import(params: Result) -> Jso } /// Build a stake transaction pub async fn stake(params: Result) -> JsonRpcResult { - log::debug!("Creating stake transaction from JSON-RPC."); + // Short-circuit if parameters are wrong + let params = params?; - match params { - Ok(msg) => { - let withdrawer = match msg.withdrawer { - Some(withdrawer) => withdrawer, - None => { - let pk = signature_mngr::public_key() - .map(|res| { - res.map_err(internal_error) - .map(|pk| pk.pkh().bech32(Environment::Mainnet)) - }) - .await; - - pk.unwrap() - } - }; - - let authorization: AuthorizationParams = match msg.authorization { - Some(authorization) => authorization, - None => { - let mut data = [0u8; witnet_crypto::secp256k1::constants::MESSAGE_SIZE]; - data[0..20].clone_from_slice(withdrawer.as_ref()); + // If a withdrawer address is not specified, default to local node address + let withdrawer = if let Some(address) = params.withdrawer { + address.try_do_magic(|hex_str| PublicKeyHash::from_bech32(get_environment(), &hex_str)).unwrap() + } else { + let pk = signature_mngr::public_key().await.unwrap(); - let keyed_signature = signature_mngr::sign_data(data) - .map(|res| res.map_err(internal_error)) - .await - .unwrap(); + PublicKeyHash::from_public_key(&pk) + }; - AuthorizationParams { - authorization: hex::encode(keyed_signature.signature.to_bytes().unwrap()), - public_key: hex::encode(keyed_signature.public_key.to_bytes()), - } - } - }; + // This is the actual message that gets signed as part of the authorization + let msg = withdrawer.as_secp256k1_msg(); - let signature = Signature::from_str(&authorization.authorization).unwrap(); - let authorization = KeyedSignature { - signature: signature.into(), - // TODO: https://docs.rs/secp256k1/0.22.2/secp256k1/struct.Secp256k1.html#method.recover_ecdsa - // public_key: signature.recover_ecdsa(withdrawer, signature) - public_key: PublicKey::from_str(&authorization.public_key), - }; + // If no authorization message is provided, generate a new one using the withdrawer address + let authorization = if let Some(signature) = params.authorization { + signature.do_magic(|hex_str| KeyedSignature::from_recoverable_hex(&hex_str, &msg)) + } else { + signature_mngr::sign_data(msg) + .map(|res| res.map_err(internal_error)) + .await + .unwrap() + }; - let build_stake = BuildStake { - dry_run: msg.dry_run, - fee: msg.fee, - utxo_strategy: msg.utxo_strategy, - stake_output: StakeOutput { - authorization, - value: msg.value, - }, - }; + // Construct a BuildStake message that we can relay to the ChainManager for creation of the Stake transaction + let build_stake = BuildStake { + dry_run: params.dry_run, + fee: params.fee, + utxo_strategy: params.utxo_strategy, + stake_output: StakeOutput { + authorization, + value: params.value, + }, + }; - ChainManager::from_registry() - .send(build_stake) - .map(|res| match res { - Ok(Ok(hash)) => match serde_json::to_value(hash) { - Ok(x) => Ok(x), - Err(e) => { - let err = internal_error_s(e); - Err(err) - } - }, - Ok(Err(e)) => { - let err = internal_error_s(e); - Err(err) - } - Err(e) => { - let err = internal_error_s(e); - Err(err) - } - }) - .await - } - Err(err) => Err(err), - } + ChainManager::from_registry() + .send(build_stake) + .map(|res| match res { + Ok(Ok(hash)) => match serde_json::to_value(hash) { + Ok(x) => Ok(x), + Err(e) => { + let err = internal_error_s(e); + Err(err) + } + }, + Ok(Err(e)) => { + let err = internal_error_s(e); + Err(err) + } + Err(e) => { + let err = internal_error_s(e); + Err(err) + } + }) + .await } /// Create a stake authorization for the given address. @@ -2026,49 +2005,33 @@ pub async fn stake(params: Result) -> JsonRpcResult { {"jsonrpc": "2.0","method": "authorizeStake", "params": {"withdrawer":"wit1lkzl4a365fvrr604pwqzykxugpglkrp5ekj0k0"}, "id": "1"} */ pub async fn authorize_stake(params: Result) -> JsonRpcResult { - print!("Inside authorize_stake"); - log::debug!("Creating an authorization stake from JSON-RPC."); - match params { - Ok(msg) => { - let mut withdrawer = msg.withdrawer; - if withdrawer.is_none() { - let pk = signature_mngr::public_key() - .map(|res| { - res.map_err(internal_error) - .map(|pk| pk.pkh().bech32(Environment::Mainnet)) - }) - .await; - withdrawer = Some(pk.unwrap()); - } - // FIXME: we could use directly the pk calculated above in one case - let pkh = - PublicKeyHash::from_bech32(Environment::Mainnet, &withdrawer.clone().unwrap()) - .unwrap(); - let mut data = [0u8; witnet_crypto::secp256k1::constants::MESSAGE_SIZE]; - data[0..20].clone_from_slice(pkh.as_ref()); - - signature_mngr::sign_data(data) - .map(|res| { - res.map_err(internal_error).and_then(|ks| { - let a = StakeAuthorization { - withdrawer: withdrawer.unwrap(), - signature: hex::encode(ks.signature.to_bytes().unwrap()), - public_key: hex::encode(ks.public_key.to_bytes()), - }; - - match serde_json::to_value(a) { - Ok(value) => Ok(value), - Err(e) => { - let err = internal_error_s(e); - Err(err) - } - } - }) - }) - .await - } - Err(err) => Err(err), - } + // Short-circuit if parameters are wrong + let params = params?; + + // If a withdrawer address is not specified, default to local node address + let withdrawer = if let Some(address) = params.withdrawer { + PublicKeyHash::from_bech32(get_environment(), &address).map_err(internal_error)? + } else { + let pk = signature_mngr::public_key().await.unwrap(); + + PublicKeyHash::from_public_key(&pk) + }; + + // This is the actual message that gets signed as part of the authorization + let msg = withdrawer.as_secp256k1_msg(); + + signature_mngr::sign_data(msg) + .map(|res| { + res.map_err(internal_error).and_then(|signature| { + let authorization = StakeAuthorization { + withdrawer, + signature, + }; + + serde_json::to_value(authorization).map_err(internal_error) + }) + }) + .await } #[cfg(test)] diff --git a/node/src/actors/messages.rs b/node/src/actors/messages.rs index fa78be1827..2cb819af90 100644 --- a/node/src/actors/messages.rs +++ b/node/src/actors/messages.rs @@ -9,21 +9,19 @@ use std::{ time::Duration, }; -use actix::{ - dev::{MessageResponse, OneshotSender, ToEnvelope}, - Actor, Addr, Handler, Message, -}; +use actix::{Actor, Addr, dev::{MessageResponse, OneshotSender, ToEnvelope}, Handler, Message}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tokio::net::TcpStream; +use witnet_crypto::secp256k1::ecdsa::{RecoverableSignature, RecoveryId}; use witnet_data_structures::{ chain::{ - priority::PrioritiesEstimate, - tapi::{ActiveWips, BitVotesCounter}, - Block, CheckpointBeacon, DataRequestInfo, DataRequestOutput, Epoch, EpochConstants, Hash, - InventoryEntry, InventoryItem, NodeStats, PointerToBlock, PublicKeyHash, - PublicKeyHashParseError, RADRequest, RADTally, Reputation, StakeOutput, StateMachine, - SuperBlock, SuperBlockVote, SupplyInfo, ValueTransferOutput, + Block, + CheckpointBeacon, + DataRequestInfo, DataRequestOutput, Epoch, EpochConstants, Hash, InventoryEntry, InventoryItem, + KeyedSignature, NodeStats, PointerToBlock, priority::PrioritiesEstimate, PublicKeyHash, PublicKeyHashParseError, + RADRequest, RADTally, Reputation, StakeOutput, StateMachine, SuperBlock, + SuperBlockVote, SupplyInfo, tapi::{ActiveWips, BitVotesCounter}, ValueTransferOutput, }, fee::{deserialize_fee_backwards_compatible, Fee}, radon_report::RadonReport, @@ -241,27 +239,18 @@ impl Message for BuildStake { type Result = Result; } -/// Authorization formatted as strings -#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] -pub struct AuthorizationParams { - /// Authorization public key - pub public_key: String, - /// Authorization signature - pub authorization: String, -} - /// Builds a `StakeTransaction` from a list of `ValueTransferOutput`s -#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] pub struct BuildStakeParams { /// Authorization signature and public key #[serde(default)] - pub authorization: Option, + pub authorization: Option>, /// List of `ValueTransferOutput`s #[serde(default)] pub value: u64, /// Withdrawer #[serde(default)] - pub withdrawer: Option, + pub withdrawer: Option>, /// Fee #[serde(default)] pub fee: Fee, @@ -273,10 +262,6 @@ pub struct BuildStakeParams { pub dry_run: bool, } -// impl Message for BuildStake { -// type Result = Result; -// } - /// Builds an `AuthorizeStake` #[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct AuthorizeStake { @@ -293,11 +278,9 @@ impl Message for AuthorizeStake { #[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct StakeAuthorization { /// Address that can withdraw the stake - pub withdrawer: String, - /// Signature of the withdrawer - pub signature: String, - /// Public key related with signature - pub public_key: String, + pub withdrawer: PublicKeyHash, + /// A node's signature of a withdrawer's address + pub signature: KeyedSignature, } impl Message for StakeAuthorization { @@ -1364,3 +1347,31 @@ pub struct EstimatePriority; impl Message for EstimatePriority { type Result = Result; } + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +/// A value that can either be L, R, where an R can always be obtained through the `do_magic` method. +pub enum MagicEither { + /// A first variant. + Left(L), + /// A second variant. + Right(R), +} + +impl MagicEither { + /// Obtain an R value, even if this was an instance of L. + pub fn do_magic(self, trick: F) -> R where F: Fn(L) -> R { + match self { + Self::Left(l) => trick(l), + Self::Right(r) => r, + } + } + + /// Fallible version of `do_magic`. + pub fn try_do_magic(self, trick: F) -> Result where F: Fn(L) -> Result { + match self { + Self::Left(l) => trick(l), + Self::Right(r) => Ok(r), + } + } +} \ No newline at end of file diff --git a/src/cli/node/json_rpc_client.rs b/src/cli/node/json_rpc_client.rs index 2a8b6ae3f3..0b6b7cdd65 100644 --- a/src/cli/node/json_rpc_client.rs +++ b/src/cli/node/json_rpc_client.rs @@ -40,10 +40,11 @@ use witnet_node::actors::{ chain_manager::run_dr_locally, json_rpc::api::{AddrType, GetBlockChainParams, GetTransactionOutput, PeersResult}, messages::{ - AuthorizationParams, AuthorizeStake, BuildDrt, BuildStakeParams, BuildVtt, + AuthorizeStake, BuildDrt, BuildStakeParams, BuildVtt, GetBalanceTarget, GetReputationResult, SignalingInfo, StakeAuthorization, }, }; +use witnet_node::actors::messages::MagicEither; use witnet_rad::types::RadonTypes; use witnet_util::{files::create_private_file, timestamp::pretty_print}; use witnet_validations::validations::{ @@ -888,11 +889,8 @@ pub fn send_st( }; let mut build_stake_params = BuildStakeParams { - authorization: Some(AuthorizationParams { - authorization: stake_authorization.signature, - public_key: stake_authorization.public_key, - }), - withdrawer: Some(stake_authorization.withdrawer), + authorization: Some(MagicEither::Right(stake_authorization.signature)), + withdrawer: Some(MagicEither::Right(stake_authorization.withdrawer)), value, fee, utxo_strategy, @@ -985,25 +983,17 @@ pub fn send_st( } pub fn authorize_st(addr: SocketAddr, withdrawer: Option) -> Result<(), failure::Error> { - if withdrawer.is_some() { - // validate withdrawer - PublicKeyHash::from_bech32(Environment::Mainnet, &withdrawer.clone().unwrap())?; - } - let mut stream = start_client(addr)?; let mut id = SequentialId::initialize(1u8); let params = AuthorizeStake { withdrawer }; - let (authorization, (_, response)): (StakeAuthorization, _) = + let (authorization, (_, _response)): (StakeAuthorization, _) = issue_method("authorizeStake", Some(params), &mut stream, id.next())?; - println!("{}", response); - - let mut str = authorization.signature.clone(); - str.push(':'); - str.push_str(&authorization.public_key); + let auth_bytes = authorization.signature.signature.to_bytes()?; + let auth_string = hex::encode(auth_bytes); - let auth_qr = qrcode::QrCode::new(str).unwrap(); + let auth_qr = qrcode::QrCode::new(&auth_string)?; let auth_ascii = auth_qr .render::() .quiet_zone(true) @@ -1012,8 +1002,8 @@ pub fn authorize_st(addr: SocketAddr, withdrawer: Option) -> Result<(), .build(); println!( - "Authorization code:\n{}\nPublicKey code:\n{}\nQR code for myWitWallet:\n{}", - authorization.signature, authorization.public_key, auth_ascii + "Authorization code:\n{}\nQR code for myWitWallet:\n{}", + auth_string, auth_ascii ); Ok(()) From a814e0cd15dc793c02ade242525aa29720f48b6d 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 14/32] fix(validations): fix mismatch in superblock validations --- data_structures/src/chain/mod.rs | 13 ++- data_structures/src/lib.rs | 48 ++++----- data_structures/src/proto/versioning.rs | 68 ++++++++++--- data_structures/src/superblock.rs | 17 ++-- data_structures/tests/serializers.rs | 4 +- node/src/actors/chain_manager/mining.rs | 2 +- node/src/actors/chain_manager/mod.rs | 11 ++- validations/src/validations.rs | 124 +++++++++++++++++++++++- 8 files changed, 234 insertions(+), 53 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index 1a69aa3f02..028f64259b 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -41,8 +41,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, @@ -674,7 +677,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() } @@ -4621,12 +4624,12 @@ mod tests { let block = block_example(); let expected = "70e15ac70bb00f49c7a593b2423f722dca187bbae53dc2f22647063b17608c01"; assert_eq!( - block.versioned_hash(ProtocolVersion::V1_6).to_string(), + block.versioned_hash(ProtocolVersion::V1_7).to_string(), expected ); let expected = "29ef68357a5c861b9dbe043d351a28472ca450edcda25de4c9b80a4560a28c0f"; assert_eq!( - block.versioned_hash(ProtocolVersion::V1_7).to_string(), + block.versioned_hash(ProtocolVersion::V1_8).to_string(), expected ); let expected = "29ef68357a5c861b9dbe043d351a28472ca450edcda25de4c9b80a4560a28c0f"; diff --git a/data_structures/src/lib.rs b/data_structures/src/lib.rs index c57f2386ac..c43328c40c 100644 --- a/data_structures/src/lib.rs +++ b/data_structures/src/lib.rs @@ -13,10 +13,13 @@ #[macro_use] extern crate protobuf_convert; -use crate::{chain::Environment, proto::versioning::ProtocolVersion}; -use lazy_static::lazy_static; use std::sync::RwLock; +use lazy_static::lazy_static; + +use crate::{chain::{Environment, Epoch}, proto::versioning::ProtocolVersion}; +use crate::proto::versioning::ProtocolInfo; + /// Module containing functions to generate Witnet's protocol messages pub mod builders; @@ -84,7 +87,7 @@ lazy_static! { 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); + static ref PROTOCOL: RwLock = RwLock::new(ProtocolInfo::default()); } /// Environment in which we are running: mainnet or testnet. @@ -118,31 +121,30 @@ pub fn set_environment(environment: Environment) { } /// Protocol version that we are running. -pub fn get_protocol_version() -> ProtocolVersion { +pub fn get_protocol_version(epoch: Option) -> 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() + let protocol_info = PROTOCOL.read().unwrap(); + + if let Some(epoch) = epoch { + protocol_info.all_versions.version_for_epoch(epoch) + } else { + *protocol_info.current_version + } +} + +pub fn register_protocol_version(epoch: Epoch, 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. + let mut protocol_info = PROTOCOL.write().unwrap(); + *protocol_info.register(epoch, protocol_version); } /// 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))] +/// #[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); - } - } + let mut protocol = PROTOCOL.write().unwrap(); + *protocol.current_version = protocol_version; } #[cfg(test)] @@ -161,6 +163,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!(get_protocol_version(), ProtocolVersion::V1_7); } } diff --git a/data_structures/src/proto/versioning.rs b/data_structures/src/proto/versioning.rs index 622d9c920d..d0e76f8a5c 100644 --- a/data_structures/src/proto/versioning.rs +++ b/data_structures/src/proto/versioning.rs @@ -1,11 +1,14 @@ use failure::{Error, Fail}; use protobuf::Message as _; +use std::collections::{BTreeMap, HashMap}; use std::fmt; use std::fmt::Formatter; +use crate::chain::Epoch; use crate::proto::schema::witnet::SuperBlock; use crate::{ chain::Hash, + get_protocol_version, proto::{ schema::witnet::{ Block, Block_BlockHeader, Block_BlockHeader_BlockMerkleRoots, Block_BlockTransactions, @@ -20,21 +23,64 @@ use crate::{ types::Message, }; -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Debug)] +pub struct ProtocolInfo { + pub current_version: ProtocolVersion, + pub all_versions: VersionsMap, +} + +impl ProtocolInfo { + pub fn register(&mut self, epoch: Epoch, version: ProtocolVersion) { + self.all_versions.register(epoch, version) + } +} + +#[derive(Clone, Debug, Default)] +pub struct VersionsMap { + efv: HashMap, + vfe: BTreeMap, +} + +impl VersionsMap { + pub fn register(&mut self, epoch: Epoch, version: ProtocolVersion) { + self.efv.insert(version, epoch); + self.vfe.insert(epoch, version); + } + + pub fn version_for_epoch(&self, queried_epoch: Epoch) -> ProtocolVersion { + *self + .vfe + .iter() + .rev() + .find(|(epoch, _)| **epoch < queried_epoch) + .map(|(_, version)| version) + .unwrap_or_default() + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] pub enum ProtocolVersion { /// The original Witnet protocol. - V1_6, - /// The transitional protocol based on 1.x but with staking enabled. + // TODO: update this default once 2.0 is completely active + #[default] V1_7, + /// The transitional protocol based on 1.x but with staking enabled. + V1_8, /// The final Witnet 2.0 protocol. V2_0, } +impl ProtocolVersion { + pub fn guess() -> Self { + get_protocol_version(None) + } +} + 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::V1_7 => "v1.7 (legacy)", + ProtocolVersion::V1_8 => "v1.8 (transitional)", ProtocolVersion::V2_0 => "v2.0 (final)", }; @@ -113,9 +159,9 @@ impl Versioned for crate::chain::BlockMerkleRoots { let versioned: Box = match version { // Legacy merkle roots need to get rearranged - V1_6 => Box::new(Self::LegacyType::from(pb)), + V1_7 => Box::new(Self::LegacyType::from(pb)), // Transition merkle roots need no transformation - V1_7 => Box::new(pb), + V1_8 => Box::new(pb), // Final merkle roots need to drop the mint hash V2_0 => { pb.set_mint_hash(Default::default()); @@ -141,9 +187,9 @@ impl Versioned for crate::chain::BlockHeader { let versioned: Box = match version { // Legacy block headers need to be rearranged - V1_6 => Box::new(Self::LegacyType::from(pb)), + V1_7 => Box::new(Self::LegacyType::from(pb)), // All other block headers need no transformation - V1_7 | V2_0 => Box::new(pb), + V1_8 | V2_0 => Box::new(pb), }; Ok(versioned) @@ -187,8 +233,8 @@ impl Versioned for Message { 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), + V1_7 => Box::new(Self::LegacyType::from(pb)), + V1_8 | V2_0 => Box::new(pb), }; Ok(versioned) diff --git a/data_structures/src/superblock.rs b/data_structures/src/superblock.rs index 39a5a8da95..8557596ed7 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) @@ -823,7 +828,7 @@ mod tests { default_hash, dr_merkle_root_1, 0, - block.versioned_hash(ProtocolVersion::V1_6), + block.versioned_hash(ProtocolVersion::V1_7), default_hash, tally_merkle_root_1, ); @@ -891,7 +896,7 @@ mod tests { default_hash, expected_superblock_dr_root, 0, - block_2.versioned_hash(ProtocolVersion::V1_6), + block_2.versioned_hash(ProtocolVersion::V1_7), default_hash, expected_superblock_tally_root, ); diff --git a/data_structures/tests/serializers.rs b/data_structures/tests/serializers.rs index 8de51b1e5e..df5467e6d0 100644 --- a/data_structures/tests/serializers.rs +++ b/data_structures/tests/serializers.rs @@ -418,11 +418,11 @@ fn message_block_to_bytes() { }; let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR_LEGACY.to_vec(); - let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::V1_6).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_TRANSITION.to_vec(); - let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::V1_7).unwrap(); + let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::V1_8).unwrap(); assert_eq!(result, expected_buf); let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR_FINAL.to_vec(); 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/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index 47e7379f4e..d668929f7e 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -682,6 +682,8 @@ impl ChainManager { block_epoch: block.block_header.beacon.checkpoint, }; + println!("1"); + let mut transaction_visitor = PriorityVisitor::default(); let utxo_diff = process_validations( @@ -701,12 +703,14 @@ impl ChainManager { Some(&mut transaction_visitor), )?; + println!("2"); // Extract the collected priorities from the internal state of the visitor let priorities = transaction_visitor.take_state(); // Persist block and update ChainState self.consolidate_block(ctx, block, utxo_diff, priorities, resynchronizing); + println!("3"); Ok(()) } else { Err(ChainManagerError::ChainNotReady.into()) @@ -2774,6 +2778,7 @@ pub fn process_validations( active_wips: &ActiveWips, transaction_visitor: Option<&mut dyn Visitor>, ) -> Result { + println!("pv1"); if !resynchronizing { let mut signatures_to_verify = vec![]; validate_block( @@ -2788,7 +2793,7 @@ pub fn process_validations( )?; verify_signatures(signatures_to_verify, vrf_ctx)?; } - + println!("pv2"); let mut signatures_to_verify = vec![]; let utxo_dif = validate_block_transactions( utxo_set, @@ -2803,11 +2808,11 @@ pub fn process_validations( active_wips, transaction_visitor, )?; - + println!("pv3"); if !resynchronizing { verify_signatures(signatures_to_verify, vrf_ctx)?; } - + println!("pv4"); Ok(utxo_dif) } diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 14c49c1155..30d33d009c 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::{ @@ -1602,7 +1601,7 @@ pub fn validate_block_transactions( let epoch = block.block_header.beacon.checkpoint; let is_genesis = block.is_genesis(&consensus_constants.genesis_hash); let mut utxo_diff = UtxoDiff::new(utxo_set, block_number); - + println!("vbt1"); // Init total fee let mut total_fee = 0; // When validating genesis block, keep track of total value created @@ -1619,6 +1618,7 @@ pub fn validate_block_transactions( // Validate value transfer transactions in a block let mut vt_mt = ProgressiveMerkleTree::sha256(); let mut vt_weight: u32 = 0; + println!("vbt2"); for transaction in &block.txns.value_transfer_txns { let (inputs, outputs, fee, weight) = if is_genesis { @@ -1671,6 +1671,7 @@ pub fn validate_block_transactions( } let vt_hash_merkle_root = vt_mt.root(); + println!("vbt3"); // Validate commit transactions in a block let mut co_mt = ProgressiveMerkleTree::sha256(); let mut commits_number = HashMap::new(); @@ -1681,6 +1682,7 @@ pub fn validate_block_transactions( } else { consensus_constants.collateral_age }; + println!("vbt4"); for transaction in &block.txns.commit_txns { let (dr_pointer, dr_witnesses, fee) = validate_commit_transaction( transaction, @@ -1722,6 +1724,7 @@ pub fn validate_block_transactions( } let co_hash_merkle_root = co_mt.root(); + println!("vbt5"); // Validate commits number and add commit fees for WitnessesCount { current, target } in commits_number.values() { if current != target { @@ -1733,6 +1736,7 @@ pub fn validate_block_transactions( } } + println!("vbt6"); // Validate reveal transactions in a block let mut re_mt = ProgressiveMerkleTree::sha256(); let mut reveal_hs = HashSet::with_capacity(block.txns.reveal_txns.len()); @@ -1754,6 +1758,7 @@ pub fn validate_block_transactions( re_mt.push(Sha256(sha)); } let re_hash_merkle_root = re_mt.root(); + println!("vbt7"); // Validate tally transactions in a block let mut ta_mt = ProgressiveMerkleTree::sha256(); @@ -1796,6 +1801,7 @@ pub fn validate_block_transactions( ta_mt.push(Sha256(sha)); } let ta_hash_merkle_root = ta_mt.root(); + println!("vbt8"); // All data requests for which we expected tally transactions should have been removed // upon creation of the tallies. If not, block is invalid due to missing expected tallies @@ -1806,6 +1812,7 @@ pub fn validate_block_transactions( } .into()); } + println!("vbt9"); let mut dr_weight: u32 = 0; if active_wips.wip_0008() { @@ -1826,6 +1833,7 @@ pub fn validate_block_transactions( } } } + println!("vbt10"); // Validate data request transactions in a block let mut dr_mt = ProgressiveMerkleTree::sha256(); @@ -1872,6 +1880,7 @@ pub fn validate_block_transactions( } let dr_hash_merkle_root = dr_mt.root(); + println!("vbt11"); if !is_genesis { // Validate mint validate_mint_transaction( @@ -1890,6 +1899,7 @@ pub fn validate_block_transactions( block.txns.mint.hash(), ); } + println!("vbt12"); // TODO skip all staking logic if protocol version is legacy @@ -1906,12 +1916,15 @@ pub fn validate_block_transactions( .duplicates() .next(); + println!("vbt13"); + if let Some(duplicate) = duplicate { return Err(BlockError::RepeatedStakeOperator { pkh: duplicate.pkh(), } .into()); } + println!("vbt14"); for transaction in &block.txns.stake_txns { let (inputs, _output, fee, weight, change) = validate_stake_transaction( @@ -1948,6 +1961,7 @@ pub fn validate_block_transactions( // visitor.visit(&(transaction, fee, weight)); // } } + println!("vbt15"); let mut ut_mt = ProgressiveMerkleTree::sha256(); let mut ut_weight: u32 = 0; @@ -1983,10 +1997,111 @@ pub fn validate_block_transactions( // visitor.visit(&(transaction, fee, weight)); // } } + println!("vbt16"); // 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_7 => Default::default(), + _ => (Hash::from(st_mt.root()), Hash::from(ut_mt.root())), + }; + + // 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; + + // 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.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 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)); + // } + } + + // Nullify roots for legacy protocol version + // TODO skip all staking logic if protocol version is legacy + let (st_root, ut_root) = match ProtocolVersion::guess() { ProtocolVersion::V1_6 => Default::default(), _ => (Hash::from(st_mt.root()), Hash::from(ut_mt.root())), }; @@ -2002,8 +2117,13 @@ pub fn validate_block_transactions( stake_hash_merkle_root: st_root, unstake_hash_merkle_root: ut_root, }; + println!("vbt17"); if merkle_roots != block.block_header.merkle_roots { + println!( + "{:?} vs {:?}", + merkle_roots, block.block_header.merkle_roots + ); Err(BlockError::NotValidMerkleTree.into()) } else { Ok(utxo_diff.take_diff()) From 49f6cda99ec5223bc7762730fa7db1b46125cd0a Mon Sep 17 00:00:00 2001 From: tommytrg Date: Wed, 10 Jan 2024 13:41:25 +0100 Subject: [PATCH 15/32] tests: add missing superblock_period arg in validate_commit_transaction --- validations/src/tests/mod.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/validations/src/tests/mod.rs b/validations/src/tests/mod.rs index 1c43c19715..4e395494a9 100644 --- a/validations/src/tests/mod.rs +++ b/validations/src/tests/mod.rs @@ -2972,6 +2972,7 @@ fn test_empty_commit(c_tx: &CommitTransaction) -> Result<(), failure::Error> { let block_number = 0; let minimum_reppoe_difficulty = 1; let utxo_diff = UtxoDiff::new(&utxo_set, block_number); + let superblock_period = 1; validate_commit_transaction( c_tx, @@ -2987,6 +2988,7 @@ fn test_empty_commit(c_tx: &CommitTransaction) -> Result<(), failure::Error> { block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, ) .map(|_| ()) } @@ -3003,6 +3005,7 @@ fn test_commit_with_dr_and_utxo_set( let collateral_minimum = 1; let collateral_age = 1; let minimum_reppoe_difficulty = 1; + let superblock_period = 1; let mut dr_pool = DataRequestPool::default(); let vrf_input = CheckpointVRF::default(); @@ -3041,6 +3044,7 @@ fn test_commit_with_dr_and_utxo_set( block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, )?; verify_signatures_test(signatures_to_verify)?; @@ -3115,6 +3119,7 @@ fn test_commit_difficult_proof() { let active_wips = ActiveWips::default(); let mut signatures_to_verify = vec![]; + let superblock_period = 8; let x = validate_commit_transaction( &c_tx, &dr_pool, @@ -3129,6 +3134,7 @@ fn test_commit_difficult_proof() { block_number, minimum_reppoe_difficulty, &active_wips, + superblock_period, ) .and_then(|_| verify_signatures_test(signatures_to_verify)); @@ -3191,6 +3197,8 @@ fn test_commit_with_collateral( let cs = sign_tx(PRIV_KEY_1, &cb); let c_tx = CommitTransaction::new(cb, vec![cs]); + let superblock_period = 1; + validate_commit_transaction( &c_tx, &dr_pool, @@ -3205,6 +3213,7 @@ fn test_commit_with_collateral( block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, ) .map(|_| ()) } @@ -3436,6 +3445,8 @@ fn commitment_invalid_proof() { let c_tx = CommitTransaction::new(cb, vec![cs]); let mut signatures_to_verify = vec![]; + let superblock_period = 1; + let x = validate_commit_transaction( &c_tx, &dr_pool, @@ -3450,6 +3461,7 @@ fn commitment_invalid_proof() { block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, ) .and_then(|_| verify_signatures_test(signatures_to_verify)); @@ -3512,6 +3524,8 @@ fn commitment_dr_in_reveal_stage() { dr_pool.update_data_request_stages(); let mut signatures_to_verify = vec![]; + let superblock_period = 1; + let x = validate_commit_transaction( &c_tx, &dr_pool, @@ -3526,6 +3540,7 @@ fn commitment_dr_in_reveal_stage() { block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, ); assert_eq!( x.unwrap_err().downcast::().unwrap(), @@ -3897,6 +3912,8 @@ fn commitment_collateral_zero_is_minimum() { let cs = sign_tx(PRIV_KEY_1, &cb); let c_tx = CommitTransaction::new(cb, vec![cs]); + let superblock_period = 1; + validate_commit_transaction( &c_tx, &dr_pool, @@ -3911,6 +3928,7 @@ fn commitment_collateral_zero_is_minimum() { block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, ) .map(|_| ()) }; @@ -3988,6 +4006,7 @@ fn commitment_timelock() { let active_wips = active_wips_from_mainnet(epoch); let mut signatures_to_verify = vec![]; + let superblock_period = 1; validate_commit_transaction( &c_tx, &dr_pool, @@ -4002,6 +4021,7 @@ fn commitment_timelock() { block_number, minimum_reppoe_difficulty, &active_wips, + superblock_period, ) .map(|_| ())?; From 9cf332bf12e2555c8fe8d0cb6a236118ebb669cf Mon Sep 17 00:00:00 2001 From: tommytrg Date: Wed, 10 Jan 2024 13:44:44 +0100 Subject: [PATCH 16/32] refactor: fix clippy errors --- data_structures/src/chain/priority.rs | 2 +- node/tests/data_request_examples.rs | 15 +++++---------- rad/src/operators/map.rs | 4 ++-- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/data_structures/src/chain/priority.rs b/data_structures/src/chain/priority.rs index f0c5c63033..a4984d8b7c 100644 --- a/data_structures/src/chain/priority.rs +++ b/data_structures/src/chain/priority.rs @@ -852,7 +852,7 @@ mod tests { let input = priorities_factory(10usize, 0.0..=100.0, None); let engine = PriorityEngine::from_vec_with_capacity(input.clone(), 5); - assert_eq!(engine.get(0), input.get(0)); + assert_eq!(engine.get(0), input.first()); assert_eq!(engine.get(1), input.get(1)); assert_eq!(engine.get(2), input.get(2)); assert_eq!(engine.get(3), input.get(3)); diff --git a/node/tests/data_request_examples.rs b/node/tests/data_request_examples.rs index 2a7eacde16..44b9b6a9e8 100644 --- a/node/tests/data_request_examples.rs +++ b/node/tests/data_request_examples.rs @@ -1,8 +1,4 @@ -use std::{ - collections::HashMap, - convert::{TryFrom, TryInto}, - fs, -}; +use std::{collections::HashMap, convert::TryFrom, fs}; use serde::{Deserialize, Serialize}; @@ -84,11 +80,10 @@ fn run_dr_locally_with_data( log::info!("Aggregation result: {:?}", aggregation_result); // Assume that all the required witnesses will report the same value - let reported_values: Result, _> = - vec![aggregation_result; dr.witnesses.try_into().unwrap()] - .into_iter() - .map(RadonTypes::try_from) - .collect(); + let reported_values: Result, _> = vec![aggregation_result; dr.witnesses.into()] + .into_iter() + .map(RadonTypes::try_from) + .collect(); log::info!("Running tally with values {:?}", reported_values); let tally_result = witnet_rad::run_tally(reported_values?, &dr.data_request.tally, &all_wips_active())?; diff --git a/rad/src/operators/map.rs b/rad/src/operators/map.rs index c81da9eef0..f0ccdc1058 100644 --- a/rad/src/operators/map.rs +++ b/rad/src/operators/map.rs @@ -116,7 +116,7 @@ pub mod legacy { #[cfg(test)] mod tests { - use std::{collections::BTreeMap, convert::TryFrom}; + use std::collections::BTreeMap; use crate::{ operators::{Operable, RadonOpCodes}, @@ -131,7 +131,7 @@ mod tests { fn test_map_get() { let key = "Zero"; let value = RadonTypes::Integer(RadonInteger::from(0)); - let args = vec![Value::try_from(String::from(key)).unwrap()]; + let args = vec![Value::from(String::from(key))]; let mut map = BTreeMap::new(); map.insert(key.to_string(), value.clone()); From f5a5b0b0af641ffc16b50a480174245a61d07754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Fri, 19 Jan 2024 14:05:29 +0100 Subject: [PATCH 17/32] feat(data_structures): implement protocol versions controller also: update secp256k1 deps close #2418 --- Cargo.lock | 8 +- crypto/Cargo.toml | 2 +- crypto/src/key.rs | 10 +- crypto/src/signature.rs | 4 +- data_structures/src/chain/mod.rs | 48 ++-- data_structures/src/error.rs | 4 +- data_structures/src/lib.rs | 41 +++- data_structures/src/proto/versioning.rs | 8 +- node/src/actors/chain_manager/handlers.rs | 2 +- node/src/actors/chain_manager/mining.rs | 2 +- node/src/actors/chain_manager/mod.rs | 11 +- node/src/actors/json_rpc/api.rs | 87 +++++-- node/src/actors/messages.rs | 48 +++- src/cli/node/json_rpc_client.rs | 130 ++++++++-- src/cli/node/with_node.rs | 17 +- validations/src/validations.rs | 281 +++++++--------------- 16 files changed, 400 insertions(+), 303 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4af2675a1..41a3ce131f 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -3549,9 +3549,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "secp256k1" -version = "0.22.2" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295642060261c80709ac034f52fca8e5a9fa2c7d341ded5cdb164b7c33768b2a" +checksum = "3f622567e3b4b38154fb8190bcf6b160d7a4301d70595a49195b48c116007a27" dependencies = [ "secp256k1-sys", "serde", @@ -3559,9 +3559,9 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.5.2" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" dependencies = [ "cc", ] diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index 2b65b8da2d..4255422f7b 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -22,7 +22,7 @@ hmac = "0.7.1" memzero = "0.1.0" rand = "0.7.3" ring = "0.16.11" -secp256k1 = { version = "0.22.2", features = ["global-context", "recovery"] } +secp256k1 = { version = "0.28.1", features = ["global-context", "recovery"] } serde = { version = "1.0.104", optional = true } sha2 = "0.8.1" tiny-bip39 = "0.7.0" diff --git a/crypto/src/key.rs b/crypto/src/key.rs index 9d99dc5464..5e1def9e42 100644 --- a/crypto/src/key.rs +++ b/crypto/src/key.rs @@ -149,6 +149,9 @@ pub enum KeyDerivationError { /// Invalid seed length #[fail(display = "The length of the seed is invalid, must be between 128/512 bits")] InvalidSeedLength, + /// A secret key is greater than the curve order + #[fail(display = "The secret key is greater than the curve order")] + SecretLargerThanCurveOrder, /// Secp256k1 internal error #[fail(display = "Error in secp256k1 crate")] Secp256k1Error(secp256k1::Error), @@ -300,8 +303,11 @@ impl ExtendedSK { let (chain_code, mut secret_key) = get_chain_code_and_secret(&index_bytes, hmac512)?; - secret_key - .add_assign(&self.secret_key[..]) + let scalar = secp256k1::Scalar::from_be_bytes(self.secret_key.secret_bytes()) + .map_err(|_| KeyDerivationError::SecretLargerThanCurveOrder)?; + + secret_key = secret_key + .add_tweak(&scalar) .map_err(KeyDerivationError::Secp256k1Error)?; Ok(ExtendedSK { diff --git a/crypto/src/signature.rs b/crypto/src/signature.rs index 5da876b731..c980c47af7 100644 --- a/crypto/src/signature.rs +++ b/crypto/src/signature.rs @@ -12,14 +12,14 @@ pub type PublicKey = secp256k1::PublicKey; /// secure hash function, otherwise this function is not secure. /// - Returns an Error if data is not a 32-byte array pub fn sign(secret_key: SecretKey, data: &[u8]) -> Result { - let msg = Message::from_slice(data)?; + let msg = Message::from_digest_slice(data)?; Ok(secret_key.sign_ecdsa(msg)) } /// Verify signature with a provided public key. /// - Returns an Error if data is not a 32-byte array pub fn verify(public_key: &PublicKey, data: &[u8], sig: &Signature) -> Result<(), Error> { - let msg = Message::from_slice(data)?; + let msg = Message::from_digest_slice(data)?; sig.verify(&msg, public_key) } diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index 028f64259b..cab521ca17 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -11,6 +11,7 @@ use std::{ use bech32::{FromBase32, ToBase32}; use bls_signatures_rs::{bn256, bn256::Bn256, MultiSignature}; +use ethereum_types::U256; use failure::Fail; use futures::future::BoxFuture; use ordered_float::OrderedFloat; @@ -23,19 +24,15 @@ use witnet_crypto::{ merkle::merkle_tree_root as crypto_merkle_tree_root, secp256k1::{ self, - PublicKey as Secp256k1_PublicKey, - ecdsa::{ - RecoverableSignature, RecoveryId, - Signature as Secp256k1_Signature, - }, - SecretKey as Secp256k1_SecretKey, + ecdsa::{RecoverableSignature, RecoveryId, Signature as Secp256k1_Signature}, + PublicKey as Secp256k1_PublicKey, SecretKey as Secp256k1_SecretKey, }, }; use witnet_protected::Protected; use witnet_reputation::{ActiveReputationSet, TotalReputationSet}; use crate::{ - chain::{Signature::Secp256k1, tapi::TapiEngine}, + chain::{tapi::TapiEngine, Signature::Secp256k1}, data_request::{calculate_reward_collateral_ratio, DataRequestPool}, error::{ DataRequestError, EpochCalculationError, OutputPointerParseError, Secp256k1ConversionError, @@ -53,7 +50,7 @@ use crate::{ UnstakeTransaction, VTTransaction, }, transaction::{ - BETA, COMMIT_WEIGHT, MemoHash, MemoizedHashable, OUTPUT_SIZE, REVEAL_WEIGHT, TALLY_WEIGHT, + MemoHash, MemoizedHashable, BETA, COMMIT_WEIGHT, OUTPUT_SIZE, REVEAL_WEIGHT, TALLY_WEIGHT, }, utxo_pool::{OldUnspentOutputsPool, OwnUnspentOutputsPool, UnspentOutputsPool}, vrf::{BlockEligibilityClaim, DataRequestEligibilityClaim}, @@ -1188,8 +1185,6 @@ impl Hash { /// /// If n is 0 because of a division by zero. pub fn div_mod(&self, n: u64) -> (Hash, u64) { - use ethereum_types::U256; - let hash_u256 = U256::from_big_endian(self.as_ref()); let n_u256 = U256::from(n); let (d, m) = hash_u256.div_mod(n_u256); @@ -1575,12 +1570,14 @@ pub struct KeyedSignature { impl KeyedSignature { pub fn from_recoverable_hex(string: &str, msg: &[u8]) -> Self { + // FIXME: make this safe by using a `Result` instead of unwrapping let bytes = hex::decode(string).unwrap(); Self::from_recoverable_slice(&bytes, msg) } pub fn from_recoverable(recoverable: &RecoverableSignature, message: &[u8]) -> Self { - let msg = secp256k1::Message::from_slice(message).unwrap(); + // FIXME: make this safe by using a `Result` instead of unwrapping + let msg = secp256k1::Message::from_digest_slice(message).unwrap(); let signature = recoverable.to_standard(); let public_key = recoverable.recover(&msg).unwrap(); @@ -1592,12 +1589,31 @@ impl KeyedSignature { // Recovers a keyed signature from its serialized form and a known message. pub fn from_recoverable_slice(compact: &[u8], message: &[u8]) -> Self { - let recid = RecoveryId::from_i32(0).unwrap(); - let recoverable = - secp256k1::ecdsa::RecoverableSignature::from_compact(compact, recid).unwrap(); + // FIXME: make this safe by using a `Result` instead of unwrapping + let recid = RecoveryId::from_i32(compact[0] as i32).unwrap(); + let recoverable = RecoverableSignature::from_compact(&compact[1..], recid).unwrap(); Self::from_recoverable(&recoverable, message) } + + /// Serializes a `KeyedSignature` into a compact encoding form that contains the public key recovery ID as a prefix. + pub fn to_recoverable_bytes(self, message: &[u8]) -> [u8; 65] { + // FIXME: make this safe by using a `Result` instead of unwrapping + let mut recoverable_bytes = [0; 65]; + recoverable_bytes[1..].clone_from_slice(&self.signature.to_bytes().unwrap()); + + for i in 0..4 { + recoverable_bytes[0] = i; + + let recovered = KeyedSignature::from_recoverable_slice(&recoverable_bytes, message); + + if recovered.public_key == self.public_key { + break; + } + } + + recoverable_bytes + } } /// Public Key data structure @@ -4508,14 +4524,14 @@ pub fn block_example() -> Block { #[cfg(test)] mod tests { use witnet_crypto::{ - merkle::{InclusionProof, merkle_tree_root}, + merkle::{merkle_tree_root, InclusionProof}, secp256k1::{PublicKey as Secp256k1_PublicKey, SecretKey as Secp256k1_SecretKey}, signature::sign, }; use crate::{ proto::versioning::{ProtocolVersion, VersionedHashable}, - superblock::{ARSIdentities, mining_build_superblock}, + superblock::{mining_build_superblock, ARSIdentities}, transaction::{CommitTransactionBody, RevealTransactionBody, VTTransactionBody}, }; diff --git a/data_structures/src/error.rs b/data_structures/src/error.rs index ee2471dbf5..754d2f7e48 100644 --- a/data_structures/src/error.rs +++ b/data_structures/src/error.rs @@ -291,12 +291,12 @@ pub enum TransactionError { /// Stake amount below minimum #[fail( display = "The amount of coins in stake ({}) is less than the minimum allowed ({})", - min_stake, stake + stake, min_stake )] StakeBelowMinimum { min_stake: u64, stake: u64 }, /// Unstaking more than the total staked #[fail( - display = "Unstaking ({}) more than the total staked ({})", + display = "Tried to unstake more coins than the current stake ({} > {})", unstake, stake )] UnstakingMoreThanStaked { stake: u64, unstake: u64 }, diff --git a/data_structures/src/lib.rs b/data_structures/src/lib.rs index c43328c40c..5524270a0d 100644 --- a/data_structures/src/lib.rs +++ b/data_structures/src/lib.rs @@ -17,8 +17,11 @@ use std::sync::RwLock; use lazy_static::lazy_static; -use crate::{chain::{Environment, Epoch}, proto::versioning::ProtocolVersion}; use crate::proto::versioning::ProtocolInfo; +use crate::{ + chain::{Environment, Epoch}, + proto::versioning::ProtocolVersion, +}; /// Module containing functions to generate Witnet's protocol messages pub mod builders; @@ -129,7 +132,7 @@ pub fn get_protocol_version(epoch: Option) -> ProtocolVersion { if let Some(epoch) = epoch { protocol_info.all_versions.version_for_epoch(epoch) } else { - *protocol_info.current_version + protocol_info.current_version } } @@ -137,14 +140,14 @@ pub fn register_protocol_version(epoch: Epoch, 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. let mut protocol_info = PROTOCOL.write().unwrap(); - *protocol_info.register(epoch, protocol_version); + protocol_info.register(epoch, protocol_version); } /// Set the protocol version that we are running. /// #[cfg(not(test))] pub fn set_protocol_version(protocol_version: ProtocolVersion) { let mut protocol = PROTOCOL.write().unwrap(); - *protocol.current_version = protocol_version; + protocol.current_version = protocol_version; } #[cfg(test)] @@ -159,10 +162,36 @@ mod tests { } #[test] - fn default_protocol_version() { + fn protocol_versions() { // 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_7); + let version = get_protocol_version(None); + assert_eq!(version, ProtocolVersion::V1_7); + + // Register the different protocol versions + register_protocol_version(100, ProtocolVersion::V1_7); + register_protocol_version(200, ProtocolVersion::V1_8); + register_protocol_version(300, ProtocolVersion::V2_0); + + // The initial protocol version should be the default one + let version = get_protocol_version(Some(0)); + assert_eq!(version, ProtocolVersion::V1_7); + + // Right after the + let version = get_protocol_version(Some(100)); + assert_eq!(version, ProtocolVersion::V1_7); + let version = get_protocol_version(Some(200)); + assert_eq!(version, ProtocolVersion::V1_8); + let version = get_protocol_version(Some(300)); + assert_eq!(version, ProtocolVersion::V2_0); + + let version = get_protocol_version(None); + assert_eq!(version, ProtocolVersion::V1_7); + + set_protocol_version(ProtocolVersion::V2_0); + + let version = get_protocol_version(None); + assert_eq!(version, ProtocolVersion::V2_0); } } diff --git a/data_structures/src/proto/versioning.rs b/data_structures/src/proto/versioning.rs index d0e76f8a5c..3a441193da 100644 --- a/data_structures/src/proto/versioning.rs +++ b/data_structures/src/proto/versioning.rs @@ -23,7 +23,7 @@ use crate::{ types::Message, }; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct ProtocolInfo { pub current_version: ProtocolVersion, pub all_versions: VersionsMap, @@ -48,12 +48,12 @@ impl VersionsMap { } pub fn version_for_epoch(&self, queried_epoch: Epoch) -> ProtocolVersion { - *self - .vfe + self.vfe .iter() .rev() - .find(|(epoch, _)| **epoch < queried_epoch) + .find(|(epoch, _)| **epoch <= queried_epoch) .map(|(_, version)| version) + .copied() .unwrap_or_default() } } diff --git a/node/src/actors/chain_manager/handlers.rs b/node/src/actors/chain_manager/handlers.rs index 95d19597de..ec28d633f9 100644 --- a/node/src/actors/chain_manager/handlers.rs +++ b/node/src/actors/chain_manager/handlers.rs @@ -1296,7 +1296,7 @@ impl Handler for ChainManager { type Result = ResponseActFuture::Result>; fn handle(&mut self, msg: BuildStake, _ctx: &mut Self::Context) -> Self::Result { - if self.sm_state != StateMachine::Synced { + if !msg.dry_run && self.sm_state != StateMachine::Synced { return Box::pin(actix::fut::err( ChainManagerError::NotSynced { current_state: self.sm_state, diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 0f37245608..1263987b5e 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()); - matches!(verify_signatures(signatures_to_verify, vrf), Ok(_)); + assert!(verify_signatures(signatures_to_verify, vrf).is_ok()); } static MILLION_TX_OUTPUT: &str = diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index d668929f7e..47e7379f4e 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -682,8 +682,6 @@ impl ChainManager { block_epoch: block.block_header.beacon.checkpoint, }; - println!("1"); - let mut transaction_visitor = PriorityVisitor::default(); let utxo_diff = process_validations( @@ -703,14 +701,12 @@ impl ChainManager { Some(&mut transaction_visitor), )?; - println!("2"); // Extract the collected priorities from the internal state of the visitor let priorities = transaction_visitor.take_state(); // Persist block and update ChainState self.consolidate_block(ctx, block, utxo_diff, priorities, resynchronizing); - println!("3"); Ok(()) } else { Err(ChainManagerError::ChainNotReady.into()) @@ -2778,7 +2774,6 @@ pub fn process_validations( active_wips: &ActiveWips, transaction_visitor: Option<&mut dyn Visitor>, ) -> Result { - println!("pv1"); if !resynchronizing { let mut signatures_to_verify = vec![]; validate_block( @@ -2793,7 +2788,7 @@ pub fn process_validations( )?; verify_signatures(signatures_to_verify, vrf_ctx)?; } - println!("pv2"); + let mut signatures_to_verify = vec![]; let utxo_dif = validate_block_transactions( utxo_set, @@ -2808,11 +2803,11 @@ pub fn process_validations( active_wips, transaction_visitor, )?; - println!("pv3"); + if !resynchronizing { verify_signatures(signatures_to_verify, vrf_ctx)?; } - println!("pv4"); + Ok(utxo_dif) } diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index 7817931e19..0c104b1d04 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -19,6 +19,7 @@ use itertools::Itertools; use jsonrpc_core::{BoxFuture, Error, Params, Value}; use jsonrpc_pubsub::{Subscriber, SubscriptionId}; use serde::{Deserialize, Serialize}; + use witnet_crypto::key::KeyPath; use witnet_data_structures::{ chain::{ @@ -38,12 +39,13 @@ use crate::{ json_rpc::Subscriptions, messages::{ AddCandidates, AddPeers, AddTransaction, AuthorizeStake, BuildDrt, BuildStake, - BuildStakeParams, BuildVtt, ClearPeers, DropAllPeers, EstimatePriority, GetBalance, - GetBalanceTarget, GetBlocksEpochRange, GetConsolidatedPeers, GetDataRequestInfo, - GetEpoch, GetHighestCheckpointBeacon, GetItemBlock, GetItemSuperblock, - GetItemTransaction, GetKnownPeers, GetMemoryTransaction, GetMempool, GetNodeStats, - GetReputation, GetSignalingInfo, GetState, GetSupplyInfo, GetUtxoInfo, InitializePeers, - IsConfirmedBlock, Rewind, SnapshotExport, SnapshotImport, StakeAuthorization, + BuildStakeParams, BuildStakeResponse, BuildVtt, ClearPeers, DropAllPeers, + EstimatePriority, GetBalance, GetBalanceTarget, GetBlocksEpochRange, + GetConsolidatedPeers, GetDataRequestInfo, GetEpoch, GetHighestCheckpointBeacon, + GetItemBlock, GetItemSuperblock, GetItemTransaction, GetKnownPeers, + GetMemoryTransaction, GetMempool, GetNodeStats, GetReputation, GetSignalingInfo, + GetState, GetSupplyInfo, GetUtxoInfo, InitializePeers, IsConfirmedBlock, Rewind, + SnapshotExport, SnapshotImport, StakeAuthorization, }, peers_manager::PeersManager, sessions_manager::SessionsManager, @@ -1942,14 +1944,26 @@ pub async fn snapshot_import(params: Result) -> Jso pub async fn stake(params: Result) -> JsonRpcResult { // Short-circuit if parameters are wrong let params = params?; + let validator; // If a withdrawer address is not specified, default to local node address + let withdrawer_was_provided = params.withdrawer.is_some(); let withdrawer = if let Some(address) = params.withdrawer { - address.try_do_magic(|hex_str| PublicKeyHash::from_bech32(get_environment(), &hex_str)).unwrap() + let address = address + .try_do_magic(|hex_str| PublicKeyHash::from_bech32(get_environment(), &hex_str)) + .map_err(internal_error)?; + log::debug!("[STAKE] A withdrawer address was provided: {}", address); + + address } else { let pk = signature_mngr::public_key().await.unwrap(); + let address = PublicKeyHash::from_public_key(&pk); + log::debug!( + "[STAKE] No withdrawer address was provided, using the node's own address: {}", + address + ); - PublicKeyHash::from_public_key(&pk) + address }; // This is the actual message that gets signed as part of the authorization @@ -1957,12 +1971,34 @@ pub async fn stake(params: Result) -> JsonRpcResult { // If no authorization message is provided, generate a new one using the withdrawer address let authorization = if let Some(signature) = params.authorization { - signature.do_magic(|hex_str| KeyedSignature::from_recoverable_hex(&hex_str, &msg)) + // TODO: change to `try_do_magic` once `from_recoverable_hex` is made safe + let signature = + signature.do_magic(|hex_str| KeyedSignature::from_recoverable_hex(&hex_str, &msg)); + validator = PublicKeyHash::from_public_key(&signature.public_key); + log::debug!( + "[STAKE] A stake authorization was provided, and it was signed by validator {}", + validator + ); + + // Avoid the risky situation where an authorization is provided, but it's authorizing a 3rd-party withdrawer + // without stating the withdrawer address explicitly. Why is this risky? Because the validator address is + // derived from the authorization itself using ECDSA recovery. If the withdrawer used here does not match the + // one that was authorized, the resulting stake will not only be non-withdrawable, but also not operable by the + // validator, because the ECDSA recovery will recover the wrong public key. + if !withdrawer_was_provided && withdrawer != validator { + return Err(internal_error_s("The provided authorization is signed by a third party but no withdrawer address was provided. Please provide a withdrawer address.")); + } + + signature } else { - signature_mngr::sign_data(msg) + let signature = signature_mngr::sign_data(msg) .map(|res| res.map_err(internal_error)) .await - .unwrap() + .map_err(internal_error)?; + validator = PublicKeyHash::from_public_key(&signature.public_key); + log::debug!("[STAKE] No stake authorization was provided, producing one using the node's own address: {}", validator); + + signature }; // Construct a BuildStake message that we can relay to the ChainManager for creation of the Stake transaction @@ -1979,13 +2015,30 @@ pub async fn stake(params: Result) -> JsonRpcResult { ChainManager::from_registry() .send(build_stake) .map(|res| match res { - Ok(Ok(hash)) => match serde_json::to_value(hash) { - Ok(x) => Ok(x), - Err(e) => { - let err = internal_error_s(e); - Err(err) + Ok(Ok(transaction)) => { + // In the event that this is a dry run, we want to inject some additional information into the + // response, so that the user can confirm the facts surrounding the stake transaction before + // submitting it + if params.dry_run { + let staker = transaction + .signatures + .iter() + .cloned() + .map(|signature| signature.public_key.pkh()) + .collect(); + + let bsr = BuildStakeResponse { + transaction, + staker, + validator, + withdrawer, + }; + + serde_json::to_value(bsr).map_err(|e| internal_error(e)) + } else { + serde_json::to_value(transaction).map_err(|e| internal_error(e)) } - }, + } Ok(Err(e)) => { let err = internal_error_s(e); Err(err) diff --git a/node/src/actors/messages.rs b/node/src/actors/messages.rs index 2cb819af90..7cf75f5110 100644 --- a/node/src/actors/messages.rs +++ b/node/src/actors/messages.rs @@ -9,19 +9,21 @@ use std::{ time::Duration, }; -use actix::{Actor, Addr, dev::{MessageResponse, OneshotSender, ToEnvelope}, Handler, Message}; +use actix::{ + dev::{MessageResponse, OneshotSender, ToEnvelope}, + Actor, Addr, Handler, Message, +}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use tokio::net::TcpStream; -use witnet_crypto::secp256k1::ecdsa::{RecoverableSignature, RecoveryId}; use witnet_data_structures::{ chain::{ - Block, - CheckpointBeacon, - DataRequestInfo, DataRequestOutput, Epoch, EpochConstants, Hash, InventoryEntry, InventoryItem, - KeyedSignature, NodeStats, PointerToBlock, priority::PrioritiesEstimate, PublicKeyHash, PublicKeyHashParseError, - RADRequest, RADTally, Reputation, StakeOutput, StateMachine, SuperBlock, - SuperBlockVote, SupplyInfo, tapi::{ActiveWips, BitVotesCounter}, ValueTransferOutput, + priority::PrioritiesEstimate, + tapi::{ActiveWips, BitVotesCounter}, + Block, CheckpointBeacon, DataRequestInfo, DataRequestOutput, Epoch, EpochConstants, Hash, + InventoryEntry, InventoryItem, KeyedSignature, NodeStats, PointerToBlock, PublicKeyHash, + PublicKeyHashParseError, RADRequest, RADTally, Reputation, StakeOutput, StateMachine, + SuperBlock, SuperBlockVote, SupplyInfo, ValueTransferOutput, }, fee::{deserialize_fee_backwards_compatible, Fee}, radon_report::RadonReport, @@ -222,7 +224,7 @@ impl Message for BuildVtt { /// Builds a `StakeTransaction` from a list of `ValueTransferOutput`s #[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct BuildStake { - /// List of `ValueTransferOutput`s + /// One instance of `StakeOutput` pub stake_output: StakeOutput, /// Fee #[serde(default)] @@ -262,6 +264,22 @@ pub struct BuildStakeParams { pub dry_run: bool, } +/// The response to a `BuildStake` message. It gives important feedback about the addresses that will be involved in a +/// stake transactions, subject to review and confirmation from the user. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct BuildStakeResponse { + /// A stake transaction that has been created as a response to a `BuildStake` message. + pub transaction: StakeTransaction, + /// The addresses of the staker. These are the addresses used in the stake transaction inputs. + pub staker: Vec, + /// The address of the validator. This shall be the address of the node that will operate this stake on behalf of + /// the staker. + pub validator: PublicKeyHash, + /// The address of the withdrawer. This shall be the an address controlled by the staker. When unstaking, the + /// staked principal plus any yield will only be spendable by this address. + pub withdrawer: PublicKeyHash, +} + /// Builds an `AuthorizeStake` #[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct AuthorizeStake { @@ -1360,7 +1378,10 @@ pub enum MagicEither { impl MagicEither { /// Obtain an R value, even if this was an instance of L. - pub fn do_magic(self, trick: F) -> R where F: Fn(L) -> R { + pub fn do_magic(self, trick: F) -> R + where + F: Fn(L) -> R, + { match self { Self::Left(l) => trick(l), Self::Right(r) => r, @@ -1368,10 +1389,13 @@ impl MagicEither { } /// Fallible version of `do_magic`. - pub fn try_do_magic(self, trick: F) -> Result where F: Fn(L) -> Result { + pub fn try_do_magic(self, trick: F) -> Result + where + F: Fn(L) -> Result, + { match self { Self::Left(l) => trick(l), Self::Right(r) => Ok(r), } } -} \ No newline at end of file +} diff --git a/src/cli/node/json_rpc_client.rs b/src/cli/node/json_rpc_client.rs index 0b6b7cdd65..d74e65203a 100644 --- a/src/cli/node/json_rpc_client.rs +++ b/src/cli/node/json_rpc_client.rs @@ -5,16 +5,7 @@ use num_format::{Locale, ToFormattedString}; use prettytable::{row, Table}; use qrcode::render::unicode; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::{ - collections::HashMap, - convert::TryFrom, - fmt, - fs::File, - io::{self, BufRead, BufReader, Read, Write}, - net::{SocketAddr, TcpStream}, - path::Path, - str::FromStr, -}; + use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; use witnet_crypto::{ hash::calculate_sha256, @@ -29,6 +20,7 @@ use witnet_data_structures::{ SupplyInfo, SyncStatus, ValueTransferOutput, }, fee::Fee, + get_environment, proto::ProtobufConvert, transaction::{DRTransaction, StakeTransaction, Transaction, VTTransaction}, transaction_factory::NodeBalance, @@ -36,21 +28,32 @@ use witnet_data_structures::{ utxo_pool::{UtxoInfo, UtxoSelectionStrategy}, wit::Wit, }; +use witnet_node::actors::messages::{BuildStakeResponse, MagicEither}; use witnet_node::actors::{ chain_manager::run_dr_locally, json_rpc::api::{AddrType, GetBlockChainParams, GetTransactionOutput, PeersResult}, messages::{ - AuthorizeStake, BuildDrt, BuildStakeParams, BuildVtt, - GetBalanceTarget, GetReputationResult, SignalingInfo, StakeAuthorization, + AuthorizeStake, BuildDrt, BuildStakeParams, BuildVtt, GetBalanceTarget, + GetReputationResult, SignalingInfo, StakeAuthorization, }, }; -use witnet_node::actors::messages::MagicEither; use witnet_rad::types::RadonTypes; use witnet_util::{files::create_private_file, timestamp::pretty_print}; use witnet_validations::validations::{ run_tally_panic_safe, validate_data_request_output, validate_rad_request, }; +use std::{ + collections::{BTreeSet, HashMap}, + convert::TryFrom, + fmt, + fs::File, + io::{self, BufRead, BufReader, Read, Write}, + net::{SocketAddr, TcpStream}, + path::Path, + str::FromStr, +}; + pub fn raw(addr: SocketAddr) -> Result<(), failure::Error> { let mut stream = start_client(addr)?; // The request is read from stdin, one line at a time @@ -862,23 +865,16 @@ pub fn send_dr( pub fn send_st( addr: SocketAddr, value: u64, - withdrawer: Option, + authorization: Option>, + withdrawer: Option>, fee: Option, sorted_bigger: Option, + requires_confirmation: Option, dry_run: bool, ) -> Result<(), failure::Error> { let mut stream = start_client(addr)?; let mut id = SequentialId::initialize(1u8); - let authorize_stake_params = AuthorizeStake { withdrawer }; - - let (stake_authorization, (_, _response)): (StakeAuthorization, _) = issue_method( - "authorizeStake", - Some(authorize_stake_params), - &mut stream, - id.next(), - )?; - // Prepare for fee estimation if no fee value was specified let (fee, estimate) = unwrap_fee_or_estimate_priority(fee, &mut stream, &mut id)?; @@ -889,8 +885,8 @@ pub fn send_st( }; let mut build_stake_params = BuildStakeParams { - authorization: Some(MagicEither::Right(stake_authorization.signature)), - withdrawer: Some(MagicEither::Right(stake_authorization.withdrawer)), + authorization, + withdrawer, value, fee, utxo_strategy, @@ -966,7 +962,19 @@ pub fn send_st( build_stake_params.fee = prompt_user_for_priority_selection(estimates)?; } + if requires_confirmation.unwrap_or(true) { + let params = BuildStakeParams { + dry_run: true, + ..build_stake_params.clone() + }; + let (bsr, _): (BuildStakeResponse, _) = + issue_method("stake", Some(params), &mut stream, id.next())?; + + prompt_user_for_stake_confirmation(bsr)?; + } + // Finally ask the node to create the transaction with the chosen fee. + build_stake_params.dry_run = dry_run; let (_st, (request, response)): (StakeTransaction, _) = issue_method("stake", Some(build_stake_params), &mut stream, id.next())?; @@ -990,7 +998,9 @@ pub fn authorize_st(addr: SocketAddr, withdrawer: Option) -> Result<(), let (authorization, (_, _response)): (StakeAuthorization, _) = issue_method("authorizeStake", Some(params), &mut stream, id.next())?; - let auth_bytes = authorization.signature.signature.to_bytes()?; + let message = authorization.withdrawer.as_secp256k1_msg(); + + let auth_bytes = authorization.signature.to_recoverable_bytes(&message); let auth_string = hex::encode(auth_bytes); let auth_qr = qrcode::QrCode::new(&auth_string)?; @@ -2053,6 +2063,74 @@ fn prompt_user_for_priority_selection( Ok(fee) } +fn prompt_user_for_stake_confirmation(data: BuildStakeResponse) -> Result<(), failure::Error> { + let environment = get_environment(); + let value = Wit::from_nanowits(data.transaction.body.output.value).to_string(); + + // Time to print the data + println!("╔══════════════════════════════════════════════════════════════════════════════╗"); + println!("║ PLEASE CAREFULLY REVIEW THE DATA BELOW ║"); + println!("╟──────────────────────────────────────────────────────────────────────────────╢"); + println!("║ Failing to review this information diligently may result in stakes that ║"); + println!("║ cannot be operated or withdrawn, i.e. loss of funds. ║"); + println!("╠══════════════════════════════════════════════════════════════════════════════╣"); + println!("║ 1. STAKER ADDRESSES ║"); + println!("║ These are the addresses from which the coins to stake will be sourced. ║"); + println!("║ None of these addresses will be able to unstake or spend the staked ║"); + println!("║ coins, unless one of them is also the withdrawer address below. ║"); + println!("║ ║"); + for (i, address) in data + .staker + .iter() + .collect::>() + .into_iter() + .enumerate() + { + let address = address.bech32(environment); + println!("║ #{:0>2}: {: <69}║", i, address); + } + println!("╟──────────────────────────────────────────────────────────────────────────────╢"); + println!("║ 2. VALIDATOR ADDRESS ║"); + println!("║ This is the address of the node that will be operating the staked coins. ║"); + println!("║ The validator will not be able to unstake or spend the staked coins — ║"); + println!("║ that role is reserved for the withdrawer address below. ║"); + println!("║ ║"); + println!( + "║ Validator address: {: <55}║", + data.validator.bech32(environment) + ); + println!("╟──────────────────────────────────────────────────────────────────────────────╢"); + println!("║ 3. WITHDRAWER ADDRESS ║"); + println!("║ This is the only address that will be allowed to unstake and eventually ║"); + println!("║ spend the staked coins, and the accumulated rewards if any. ║"); + println!("║ This MUST belong to your wallet, otherwise you may be giving away or ║"); + println!("║ or burning your coins. ║"); + println!("║ ║"); + println!( + "║ Withdrawer address: {: <54}║", + data.withdrawer.bech32(environment) + ); + println!("╟──────────────────────────────────────────────────────────────────────────────╢"); + println!("║ 4. STAKE AMOUNT ║"); + println!("║ This is the number of coins that will be staked. While staked, the coins ║"); + println!("║ cannot be transferred or spent. They can only be unstaked and eventually ║"); + println!("║ spent by the withdrawer address above. ║"); + println!("║ ║"); + println!("║ Stake amount: {} {: <44}║", value, "Wit coins"); + println!("╚══════════════════════════════════════════════════════════════════════════════╝"); + + // This is where we prompt the user for typing the desired priority tier from the options + // printed above. This is done in a loop until a valid option is selected. + let mut input = String::new(); + let stdin = io::stdin(); + let mut stdin = stdin.lock(); + io::stdout().flush()?; + input.clear(); + stdin.read_line(&mut input)?; + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/cli/node/with_node.rs b/src/cli/node/with_node.rs index 408ef70a02..567e32f918 100644 --- a/src/cli/node/with_node.rs +++ b/src/cli/node/with_node.rs @@ -10,7 +10,7 @@ use structopt::StructOpt; use witnet_config::config::Config; use witnet_data_structures::{chain::Epoch, fee::Fee}; use witnet_node as node; -use witnet_node::actors::messages::GetBalanceTarget; +use witnet_node::actors::messages::{GetBalanceTarget, MagicEither}; use super::json_rpc_client as rpc; @@ -272,15 +272,19 @@ pub fn exec_cmd( Command::Stake { node, value, + authorization, withdrawer, fee, + require_confirmation, dry_run, } => rpc::send_st( node.unwrap_or(default_jsonrpc), value, - withdrawer, + authorization.map(MagicEither::Left), + withdrawer.map(MagicEither::Left), fee.map(Fee::absolute_from_nanowits), None, + require_confirmation, dry_run, ), Command::AuthorizeStake { node, withdrawer } => { @@ -755,14 +759,19 @@ pub enum Command { /// Value #[structopt(long = "value")] value: u64, + /// Stake authorization code (the withdrawer address, signed by the validator node) + #[structopt(long = "authorization")] + authorization: Option, /// Withdrawer #[structopt(long = "withdrawer")] - // make it also optional in jsonrcp - // see get balance in main withdrawer: Option, /// Fee #[structopt(long = "fee")] fee: Option, + /// If unset or set to true, the command is interactive and prompts for user confirmation. + /// If set to false, skip confirmation and complete the command without user confirmation. + #[structopt(long = "require_confirmation")] + require_confirmation: Option, /// Print the request that would be sent to the node and exit without doing anything #[structopt(long = "dry-run")] dry_run: bool, diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 30d33d009c..a566a69346 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -29,6 +29,7 @@ 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::{ @@ -1601,7 +1602,6 @@ pub fn validate_block_transactions( let epoch = block.block_header.beacon.checkpoint; let is_genesis = block.is_genesis(&consensus_constants.genesis_hash); let mut utxo_diff = UtxoDiff::new(utxo_set, block_number); - println!("vbt1"); // Init total fee let mut total_fee = 0; // When validating genesis block, keep track of total value created @@ -1618,7 +1618,6 @@ pub fn validate_block_transactions( // Validate value transfer transactions in a block let mut vt_mt = ProgressiveMerkleTree::sha256(); let mut vt_weight: u32 = 0; - println!("vbt2"); for transaction in &block.txns.value_transfer_txns { let (inputs, outputs, fee, weight) = if is_genesis { @@ -1671,7 +1670,6 @@ pub fn validate_block_transactions( } let vt_hash_merkle_root = vt_mt.root(); - println!("vbt3"); // Validate commit transactions in a block let mut co_mt = ProgressiveMerkleTree::sha256(); let mut commits_number = HashMap::new(); @@ -1682,7 +1680,6 @@ pub fn validate_block_transactions( } else { consensus_constants.collateral_age }; - println!("vbt4"); for transaction in &block.txns.commit_txns { let (dr_pointer, dr_witnesses, fee) = validate_commit_transaction( transaction, @@ -1724,7 +1721,6 @@ pub fn validate_block_transactions( } let co_hash_merkle_root = co_mt.root(); - println!("vbt5"); // Validate commits number and add commit fees for WitnessesCount { current, target } in commits_number.values() { if current != target { @@ -1736,7 +1732,6 @@ pub fn validate_block_transactions( } } - println!("vbt6"); // Validate reveal transactions in a block let mut re_mt = ProgressiveMerkleTree::sha256(); let mut reveal_hs = HashSet::with_capacity(block.txns.reveal_txns.len()); @@ -1758,7 +1753,6 @@ pub fn validate_block_transactions( re_mt.push(Sha256(sha)); } let re_hash_merkle_root = re_mt.root(); - println!("vbt7"); // Validate tally transactions in a block let mut ta_mt = ProgressiveMerkleTree::sha256(); @@ -1801,7 +1795,6 @@ pub fn validate_block_transactions( ta_mt.push(Sha256(sha)); } let ta_hash_merkle_root = ta_mt.root(); - println!("vbt8"); // All data requests for which we expected tally transactions should have been removed // upon creation of the tallies. If not, block is invalid due to missing expected tallies @@ -1812,7 +1805,6 @@ pub fn validate_block_transactions( } .into()); } - println!("vbt9"); let mut dr_weight: u32 = 0; if active_wips.wip_0008() { @@ -1833,7 +1825,6 @@ pub fn validate_block_transactions( } } } - println!("vbt10"); // Validate data request transactions in a block let mut dr_mt = ProgressiveMerkleTree::sha256(); @@ -1880,7 +1871,6 @@ pub fn validate_block_transactions( } let dr_hash_merkle_root = dr_mt.root(); - println!("vbt11"); if !is_genesis { // Validate mint validate_mint_transaction( @@ -1899,211 +1889,109 @@ pub fn validate_block_transactions( block.txns.mint.hash(), ); } - println!("vbt12"); - // 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; - - // 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(); - - println!("vbt13"); - - if let Some(duplicate) = duplicate { - return Err(BlockError::RepeatedStakeOperator { - pkh: duplicate.pkh(), - } - .into()); - } - println!("vbt14"); - - 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, + let protocol_version = get_protocol_version(Some(epoch)); + let (st_root, ut_root) = if protocol_version != ProtocolVersion::V1_7 { + // 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()); } - st_weight = acc_weight; - - let outputs = change.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)); - // } - } - println!("vbt15"); - - let mut ut_mt = ProgressiveMerkleTree::sha256(); - let mut ut_weight: u32 = 0; + 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, + )?; - 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; - 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, + // 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()); } - .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)); - // } - } - println!("vbt16"); - - // 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_7 => Default::default(), - _ => (Hash::from(st_mt.root()), Hash::from(ut_mt.root())), - }; - - // TODO skip all staking logic if protocol version is legacy + st_weight = acc_weight; - // validate stake transactions in a block - let mut st_mt = ProgressiveMerkleTree::sha256(); - let mut st_weight: u32 = 0; + let outputs = change.iter().collect_vec(); + update_utxo_diff(&mut utxo_diff, inputs, outputs, transaction.hash()); - // 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(); + // Add new hash to merkle tree + st_mt.push(transaction.hash().into()); - if let Some(duplicate) = duplicate { - return Err(BlockError::RepeatedStakeOperator { - pkh: duplicate.pkh(), + // 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)); + // } } - .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.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 mut ut_mt = ProgressiveMerkleTree::sha256(); + let mut ut_weight: u32 = 0; - 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)?; + 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; + 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, + // 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()); } - .into()); - } - ut_weight = acc_weight; + 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)); + // 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)); - // } - } + // 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)); + // } + } - // Nullify roots for legacy protocol version - // TODO skip all staking logic if protocol version is legacy - let (st_root, ut_root) = match ProtocolVersion::guess() { - ProtocolVersion::V1_6 => Default::default(), - _ => (Hash::from(st_mt.root()), Hash::from(ut_mt.root())), + (Hash::from(st_mt.root()), Hash::from(ut_mt.root())) + } else { + // Nullify stake and unstake merkle roots for the legacy protocol version + Default::default() }; // Validate Merkle Root @@ -2117,7 +2005,6 @@ pub fn validate_block_transactions( stake_hash_merkle_root: st_root, unstake_hash_merkle_root: ut_root, }; - println!("vbt17"); if merkle_roots != block.block_header.merkle_roots { println!( From 0618bd3c7f26dbcac463be41b0d10f1130cdc620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Thu, 25 Jan 2024 11:39:18 +0100 Subject: [PATCH 18/32] feat(data_structures): enable protocol versions injection through config also: make protocol versioning safer --- Cargo.lock | 21 ++++++++ config/src/config.rs | 71 +++++++++++++++++++++++-- config/src/defaults.rs | 19 +++++-- data_structures/Cargo.toml | 2 + data_structures/src/chain/mod.rs | 66 +++++++++++++++-------- data_structures/src/error.rs | 13 ++++- data_structures/src/lib.rs | 21 ++++++-- data_structures/src/proto/versioning.rs | 24 +++------ node/src/actors/json_rpc/api.rs | 10 ++-- src/cli/mod.rs | 7 +++ src/cli/node/json_rpc_client.rs | 3 +- 11 files changed, 198 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41a3ce131f..ad887b104b 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -3935,6 +3935,25 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2 1.0.60", + "quote 1.0.28", + "rustversion", + "syn 2.0.18", +] + [[package]] name = "subtle" version = "1.0.0" @@ -5284,6 +5303,8 @@ dependencies = [ "serde", "serde_cbor", "serde_json", + "strum", + "strum_macros", "vrf", "witnet_crypto", "witnet_protected", diff --git a/config/src/config.rs b/config/src/config.rs index 72e68f2d6c..dbc5fc5eb5 100644 --- a/config/src/config.rs +++ b/config/src/config.rs @@ -38,16 +38,20 @@ //! // Default config for mainnet //! // Config::from_partial(&PartialConfig::default_mainnet()); //! ``` -use std::convert::TryFrom; use std::{ - collections::HashSet, fmt, marker::PhantomData, net::SocketAddr, path::PathBuf, time::Duration, + array::IntoIter, collections::HashSet, convert::TryFrom, fmt, marker::PhantomData, + net::SocketAddr, path::PathBuf, time::Duration, }; -use partial_struct::PartialStruct; use serde::{de, Deserialize, Deserializer, Serialize}; + +use partial_struct::PartialStruct; use witnet_crypto::hash::HashFunction; -use witnet_data_structures::chain::{ConsensusConstants, Environment, PartialConsensusConstants}; -use witnet_data_structures::witnessing::WitnessingConfig; +use witnet_data_structures::{ + chain::{ConsensusConstants, Environment, Epoch, PartialConsensusConstants}, + proto::versioning::ProtocolVersion, + witnessing::WitnessingConfig, +}; use witnet_protected::ProtectedString; use crate::{ @@ -125,6 +129,11 @@ pub struct Config { #[partial_struct(ty = "PartialWitnessing")] #[partial_struct(serde(default))] pub witnessing: Witnessing, + + /// Configuration related with protocol versions + #[partial_struct(skip)] + #[partial_struct(serde(default))] + pub protocol: Protocol, } /// Log-specific configuration. @@ -420,6 +429,25 @@ pub struct Tapi { pub oppose_wip0027: bool, } +/// Configuration related to protocol versions. +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub struct Protocol { + pub v1_7: Option, + pub v1_8: Option, + pub v2_0: Option, +} + +impl Protocol { + pub fn iter(&self) -> IntoIter<(ProtocolVersion, Option), 3> { + [ + (ProtocolVersion::V1_7, self.v1_7), + (ProtocolVersion::V1_8, self.v1_8), + (ProtocolVersion::V2_0, self.v2_0), + ] + .into_iter() + } +} + fn to_partial_consensus_constants(c: &ConsensusConstants) -> PartialConsensusConstants { PartialConsensusConstants { checkpoint_zero_timestamp: Some(c.checkpoint_zero_timestamp), @@ -450,6 +478,13 @@ fn to_partial_consensus_constants(c: &ConsensusConstants) -> PartialConsensusCon } } +pub trait Partializable { + type Partial; + + fn from_partial(config: &Self::Partial, defaults: &dyn Defaults) -> Self; + fn to_partial(&self) -> Self::Partial; +} + impl Config { pub fn from_partial(config: &PartialConfig) -> Self { let defaults: &dyn Defaults = match config.environment { @@ -478,6 +513,7 @@ impl Config { mempool: Mempool::from_partial(&config.mempool, defaults), tapi: config.tapi.clone(), witnessing: Witnessing::from_partial(&config.witnessing, defaults), + protocol: Protocol::from_partial(&config.protocol, defaults), } } @@ -496,6 +532,7 @@ impl Config { mempool: self.mempool.to_partial(), tapi: self.tapi.clone(), witnessing: self.witnessing.to_partial(), + protocol: self.protocol.to_partial(), } } } @@ -1171,6 +1208,30 @@ impl Witnessing { } } +impl Partializable for Protocol { + type Partial = Self; + + fn from_partial(config: &Self::Partial, defaults: &dyn Defaults) -> Self { + let defaults = defaults.protocol_versions(); + + Protocol { + v1_7: config + .v1_7 + .or(defaults.get(&ProtocolVersion::V1_7).copied()), + v1_8: config + .v1_8 + .or(defaults.get(&ProtocolVersion::V1_8).copied()), + v2_0: config + .v2_0 + .or(defaults.get(&ProtocolVersion::V2_0).copied()), + } + } + + fn to_partial(&self) -> Self::Partial { + self.clone() + } +} + // Serialization helpers fn as_log_filter_string( diff --git a/config/src/defaults.rs b/config/src/defaults.rs index 6d7d924822..7d05763160 100644 --- a/config/src/defaults.rs +++ b/config/src/defaults.rs @@ -2,13 +2,18 @@ //! //! This module contains per-environment default values for the Witnet //! protocol params. -use std::collections::HashSet; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::path::PathBuf; -use std::time::Duration; +use std::{ + collections::{HashMap, HashSet}, + net::{IpAddr, Ipv4Addr, SocketAddr}, + path::PathBuf, + time::Duration, +}; use witnet_crypto::hash::HashFunction; -use witnet_data_structures::chain::Hash; +use witnet_data_structures::{ + chain::{Epoch, Hash}, + proto::versioning::ProtocolVersion, +}; use witnet_protected::ProtectedString; // When changing the defaults, remember to update the documentation! @@ -475,6 +480,10 @@ pub trait Defaults { fn mempool_max_reinserted_transactions(&self) -> u32 { 100 } + + fn protocol_versions(&self) -> HashMap { + [(ProtocolVersion::V1_7, 0)].into_iter().collect() + } } /// Allow setting a reward to collateral percentage for a data request to be included in a block diff --git a/data_structures/Cargo.toml b/data_structures/Cargo.toml index 29e57caeb2..a8e0c4f39b 100644 --- a/data_structures/Cargo.toml +++ b/data_structures/Cargo.toml @@ -33,6 +33,8 @@ rand = "0.8.5" serde = { version = "1.0.104", features = ["derive"] } serde_cbor = "0.11.1" serde_json = "1.0.48" +strum = "0.25.0" +strum_macros = "0.25.3" vrf = "0.2.3" witnet_crypto = { path = "../crypto" } diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index cab521ca17..8ce6b34489 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -1569,50 +1569,74 @@ pub struct KeyedSignature { } impl KeyedSignature { - pub fn from_recoverable_hex(string: &str, msg: &[u8]) -> Self { - // FIXME: make this safe by using a `Result` instead of unwrapping - let bytes = hex::decode(string).unwrap(); + pub fn from_recoverable_hex( + string: &str, + msg: &[u8], + ) -> Result { + let bytes = hex::decode(string).map_err(|e| Secp256k1ConversionError::HexDecode { + hex: String::from(string), + inner: e, + })?; Self::from_recoverable_slice(&bytes, msg) } - pub fn from_recoverable(recoverable: &RecoverableSignature, message: &[u8]) -> Self { - // FIXME: make this safe by using a `Result` instead of unwrapping - let msg = secp256k1::Message::from_digest_slice(message).unwrap(); + pub fn from_recoverable( + recoverable: &RecoverableSignature, + message: &[u8], + ) -> Result { + let msg = secp256k1::Message::from_digest_slice(message) + .map_err(|e| Secp256k1ConversionError::Secp256k1 { inner: e })?; let signature = recoverable.to_standard(); - let public_key = recoverable.recover(&msg).unwrap(); + let public_key = recoverable + .recover(&msg) + .map_err(|e| Secp256k1ConversionError::Secp256k1 { inner: e })?; - KeyedSignature { + Ok(KeyedSignature { signature: signature.into(), public_key: public_key.into(), - } + }) } // Recovers a keyed signature from its serialized form and a known message. - pub fn from_recoverable_slice(compact: &[u8], message: &[u8]) -> Self { - // FIXME: make this safe by using a `Result` instead of unwrapping - let recid = RecoveryId::from_i32(compact[0] as i32).unwrap(); - let recoverable = RecoverableSignature::from_compact(&compact[1..], recid).unwrap(); + pub fn from_recoverable_slice( + compact: &[u8], + message: &[u8], + ) -> Result { + let recid = RecoveryId::from_i32(compact[0] as i32) + .map_err(|e| Secp256k1ConversionError::Secp256k1 { inner: e })?; + let recoverable = RecoverableSignature::from_compact(&compact[1..], recid) + .map_err(|e| Secp256k1ConversionError::Secp256k1 { inner: e })?; Self::from_recoverable(&recoverable, message) } /// Serializes a `KeyedSignature` into a compact encoding form that contains the public key recovery ID as a prefix. - pub fn to_recoverable_bytes(self, message: &[u8]) -> [u8; 65] { - // FIXME: make this safe by using a `Result` instead of unwrapping + pub fn to_recoverable_bytes( + self, + message: &[u8], + ) -> Result<[u8; 65], Secp256k1ConversionError> { let mut recoverable_bytes = [0; 65]; - recoverable_bytes[1..].clone_from_slice(&self.signature.to_bytes().unwrap()); - + let bytes = self + .signature + .to_bytes() + .map_err(|e| Secp256k1ConversionError::Other { + inner: e.to_string(), + })?; + recoverable_bytes[1..].clone_from_slice(&bytes); + + // Silly algorithm that tries recovery with different recovery IDs in an attempt to guess which one is correct, + // provided that our `KeyedSignature` misses that information in comparison with `RecoverableSignature` for i in 0..4 { recoverable_bytes[0] = i; - let recovered = KeyedSignature::from_recoverable_slice(&recoverable_bytes, message); + let recovered = KeyedSignature::from_recoverable_slice(&recoverable_bytes, message)?; if recovered.public_key == self.public_key { break; } } - recoverable_bytes + Ok(recoverable_bytes) } } @@ -4925,7 +4949,7 @@ mod tests { let secp = Secp256k1::new(); let secret_key = Secp256k1_SecretKey::from_slice(&[0xcd; 32]).expect("32 bytes, within curve order"); - let msg = Secp256k1_Message::from_slice(&data).unwrap(); + let msg = Secp256k1_Message::from_digest_slice(&data).unwrap(); let signature = secp.sign_ecdsa(&msg, &secret_key); let witnet_signature = Secp256k1Signature::from(signature); @@ -4946,7 +4970,7 @@ mod tests { let secp = Secp256k1::new(); let secret_key = Secp256k1_SecretKey::from_slice(&[0xcd; 32]).expect("32 bytes, within curve order"); - let msg = Secp256k1_Message::from_slice(&data).unwrap(); + let msg = Secp256k1_Message::from_digest_slice(&data).unwrap(); let signature = secp.sign_ecdsa(&msg, &secret_key); let witnet_signature = Signature::from(signature); diff --git a/data_structures/src/error.rs b/data_structures/src/error.rs index 754d2f7e48..24dda7e781 100644 --- a/data_structures/src/error.rs +++ b/data_structures/src/error.rs @@ -1,7 +1,9 @@ //! Error type definitions for the data structure module. use failure::Fail; +use hex::FromHexError; use std::num::ParseIntError; +use witnet_crypto::secp256k1; use crate::chain::{ DataRequestOutput, Epoch, Hash, HashParseError, OutputPointer, PublicKeyHash, RADType, @@ -484,7 +486,7 @@ pub enum OutputPointerParseError { } /// The error type for operations on a [`Secp256k1Signature`](Secp256k1Signature) -#[derive(Debug, PartialEq, Eq, Fail)] +#[derive(Debug, PartialEq, Fail)] pub enum Secp256k1ConversionError { #[fail( display = "Failed to convert `witnet_data_structures::Signature` into `secp256k1::Signature`" @@ -503,6 +505,15 @@ pub enum Secp256k1ConversionError { display = "Failed to convert `witnet_data_structures::SecretKey` into `secp256k1::SecretKey`" )] FailSecretKeyConversion, + #[fail( + display = "Cannot decode a `witnet_data_structures::KeyedSignature` from the allegedly hex-encoded string '{}': {}", + hex, inner + )] + HexDecode { hex: String, inner: FromHexError }, + #[fail(display = "{}", inner)] + Secp256k1 { inner: secp256k1::Error }, + #[fail(display = "{}", inner)] + Other { inner: String }, } /// The error type for operations on a [`DataRequestPool`](DataRequestPool) diff --git a/data_structures/src/lib.rs b/data_structures/src/lib.rs index 5524270a0d..67365e3009 100644 --- a/data_structures/src/lib.rs +++ b/data_structures/src/lib.rs @@ -136,7 +136,11 @@ pub fn get_protocol_version(epoch: Option) -> ProtocolVersion { } } -pub fn register_protocol_version(epoch: Epoch, protocol_version: ProtocolVersion) { +/// Let the protocol versions controller know about the a protocol version, and its activation epoch. +pub fn register_protocol_version(protocol_version: ProtocolVersion, epoch: Epoch) { + log::debug!( + "Registering protocol version {protocol_version}, which enters into force at epoch {epoch}" + ); // This unwrap is safe as long as the lock is not poisoned. // The lock can only become poisoned when a writer panics. let mut protocol_info = PROTOCOL.write().unwrap(); @@ -144,12 +148,19 @@ pub fn register_protocol_version(epoch: Epoch, protocol_version: ProtocolVersion } /// Set the protocol version that we are running. -/// #[cfg(not(test))] pub fn set_protocol_version(protocol_version: ProtocolVersion) { + // The lock can only become poisoned when a writer panics. let mut protocol = PROTOCOL.write().unwrap(); protocol.current_version = protocol_version; } +/// Refresh the protocol version, i.e. derive the current version from the current epoch, and update `current_version` +/// accordingly. +pub fn refresh_protocol_version(current_epoch: Epoch) { + let current_version = get_protocol_version(Some(current_epoch)); + set_protocol_version(current_version) +} + #[cfg(test)] mod tests { use super::*; @@ -170,9 +181,9 @@ mod tests { assert_eq!(version, ProtocolVersion::V1_7); // Register the different protocol versions - register_protocol_version(100, ProtocolVersion::V1_7); - register_protocol_version(200, ProtocolVersion::V1_8); - register_protocol_version(300, ProtocolVersion::V2_0); + register_protocol_version(ProtocolVersion::V1_7, 100); + register_protocol_version(ProtocolVersion::V1_8, 200); + register_protocol_version(ProtocolVersion::V2_0, 300); // The initial protocol version should be the default one let version = get_protocol_version(Some(0)); diff --git a/data_structures/src/proto/versioning.rs b/data_structures/src/proto/versioning.rs index 3a441193da..244c520378 100644 --- a/data_structures/src/proto/versioning.rs +++ b/data_structures/src/proto/versioning.rs @@ -1,8 +1,8 @@ use failure::{Error, Fail}; use protobuf::Message as _; +use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; -use std::fmt; -use std::fmt::Formatter; +use strum_macros::{Display, EnumString}; use crate::chain::Epoch; use crate::proto::schema::witnet::SuperBlock; @@ -58,7 +58,9 @@ impl VersionsMap { } } -#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +#[derive( + Clone, Copy, Debug, Default, Deserialize, Display, EnumString, Eq, Hash, PartialEq, Serialize, +)] pub enum ProtocolVersion { /// The original Witnet protocol. // TODO: update this default once 2.0 is completely active @@ -76,18 +78,6 @@ impl ProtocolVersion { } } -impl fmt::Display for ProtocolVersion { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let s = match self { - ProtocolVersion::V1_7 => "v1.7 (legacy)", - ProtocolVersion::V1_8 => "v1.8 (transitional)", - ProtocolVersion::V2_0 => "v2.0 (final)", - }; - - f.write_str(s) - } -} - pub trait Versioned: ProtobufConvert { type LegacyType: protobuf::Message; @@ -110,7 +100,7 @@ pub trait Versioned: ProtobufConvert { where ::ProtoStruct: protobuf::Message, { - Ok(self.to_versioned_pb(version)?.write_to_bytes().unwrap()) + Ok(self.to_versioned_pb(version)?.write_to_bytes()?) } /// Constructs an instance of this data structure based on a protobuf instance of its legacy @@ -256,6 +246,8 @@ where ::ProtoStruct: protobuf::Message, { fn versioned_hash(&self, version: ProtocolVersion) -> Hash { + // This unwrap is kept in here just because we want `VersionedHashable` to have the same interface as + // `Hashable`. witnet_crypto::hash::calculate_sha256(&self.to_versioned_pb_bytes(version).unwrap()).into() } } diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index 0c104b1d04..9fae1df67e 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -1971,9 +1971,9 @@ pub async fn stake(params: Result) -> JsonRpcResult { // If no authorization message is provided, generate a new one using the withdrawer address let authorization = if let Some(signature) = params.authorization { - // TODO: change to `try_do_magic` once `from_recoverable_hex` is made safe - let signature = - signature.do_magic(|hex_str| KeyedSignature::from_recoverable_hex(&hex_str, &msg)); + let signature = signature + .try_do_magic(|hex_str| KeyedSignature::from_recoverable_hex(&hex_str, &msg)) + .map_err(internal_error)?; validator = PublicKeyHash::from_public_key(&signature.public_key); log::debug!( "[STAKE] A stake authorization was provided, and it was signed by validator {}", @@ -2034,9 +2034,9 @@ pub async fn stake(params: Result) -> JsonRpcResult { withdrawer, }; - serde_json::to_value(bsr).map_err(|e| internal_error(e)) + serde_json::to_value(bsr).map_err(internal_error) } else { - serde_json::to_value(transaction).map_err(|e| internal_error(e)) + serde_json::to_value(transaction).map_err(internal_error) } } Ok(Err(e)) => { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index bf8ebd6630..2e1eedc448 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -6,6 +6,7 @@ use terminal_size as term; use env_logger::TimestampPrecision; use witnet_config as config; +use witnet_data_structures::register_protocol_version; mod node; mod wallet; @@ -56,6 +57,12 @@ pub fn exec( let _guard = init_logger(log_opts); witnet_data_structures::set_environment(config.environment); + for (version, epoch) in config.protocol.iter() { + if let Some(epoch) = epoch { + register_protocol_version(version, epoch); + } + } + exec_cmd(cmd, config_path, config) } diff --git a/src/cli/node/json_rpc_client.rs b/src/cli/node/json_rpc_client.rs index d74e65203a..e35ef9c3a5 100644 --- a/src/cli/node/json_rpc_client.rs +++ b/src/cli/node/json_rpc_client.rs @@ -862,6 +862,7 @@ pub fn send_dr( Ok(()) } +#[allow(clippy::too_many_arguments)] pub fn send_st( addr: SocketAddr, value: u64, @@ -1000,7 +1001,7 @@ pub fn authorize_st(addr: SocketAddr, withdrawer: Option) -> Result<(), let message = authorization.withdrawer.as_secp256k1_msg(); - let auth_bytes = authorization.signature.to_recoverable_bytes(&message); + let auth_bytes = authorization.signature.to_recoverable_bytes(&message)?; let auth_string = hex::encode(auth_bytes); let auth_qr = qrcode::QrCode::new(&auth_string)?; From 35bc88a8cd936501178265e4e08f0a30ff9c3426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Fri, 26 Jan 2024 14:37:26 +0100 Subject: [PATCH 19/32] feat(data_structures): enable multiple withdrawers for a single validator --- data_structures/benches/staking.rs | 8 +- data_structures/src/staking/aux.rs | 48 ++++- data_structures/src/staking/errors.rs | 10 +- data_structures/src/staking/simple.rs | 0 data_structures/src/staking/stakes.rs | 268 ++++++++++++++++---------- 5 files changed, 213 insertions(+), 121 deletions(-) delete mode 100644 data_structures/src/staking/simple.rs diff --git a/data_structures/benches/staking.rs b/data_structures/benches/staking.rs index 8bbee63f89..d2d96edc3a 100644 --- a/data_structures/benches/staking.rs +++ b/data_structures/benches/staking.rs @@ -12,7 +12,7 @@ fn populate(b: &mut Bencher) { let address = format!("{i}"); let coins = i; let epoch = i; - stakes.add_stake(address, coins, epoch).unwrap(); + stakes.add_stake(address.as_str(), coins, epoch).unwrap(); i += 1; }); @@ -32,7 +32,7 @@ fn rank(b: &mut Bencher) { let epoch = i; let address = format!("{}", rng.gen::()); - stakes.add_stake(address, coins, epoch).unwrap(); + stakes.add_stake(address.as_str(), coins, epoch).unwrap(); i += 1; @@ -62,7 +62,7 @@ fn query_power(b: &mut Bencher) { let epoch = i; let address = format!("{i}"); - stakes.add_stake(address, coins, epoch).unwrap(); + stakes.add_stake(address.as_str(), coins, epoch).unwrap(); i += 1; @@ -75,7 +75,7 @@ fn query_power(b: &mut Bencher) { b.iter(|| { let address = format!("{i}"); - let _power = stakes.query_power(&address, Capability::Mining, i); + let _power = stakes.query_power(address.as_str(), Capability::Mining, i); i += 1; }) diff --git a/data_structures/src/staking/aux.rs b/data_structures/src/staking/aux.rs index 4241581648..65543d05d0 100644 --- a/data_structures/src/staking/aux.rs +++ b/data_structures/src/staking/aux.rs @@ -1,5 +1,4 @@ -use std::rc::Rc; -use std::sync::RwLock; +use std::{rc::Rc, str::FromStr, sync::RwLock}; use super::prelude::*; @@ -10,14 +9,49 @@ pub type SyncStake = Rc = 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.. +/// Couples a validator address with a withdrawer address together. This is meant to be used in `Stakes` as the index +/// for the `by_key` index. +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct StakeKey
{ + /// A validator address. + pub validator: Address, + /// A withdrawer address. + pub withdrawer: Address, +} + +impl From<(T, T)> for StakeKey
+where + T: Into
, +{ + fn from(val: (T, T)) -> Self { + StakeKey { + validator: val.0.into(), + withdrawer: val.1.into(), + } + } +} + +impl
From<&str> for StakeKey
+where + Address: FromStr, +
::Err: std::fmt::Debug, +{ + fn from(val: &str) -> Self { + StakeKey { + validator: Address::from_str(val).unwrap(), + withdrawer: Address::from_str(val).unwrap(), + } + } +} + +/// Couples an amount of coins, a validator address and a withdrawer address together. This is meant to be used in +/// `Stakes` as the index of the `by_coins` index. #[derive(Eq, Ord, PartialEq, PartialOrd)] -pub struct CoinsAndAddress { +pub struct CoinsAndAddresses { /// An amount of coins. pub coins: Coins, - /// The address of a staker. - pub address: Address, + /// A validator and withdrawer addresses pair. + pub addresses: StakeKey
, } /// Allows telling the `census` method in `Stakes` to source addresses from its internal `by_coins` diff --git a/data_structures/src/staking/errors.rs b/data_structures/src/staking/errors.rs index 6169073f47..0c4bc69a39 100644 --- a/data_structures/src/staking/errors.rs +++ b/data_structures/src/staking/errors.rs @@ -1,5 +1,7 @@ use std::sync::PoisonError; +use crate::staking::aux::StakeKey; + /// All errors related to the staking functionality. #[derive(Debug, PartialEq)] pub enum StakesError { @@ -25,10 +27,10 @@ pub enum StakesError { /// 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 query for a stake entry that is not registered in `Stakes`. + EntryNotFound { + /// A validator and withdrawer address pair. + key: StakeKey
, }, /// Tried to obtain a lock on a write-locked piece of data that is already locked. PoisonedLock, diff --git a/data_structures/src/staking/simple.rs b/data_structures/src/staking/simple.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/data_structures/src/staking/stakes.rs b/data_structures/src/staking/stakes.rs index 13d0896d2b..29d5e72757 100644 --- a/data_structures/src/staking/stakes.rs +++ b/data_structures/src/staking/stakes.rs @@ -16,12 +16,12 @@ where Epoch: Default, { /// A listing of all the stakers, indexed by their address. - by_address: BTreeMap>, + by_key: BTreeMap, SyncStake>, /// 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>, + by_coins: BTreeMap, SyncStake>, /// The amount of coins that can be staked or can be left staked after unstaking. minimum_stakeable: Option, } @@ -49,17 +49,19 @@ where + 'static, { /// Register a certain amount of additional stake for a certain address and epoch. - pub fn add_stake( + pub fn add_stake( &mut self, - address: IA, + key: ISK, coins: Coins, epoch: Epoch, ) -> Result, Address, Coins, Epoch> where - IA: Into
, + ISK: Into>, { - let address = address.into(); - let stake_arc = self.by_address.entry(address.clone()).or_default(); + let key = key.into(); + + // Find or create a matching stake entry + let stake_arc = self.by_key.entry(key.clone()).or_default(); // Actually increase the number of coins stake_arc @@ -68,9 +70,9 @@ where // 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 { + let key = CoinsAndAddresses { coins, - address: address.clone(), + addresses: key, }; self.by_coins.remove(&key); self.by_coins.insert(key, stake_arc.clone()); @@ -92,7 +94,7 @@ where capability: Capability, epoch: Epoch, strategy: CensusStrategy, - ) -> Box> { + ) -> Box> + '_> { let iterator = self.rank(capability, epoch).map(|(address, _)| address); match strategy { @@ -109,18 +111,21 @@ where } /// Tells what is the power of an identity in the network on a certain epoch. - pub fn query_power( + pub fn query_power( &self, - address: &Address, + key: ISK, capability: Capability, epoch: Epoch, - ) -> Result { + ) -> Result + where + ISK: Into>, + { + let key = key.into(); + Ok(self - .by_address - .get(address) - .ok_or(StakesError::IdentityNotFound { - identity: address.clone(), - })? + .by_key + .get(&key) + .ok_or(StakesError::EntryNotFound { key })? .read()? .power(capability, epoch)) } @@ -131,29 +136,30 @@ where &self, capability: Capability, current_epoch: Epoch, - ) -> impl Iterator + 'static { + ) -> impl Iterator, Power)> + '_ { self.by_coins .iter() - .flat_map(move |(CoinsAndAddress { address, .. }, stake)| { + .flat_map(move |(CoinsAndAddresses { addresses, .. }, stake)| { stake .read() - .map(move |stake| (address.clone(), stake.power(capability, current_epoch))) + .map(move |stake| (addresses.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( + pub fn remove_stake( &mut self, - address: IA, + key: ISK, coins: Coins, ) -> Result where - IA: Into
, + ISK: Into>, { - let address = address.into(); - if let Entry::Occupied(mut by_address_entry) = self.by_address.entry(address.clone()) { + let key = key.into(); + + if let Entry::Occupied(mut by_address_entry) = self.by_key.entry(key.clone()) { let (initial_coins, final_coins) = { let mut stake = by_address_entry.get_mut().write()?; @@ -169,34 +175,35 @@ where // 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 { + self.by_coins.remove(&CoinsAndAddresses { coins: initial_coins, - address, + addresses: key, }); } Ok(final_coins) } else { - Err(StakesError::IdentityNotFound { identity: address }) + Err(StakesError::EntryNotFound { key }) } } /// Set the epoch for a certain address and capability. Most normally, the epoch is the current /// epoch. - pub fn reset_age( + pub fn reset_age( &mut self, - address: IA, + key: ISK, capability: Capability, current_epoch: Epoch, ) -> Result<(), Address, Coins, Epoch> where - IA: Into
, + ISK: Into>, { - let address = address.into(); + let key = key.into(); + let mut stake = self - .by_address - .get_mut(&address) - .ok_or(StakesError::IdentityNotFound { identity: address })? + .by_key + .get_mut(&key) + .ok_or(StakesError::EntryNotFound { key })? .write()?; stake.epochs.update(capability, current_epoch); @@ -226,26 +233,37 @@ mod tests { #[test] fn test_add_stake() { let mut stakes = Stakes::::with_minimum(5); - let alice = "Alice".into(); - let bob = "Bob".into(); + let alice = "Alice"; + let bob = "Bob"; + let charlie = "Charlie"; + let david = "David"; + + let alice_charlie = (alice, charlie); + let bob_david = (bob, david); // Let's check default power assert_eq!( - stakes.query_power(&alice, Capability::Mining, 0), - Err(StakesError::IdentityNotFound { - identity: alice.clone() + stakes.query_power(alice_charlie, Capability::Mining, 0), + Err(StakesError::EntryNotFound { + key: StakeKey { + validator: alice.into(), + withdrawer: charlie.into() + }, }) ); assert_eq!( - stakes.query_power(&alice, Capability::Mining, 1_000), - Err(StakesError::IdentityNotFound { - identity: alice.clone() + stakes.query_power(alice_charlie, Capability::Mining, 1_000), + Err(StakesError::EntryNotFound { + key: StakeKey { + validator: alice.into(), + withdrawer: charlie.into() + }, }) ); // Let's make Alice stake 100 Wit at epoch 100 assert_eq!( - stakes.add_stake(&alice, 100, 100).unwrap(), + stakes.add_stake(alice_charlie, 100, 100).unwrap(), Stake::from_parts( 100, CapabilityMap { @@ -256,17 +274,26 @@ mod tests { ); // 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), + stakes.query_power(alice_charlie, Capability::Mining, 99), + Ok(0) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 100), + Ok(0) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 101), + Ok(100) + ); + assert_eq!( + stakes.query_power(alice_charlie, 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(), + stakes.add_stake(alice_charlie, 50, 300).unwrap(), Stake::from_parts( 150, CapabilityMap { @@ -276,25 +303,25 @@ mod tests { ) ); assert_eq!( - stakes.query_power(&alice, Capability::Mining, 299), + stakes.query_power(alice_charlie, Capability::Mining, 299), Ok(19_950) ); assert_eq!( - stakes.query_power(&alice, Capability::Mining, 300), + stakes.query_power(alice_charlie, Capability::Mining, 300), Ok(20_100) ); assert_eq!( - stakes.query_power(&alice, Capability::Mining, 301), + stakes.query_power(alice_charlie, Capability::Mining, 301), Ok(20_250) ); assert_eq!( - stakes.query_power(&alice, Capability::Mining, 400), + stakes.query_power(alice_charlie, 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(), + stakes.add_stake(bob_david, 500, 1_000).unwrap(), Stake::from_parts( 500, CapabilityMap { @@ -306,32 +333,41 @@ mod tests { // Before Bob stakes, Alice has all the power assert_eq!( - stakes.query_power(&alice, Capability::Mining, 999), + stakes.query_power(alice_charlie, Capability::Mining, 999), Ok(124950) ); - assert_eq!(stakes.query_power(&bob, Capability::Mining, 999), Ok(0)); + assert_eq!( + stakes.query_power(bob_david, 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), + stakes.query_power(alice_charlie, Capability::Mining, 1_000), Ok(125100) ); - assert_eq!(stakes.query_power(&bob, Capability::Mining, 1_000), Ok(0)); + assert_eq!( + stakes.query_power(bob_david, 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), + stakes.query_power(alice_charlie, Capability::Mining, 1_001), Ok(125250) ); - assert_eq!(stakes.query_power(&bob, Capability::Mining, 1_001), Ok(500)); + assert_eq!( + stakes.query_power(bob_david, Capability::Mining, 1_001), + Ok(500) + ); // After enough time, Bob overpowers Alice assert_eq!( - stakes.query_power(&alice, Capability::Mining, 2_000), + stakes.query_power(alice_charlie, Capability::Mining, 2_000), Ok(275_100) ); assert_eq!( - stakes.query_power(&bob, Capability::Mining, 2_000), + stakes.query_power(bob_david, Capability::Mining, 2_000), Ok(500_000) ); } @@ -340,126 +376,146 @@ mod tests { 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(); + let alice = "Alice"; + let bob = "Bob"; + let charlie = "Charlie"; + let david = "David"; + let erin = "Erin"; + + let alice_charlie = (alice, charlie); + let bob_david = (bob, david); + let charlie_erin = (charlie, erin); - stakes.add_stake(&alice, 10, 0).unwrap(); - stakes.add_stake(&bob, 20, 20).unwrap(); - stakes.add_stake(&charlie, 30, 30).unwrap(); + stakes.add_stake(alice_charlie, 10, 0).unwrap(); + stakes.add_stake(bob_david, 20, 20).unwrap(); + stakes.add_stake(charlie_erin, 30, 30).unwrap(); // Let's really start our test at epoch 100 assert_eq!( - stakes.query_power(&alice, Capability::Mining, 100), + stakes.query_power(alice_charlie, 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), + stakes.query_power(bob_david, Capability::Mining, 100), + Ok(1_600) + ); + assert_eq!( + stakes.query_power(charlie_erin, Capability::Mining, 100), Ok(2_100) ); assert_eq!( - stakes.query_power(&alice, Capability::Witnessing, 100), + stakes.query_power(alice_charlie, Capability::Witnessing, 100), Ok(1_000) ); assert_eq!( - stakes.query_power(&bob, Capability::Witnessing, 100), + stakes.query_power(bob_david, Capability::Witnessing, 100), Ok(1_600) ); assert_eq!( - stakes.query_power(&charlie, Capability::Witnessing, 100), + stakes.query_power(charlie_erin, Capability::Witnessing, 100), Ok(2_100) ); assert_eq!( stakes.rank(Capability::Mining, 100).collect::>(), [ - (charlie.clone(), 2100), - (bob.clone(), 1600), - (alice.clone(), 1000) + (charlie_erin.into(), 2100), + (bob_david.into(), 1600), + (alice_charlie.into(), 1000) ] ); assert_eq!( stakes.rank(Capability::Witnessing, 100).collect::>(), [ - (charlie.clone(), 2100), - (bob.clone(), 1600), - (alice.clone(), 1000) + (charlie_erin.into(), 2100), + (bob_david.into(), 1600), + (alice_charlie.into(), 1000) ] ); // Now let's slash Charlie's mining coin age right after - stakes.reset_age(&charlie, Capability::Mining, 101).unwrap(); + stakes + .reset_age(charlie_erin, Capability::Mining, 101) + .unwrap(); assert_eq!( - stakes.query_power(&alice, Capability::Mining, 101), + stakes.query_power(alice_charlie, 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), + stakes.query_power(bob_david, Capability::Mining, 101), + Ok(1_620) + ); + assert_eq!( + stakes.query_power(charlie_erin, Capability::Mining, 101), + Ok(0) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Witnessing, 101), Ok(1_010) ); assert_eq!( - stakes.query_power(&bob, Capability::Witnessing, 101), + stakes.query_power(bob_david, Capability::Witnessing, 101), Ok(1_620) ); assert_eq!( - stakes.query_power(&charlie, Capability::Witnessing, 101), + stakes.query_power(charlie_erin, 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) + (bob_david.into(), 1_620), + (alice_charlie.into(), 1_010), + (charlie_erin.into(), 0) ] ); assert_eq!( stakes.rank(Capability::Witnessing, 101).collect::>(), [ - (charlie.clone(), 2_130), - (bob.clone(), 1_620), - (alice.clone(), 1_010) + (charlie_erin.into(), 2_130), + (bob_david.into(), 1_620), + (alice_charlie.into(), 1_010) ] ); // Don't panic, Charlie! After enough time, you can take over again ;) assert_eq!( - stakes.query_power(&alice, Capability::Mining, 300), + stakes.query_power(alice_charlie, 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), + stakes.query_power(bob_david, Capability::Mining, 300), + Ok(5_600) + ); + assert_eq!( + stakes.query_power(charlie_erin, Capability::Mining, 300), Ok(5_970) ); assert_eq!( - stakes.query_power(&alice, Capability::Witnessing, 300), + stakes.query_power(alice_charlie, Capability::Witnessing, 300), Ok(3_000) ); assert_eq!( - stakes.query_power(&bob, Capability::Witnessing, 300), + stakes.query_power(bob_david, Capability::Witnessing, 300), Ok(5_600) ); assert_eq!( - stakes.query_power(&charlie, Capability::Witnessing, 300), + stakes.query_power(charlie_erin, 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) + (charlie_erin.into(), 5_970), + (bob_david.into(), 5_600), + (alice_charlie.into(), 3_000) ] ); assert_eq!( stakes.rank(Capability::Witnessing, 300).collect::>(), [ - (charlie.clone(), 8_100), - (bob.clone(), 5_600), - (alice.clone(), 3_000) + (charlie_erin.into(), 8_100), + (bob_david.into(), 5_600), + (alice_charlie.into(), 3_000) ] ); } From 30bf1409a3d2e65c79faace9dc0d8654f90aa831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Fri, 2 Feb 2024 12:33:50 +0100 Subject: [PATCH 20/32] feat(node): make mining aware of protocol versions --- node/src/actors/chain_manager/mining.rs | 33 ++++++++++++++++++------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 1263987b5e..4235664a84 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -4,8 +4,8 @@ use std::{ future, future::Future, sync::{ - atomic::{self, AtomicU16}, Arc, + atomic::{self, AtomicU16}, }, }; @@ -14,13 +14,14 @@ use actix::{ WrapFuture, }; use ansi_term::Color::{White, Yellow}; -use futures::future::{try_join_all, FutureExt}; +use futures::future::{FutureExt, try_join_all}; + use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE; use witnet_data_structures::{ chain::{ - tapi::{after_second_hard_fork, ActiveWips}, - Block, BlockHeader, BlockMerkleRoots, BlockTransactions, Bn256PublicKey, CheckpointBeacon, - CheckpointVRF, DataRequestOutput, EpochConstants, Hash, Hashable, Input, PublicKeyHash, + Block, + BlockHeader, BlockMerkleRoots, BlockTransactions, Bn256PublicKey, CheckpointBeacon, CheckpointVRF, + DataRequestOutput, EpochConstants, Hash, Hashable, Input, PublicKeyHash, tapi::{ActiveWips, after_second_hard_fork}, TransactionsPool, ValueTransferOutput, }, data_request::{ @@ -28,7 +29,8 @@ use witnet_data_structures::{ DataRequestPool, }, error::TransactionError, - get_environment, + get_environment, get_protocol_version, + proto::versioning::ProtocolVersion, radon_error::RadonError, radon_report::{RadonReport, ReportContext, TypeLike}, transaction::{ @@ -44,7 +46,7 @@ use witnet_futures_utils::TryFutureExt2; use witnet_rad::{ conditions::radon_report_from_error, error::RadError, - types::{serial_iter_decode, RadonTypes}, + types::{RadonTypes, serial_iter_decode}, }; use witnet_util::timestamp::get_timestamp; use witnet_validations::validations::{ @@ -1004,8 +1006,21 @@ 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 unstake_hash_merkle_root = merkle_tree_root(&unstake_txns); + + let protocol = get_protocol_version(Some(beacon.checkpoint)); + + let stake_hash_merkle_root = if protocol == ProtocolVersion::V1_7 { + Hash::default() + } else { + merkle_tree_root(&stake_txns) + }; + + let unstake_hash_merkle_root = if protocol == ProtocolVersion::V1_7 { + Hash::default() + } else { + merkle_tree_root(&unstake_txns) + }; + let merkle_roots = BlockMerkleRoots { mint_hash: mint.hash(), vt_hash_merkle_root, From 6da1773f5df7ca132671bebd1eca48d500b1f510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Fri, 2 Feb 2024 12:36:26 +0100 Subject: [PATCH 21/32] feat(node): ask for confirmation when creating staking transactions also: fix #2429 --- node/src/actors/chain_manager/mining.rs | 12 +-- src/cli/node/json_rpc_client.rs | 99 ++++++++++++++++--------- 2 files changed, 70 insertions(+), 41 deletions(-) diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 4235664a84..69560baeeb 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -4,8 +4,8 @@ use std::{ future, future::Future, sync::{ - Arc, atomic::{self, AtomicU16}, + Arc, }, }; @@ -14,14 +14,14 @@ use actix::{ WrapFuture, }; use ansi_term::Color::{White, Yellow}; -use futures::future::{FutureExt, try_join_all}; +use futures::future::{try_join_all, FutureExt}; use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE; use witnet_data_structures::{ chain::{ - Block, - BlockHeader, BlockMerkleRoots, BlockTransactions, Bn256PublicKey, CheckpointBeacon, CheckpointVRF, - DataRequestOutput, EpochConstants, Hash, Hashable, Input, PublicKeyHash, tapi::{ActiveWips, after_second_hard_fork}, + tapi::{after_second_hard_fork, ActiveWips}, + Block, BlockHeader, BlockMerkleRoots, BlockTransactions, Bn256PublicKey, CheckpointBeacon, + CheckpointVRF, DataRequestOutput, EpochConstants, Hash, Hashable, Input, PublicKeyHash, TransactionsPool, ValueTransferOutput, }, data_request::{ @@ -46,7 +46,7 @@ use witnet_futures_utils::TryFutureExt2; use witnet_rad::{ conditions::radon_report_from_error, error::RadError, - types::{RadonTypes, serial_iter_decode}, + types::{serial_iter_decode, RadonTypes}, }; use witnet_util::timestamp::get_timestamp; use witnet_validations::validations::{ diff --git a/src/cli/node/json_rpc_client.rs b/src/cli/node/json_rpc_client.rs index e35ef9c3a5..b022a14b4e 100644 --- a/src/cli/node/json_rpc_client.rs +++ b/src/cli/node/json_rpc_client.rs @@ -1,3 +1,14 @@ +use std::{ + collections::{BTreeSet, HashMap, HashSet}, + convert::TryFrom, + fmt, + fs::File, + io::{self, BufRead, BufReader, Read, Write}, + net::{SocketAddr, TcpStream}, + path::Path, + str::FromStr, +}; + use ansi_term::Color::{Purple, Red, White, Yellow}; use failure::{bail, Fail}; use itertools::Itertools; @@ -28,13 +39,12 @@ use witnet_data_structures::{ utxo_pool::{UtxoInfo, UtxoSelectionStrategy}, wit::Wit, }; -use witnet_node::actors::messages::{BuildStakeResponse, MagicEither}; use witnet_node::actors::{ chain_manager::run_dr_locally, json_rpc::api::{AddrType, GetBlockChainParams, GetTransactionOutput, PeersResult}, messages::{ - AuthorizeStake, BuildDrt, BuildStakeParams, BuildVtt, GetBalanceTarget, - GetReputationResult, SignalingInfo, StakeAuthorization, + AuthorizeStake, BuildDrt, BuildStakeParams, BuildStakeResponse, BuildVtt, GetBalanceTarget, + GetReputationResult, MagicEither, SignalingInfo, StakeAuthorization, }, }; use witnet_rad::types::RadonTypes; @@ -43,17 +53,6 @@ use witnet_validations::validations::{ run_tally_panic_safe, validate_data_request_output, validate_rad_request, }; -use std::{ - collections::{BTreeSet, HashMap}, - convert::TryFrom, - fmt, - fs::File, - io::{self, BufRead, BufReader, Read, Write}, - net::{SocketAddr, TcpStream}, - path::Path, - str::FromStr, -}; - pub fn raw(addr: SocketAddr) -> Result<(), failure::Error> { let mut stream = start_client(addr)?; // The request is read from stdin, one line at a time @@ -943,9 +942,9 @@ pub fn send_st( dry_run: true, ..build_stake_params.clone() }; - let (dry_st, ..): (StakeTransaction, _) = + let (bsr, ..): (BuildStakeResponse, _) = issue_method("stake", Some(dry_params), &mut stream, id.next())?; - let dry_weight = dry_st.weight(); + let dry_weight = bsr.transaction.weight(); // We retry up to 5 times, or until the weight is stable if rounds > 5 || dry_weight == weight { @@ -963,29 +962,49 @@ pub fn send_st( build_stake_params.fee = prompt_user_for_priority_selection(estimates)?; } - if requires_confirmation.unwrap_or(true) { + let confirmation = if requires_confirmation.unwrap_or(true) { let params = BuildStakeParams { dry_run: true, ..build_stake_params.clone() }; - let (bsr, _): (BuildStakeResponse, _) = + let (dry, _): (BuildStakeResponse, _) = issue_method("stake", Some(params), &mut stream, id.next())?; - prompt_user_for_stake_confirmation(bsr)?; - } + // Exactly what it says: shows all the facts about the staking transaction, and expects confirmation through + // user input + if prompt_user_for_stake_confirmation(&dry)? { + Some(dry) + } else { + None + } + } else { + None + }; - // Finally ask the node to create the transaction with the chosen fee. - build_stake_params.dry_run = dry_run; - let (_st, (request, response)): (StakeTransaction, _) = - issue_method("stake", Some(build_stake_params), &mut stream, id.next())?; + if let Some(dry) = confirmation { + // Finally ask the node to create the transaction with the chosen fee. + build_stake_params.dry_run = dry_run; + let (st, (request, response)): (StakeTransaction, _) = + issue_method("stake", Some(build_stake_params), &mut stream, id.next())?; - // On dry run mode, print the request, otherwise, print the response. - // This is kept like this strictly for backwards compatibility. - // TODO: wouldn't it be better to always print the response or both? - if dry_run { - println!("{}", request); + println!("> {}", request); + println!("< {}", response); + + let environment = get_environment(); + let value = Wit::from_nanowits(st.body.output.value).to_string(); + let staker = dry + .staker + .iter() + .map(|pkh| pkh.bech32(environment)) + .collect::>() + .iter() + .join(","); + let validator = dry.validator.bech32(environment); + let withdrawer = dry.withdrawer.bech32(environment); + + println!("Congratulations! {} Wit have been staked by addresses {:?} onto validator {}, using {} as the withdrawal address.", value, staker, validator, withdrawer); } else { - println!("{}", response); + println!("The stake facts have not been confirmed. No stake transaction has been created."); } Ok(()) @@ -2064,7 +2083,7 @@ fn prompt_user_for_priority_selection( Ok(fee) } -fn prompt_user_for_stake_confirmation(data: BuildStakeResponse) -> Result<(), failure::Error> { +fn prompt_user_for_stake_confirmation(data: &BuildStakeResponse) -> Result { let environment = get_environment(); let value = Wit::from_nanowits(data.transaction.body.output.value).to_string(); @@ -2125,11 +2144,21 @@ fn prompt_user_for_stake_confirmation(data: BuildStakeResponse) -> Result<(), fa let mut input = String::new(); let stdin = io::stdin(); let mut stdin = stdin.lock(); - io::stdout().flush()?; - input.clear(); - stdin.read_line(&mut input)?; + loop { + print!("Please double-check the information above and confirm if it is correct (y/N): ",); + io::stdout().flush()?; + input.clear(); + stdin.read_line(&mut input)?; + let selected = input.trim().to_uppercase(); - Ok(()) + if ["Y", "YES"].contains(&selected.as_str()) { + return Ok(true); + } else if ["", "N", "NO"].contains(&selected.as_str()) { + break; + } + } + + Ok(false) } #[cfg(test)] From f894632c7e5d511cdbbc54650f989b518b59f7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Fri, 2 Feb 2024 12:38:57 +0100 Subject: [PATCH 22/32] feat(validations): make block signature validations aware of protocol versions --- validations/src/validations.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/validations/src/validations.rs b/validations/src/validations.rs index a566a69346..01721f807c 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -30,7 +30,7 @@ use witnet_data_structures::{ }, error::{BlockError, DataRequestError, TransactionError}, get_protocol_version, - proto::versioning::ProtocolVersion, + proto::versioning::{ProtocolVersion, VersionedHashable}, radon_report::{RadonReport, ReportContext}, transaction::{ CommitTransaction, DRTransaction, MintTransaction, RevealTransaction, StakeTransaction, @@ -1320,9 +1320,9 @@ 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(); + let Hash::SHA256(message) = block.versioned_hash(get_protocol_version(Some( + block.block_header.beacon.checkpoint, + ))); add_secp_block_signature_to_verify(signatures_to_verify, &public_key, &message, &signature); From 88f04aeac50991297cbdab49f55ea3eb43f23add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Mon, 5 Feb 2024 17:00:29 +0100 Subject: [PATCH 23/32] fix(validations): solve synchronization and bootstrapping issues fix #2428 --- data_structures/src/chain/mod.rs | 5 ++++- validations/src/validations.rs | 28 ++++++++++++---------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index 8ce6b34489..f37d06aa6c 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -1114,7 +1114,10 @@ pub enum Hash { impl Default for Hash { fn default() -> Hash { - Hash::SHA256([0; 32]) + Hash::SHA256([ + 227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200, 153, 111, 185, 36, 39, 174, 65, 228, + 100, 155, 147, 76, 164, 149, 153, 27, 120, 82, 184, 85, + ]) } } diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 01721f807c..7d99da6fd4 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -11,18 +11,14 @@ use witnet_config::defaults::{ PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO, PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE, }; -use witnet_crypto::{ - hash::{calculate_sha256, Sha256}, - merkle::{merkle_tree_root as crypto_merkle_tree_root, ProgressiveMerkleTree}, - signature::{verify, PublicKey, Signature}, -}; +use witnet_crypto::{hash::{calculate_sha256, Sha256}, hash, merkle::{merkle_tree_root as crypto_merkle_tree_root, ProgressiveMerkleTree}, signature::{PublicKey, Signature, verify}}; 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, - StakeOutput, ValueTransferOutput, + Block, BlockMerkleRoots, CheckpointBeacon, CheckpointVRF, ConsensusConstants, + DataRequestOutput, DataRequestStage, DataRequestState, Epoch, EpochConstants, + Hash, Hashable, Input, KeyedSignature, OutputPointer, PublicKeyHash, RADRequest, + RADTally, RADType, Reputation, ReputationEngine, SignaturesToVerify, StakeOutput, + tapi::ActiveWips, ValueTransferOutput, }, data_request::{ calculate_reward_collateral_ratio, calculate_tally_change, calculate_witness_reward, @@ -50,7 +46,7 @@ use witnet_rad::{ error::RadError, operators::RadonOpCodes, script::{create_radon_script_from_filters_and_reducer, unpack_radon_script}, - types::{serial_iter_decode, RadonTypes}, + types::{RadonTypes, serial_iter_decode}, }; // TODO: move to a configuration @@ -1988,10 +1984,10 @@ pub fn validate_block_transactions( // } } - (Hash::from(st_mt.root()), Hash::from(ut_mt.root())) + (st_mt.root(), ut_mt.root()) } else { // Nullify stake and unstake merkle roots for the legacy protocol version - Default::default() + (hash::EMPTY_SHA256, hash::EMPTY_SHA256) }; // Validate Merkle Root @@ -2002,12 +1998,12 @@ 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: st_root, - unstake_hash_merkle_root: ut_root, + stake_hash_merkle_root: Hash::from(st_root), + unstake_hash_merkle_root: Hash::from(ut_root), }; if merkle_roots != block.block_header.merkle_roots { - println!( + log::debug!( "{:?} vs {:?}", merkle_roots, block.block_header.merkle_roots ); From 7c749f86b062179b257d8e79e2f498042ade6653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Fri, 9 Feb 2024 11:38:25 +0100 Subject: [PATCH 24/32] fix(wallet): fix keypair decoding from storage --- data_structures/src/chain/mod.rs | 4 +-- validations/src/validations.rs | 22 +++++++----- wallet/src/actors/worker/methods.rs | 7 ++++ wallet/src/db/encrypted/engine.rs | 3 +- wallet/src/db/encrypted/mod.rs | 23 ++++++++++++ wallet/src/db/mod.rs | 20 +++++++++++ wallet/src/db/tests.rs | 21 +++++++++++ wallet/src/repository/wallet/mod.rs | 55 ++++++++++++++++++++++++++--- wallet/src/types.rs | 1 + 9 files changed, 140 insertions(+), 16 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index f37d06aa6c..c8250f45c4 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -1115,8 +1115,8 @@ pub enum Hash { impl Default for Hash { fn default() -> Hash { Hash::SHA256([ - 227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200, 153, 111, 185, 36, 39, 174, 65, 228, - 100, 155, 147, 76, 164, 149, 153, 27, 120, 82, 184, 85, + 227, 176, 196, 66, 152, 252, 28, 20, 154, 251, 244, 200, 153, 111, 185, 36, 39, 174, + 65, 228, 100, 155, 147, 76, 164, 149, 153, 27, 120, 82, 184, 85, ]) } } diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 7d99da6fd4..3a8b5247a1 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -11,14 +11,19 @@ use witnet_config::defaults::{ PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO, PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE, }; -use witnet_crypto::{hash::{calculate_sha256, Sha256}, hash, merkle::{merkle_tree_root as crypto_merkle_tree_root, ProgressiveMerkleTree}, signature::{PublicKey, Signature, verify}}; +use witnet_crypto::{ + hash, + hash::{calculate_sha256, Sha256}, + merkle::{merkle_tree_root as crypto_merkle_tree_root, ProgressiveMerkleTree}, + signature::{verify, PublicKey, Signature}, +}; use witnet_data_structures::{ chain::{ - Block, BlockMerkleRoots, CheckpointBeacon, CheckpointVRF, ConsensusConstants, - DataRequestOutput, DataRequestStage, DataRequestState, Epoch, EpochConstants, - Hash, Hashable, Input, KeyedSignature, OutputPointer, PublicKeyHash, RADRequest, - RADTally, RADType, Reputation, ReputationEngine, SignaturesToVerify, StakeOutput, - tapi::ActiveWips, ValueTransferOutput, + tapi::ActiveWips, Block, BlockMerkleRoots, CheckpointBeacon, CheckpointVRF, + ConsensusConstants, DataRequestOutput, DataRequestStage, DataRequestState, Epoch, + EpochConstants, Hash, Hashable, Input, KeyedSignature, OutputPointer, PublicKeyHash, + RADRequest, RADTally, RADType, Reputation, ReputationEngine, SignaturesToVerify, + StakeOutput, ValueTransferOutput, }, data_request::{ calculate_reward_collateral_ratio, calculate_tally_change, calculate_witness_reward, @@ -46,7 +51,7 @@ use witnet_rad::{ error::RadError, operators::RadonOpCodes, script::{create_radon_script_from_filters_and_reducer, unpack_radon_script}, - types::{RadonTypes, serial_iter_decode}, + types::{serial_iter_decode, RadonTypes}, }; // TODO: move to a configuration @@ -2005,7 +2010,8 @@ pub fn validate_block_transactions( if merkle_roots != block.block_header.merkle_roots { log::debug!( "{:?} vs {:?}", - merkle_roots, block.block_header.merkle_roots + merkle_roots, + block.block_header.merkle_roots ); Err(BlockError::NotValidMerkleTree.into()) } else { diff --git a/wallet/src/actors/worker/methods.rs b/wallet/src/actors/worker/methods.rs index 268daf6cfe..4904d775be 100644 --- a/wallet/src/actors/worker/methods.rs +++ b/wallet/src/actors/worker/methods.rs @@ -300,6 +300,7 @@ impl Worker { wallet_id: &str, password: &[u8], ) -> Result { + log::debug!("Unlocking wallet with ID {wallet_id}"); let (salt, iv) = self .wallets .wallet_salt_and_iv(wallet_id) @@ -308,7 +309,9 @@ impl Worker { | repository::Error::WalletNotFound => Error::WalletNotFound, err => Error::Repository(err), })?; + log::debug!("Found salt and IV for wallet with ID {wallet_id}. Deriving key now."); let key = crypto::key_from_password(password, &salt, self.params.db_hash_iterations); + log::debug!("Derived key for wallet with ID {wallet_id}. Generating session now."); let session_id: types::SessionId = From::from(crypto::gen_session_id( &mut self.rng, &self.params.id_hash_function, @@ -320,6 +323,7 @@ impl Worker { let wallet_db = db::EncryptedDb::new(self.db.clone(), prefix, key, iv); // Check if password-derived key is able to read the special stored value + log::debug!("Wallet {wallet_id} now has a session with ID {session_id}"); wallet_db .get(&constants::ENCRYPTION_CHECK_KEY) .map_err(|err| match err { @@ -327,13 +331,16 @@ impl Worker { err => Error::Db(err), })?; + log::debug!("Encryption key for wallet {wallet_id} seems to be valid. Decrypting wallet object now."); let wallet = Arc::new(repository::Wallet::unlock( wallet_id, session_id.clone(), wallet_db, self.params.clone(), )?); + log::debug!("Extracting public data for wallet {wallet_id}."); let data = wallet.public_data()?; + log::debug!("Wallet data: {:?}", data); Ok(types::UnlockedSessionWallet { wallet, diff --git a/wallet/src/db/encrypted/engine.rs b/wallet/src/db/encrypted/engine.rs index cf7fc46527..5982fd2773 100644 --- a/wallet/src/db/encrypted/engine.rs +++ b/wallet/src/db/encrypted/engine.rs @@ -1,6 +1,7 @@ -use super::*; use crate::types; +use super::*; + #[derive(Clone)] pub struct CryptoEngine { key: types::Secret, diff --git a/wallet/src/db/encrypted/mod.rs b/wallet/src/db/encrypted/mod.rs index 7309b428e2..35f060ae5a 100644 --- a/wallet/src/db/encrypted/mod.rs +++ b/wallet/src/db/encrypted/mod.rs @@ -1,3 +1,4 @@ +use serde::de::DeserializeOwned; use std::sync::Arc; use witnet_crypto::cipher; @@ -101,3 +102,25 @@ impl Database for EncryptedDb { EncryptedWriteBatch::new(self.prefixer.clone(), self.engine.clone()) } } + +impl GetWith for EncryptedDb { + fn get_with_opt(&self, key: &Key, with: F) -> Result> + where + K: AsRef<[u8]>, + V: DeserializeOwned, + F: Fn(&[u8]) -> Vec, + { + let prefix_key = self.prefixer.prefix(key); + let enc_key = self.engine.encrypt(&prefix_key)?; + let res = self.as_ref().get(enc_key)?; + + match res { + Some(dbvec) => { + let value = self.engine.decrypt_with(&dbvec, with)?; + + Ok(Some(value)) + } + None => Ok(None), + } + } +} diff --git a/wallet/src/db/mod.rs b/wallet/src/db/mod.rs index c29a7a3f2e..f5ac6370bb 100644 --- a/wallet/src/db/mod.rs +++ b/wallet/src/db/mod.rs @@ -71,3 +71,23 @@ pub trait WriteBatch { V: serde::Serialize + ?Sized, Vref: Borrow; } + +pub trait GetWith { + fn get_with(&self, key: &Key, with: F) -> Result + where + K: AsRef<[u8]> + Debug, + V: serde::de::DeserializeOwned, + F: Fn(&[u8]) -> Vec, + { + let opt = self.get_with_opt(key, with)?; + + opt.ok_or_else(|| Error::DbKeyNotFound { + key: format!("{:?}", key), + }) + } + fn get_with_opt(&self, key: &Key, with: F) -> Result> + where + K: AsRef<[u8]>, + V: serde::de::DeserializeOwned, + F: Fn(&[u8]) -> Vec; +} diff --git a/wallet/src/db/tests.rs b/wallet/src/db/tests.rs index 2d4275674c..98bfeb3eea 100644 --- a/wallet/src/db/tests.rs +++ b/wallet/src/db/tests.rs @@ -1,3 +1,4 @@ +use serde::de::DeserializeOwned; use std::{cell::RefCell, collections::HashMap, rc::Rc}; use super::*; @@ -137,6 +138,26 @@ impl IntoIterator for HashMapWriteBatch { } } +impl GetWith for HashMapDb { + fn get_with_opt(&self, key: &Key, with: F) -> Result> + where + K: AsRef<[u8]>, + V: DeserializeOwned, + F: Fn(&[u8]) -> Vec, + { + let k = key.as_ref().to_vec(); + let res = match RefCell::borrow(&self.rc).get(&k) { + Some(value) => { + let value = with(value); + Some(bincode::deserialize(&value)?) + } + None => None, + }; + + Ok(res) + } +} + #[test] fn test_hashmap_db() { let db = HashMapDb::default(); diff --git a/wallet/src/repository/wallet/mod.rs b/wallet/src/repository/wallet/mod.rs index 958feaea06..5fde718d05 100644 --- a/wallet/src/repository/wallet/mod.rs +++ b/wallet/src/repository/wallet/mod.rs @@ -13,7 +13,7 @@ use state::State; use witnet_crypto::{ hash::calculate_sha256, key::{ExtendedPK, ExtendedSK, KeyPath, PK}, - signature, + secp256k1, signature, }; use witnet_data_structures::{ chain::{ @@ -37,7 +37,7 @@ use witnet_util::timestamp::get_timestamp; use crate::{ constants, crypto, - db::{Database, WriteBatch as _}, + db::{Database, GetWith, WriteBatch as _}, model, params::Params, types, @@ -189,7 +189,7 @@ pub struct Wallet { impl Wallet where - T: Database, + T: Database + GetWith, { /// Generate transient addresses for synchronization purposes /// This function only creates and inserts addresses @@ -295,6 +295,11 @@ where let id = id.to_owned(); let name = db.get_opt(&keys::wallet_name())?; let description = db.get_opt(&keys::wallet_description())?; + log::debug!( + "Unlocking wallet with name '{}' and description '{}'", + name.clone().unwrap_or_default(), + description.clone().unwrap_or_default() + ); let account = db.get_or_default(&keys::wallet_default_account())?; let available_accounts = db .get_opt(&keys::wallet_accounts())? @@ -333,6 +338,7 @@ where unconfirmed: balance_info, confirmed: balance_info, }; + log::debug!("Wallet {id} has balance: {:?}", balance); let last_sync = db .get(&keys::wallet_last_sync()) @@ -342,17 +348,34 @@ where }); let last_confirmed = last_sync; + log::debug!( + "Wallet {id} has last_sync={:?} and last_confirmed={:?}", + last_sync, + last_confirmed + ); - let external_key = db.get(&keys::account_key(account, constants::EXTERNAL_KEYCHAIN))?; + let external_key = db.get_with( + &keys::account_key(account, constants::EXTERNAL_KEYCHAIN), + backwards_compatible_keypair_decoding, + )?; let next_external_index = db.get_or_default(&keys::account_next_index( account, constants::EXTERNAL_KEYCHAIN, ))?; - let internal_key = db.get(&keys::account_key(account, constants::INTERNAL_KEYCHAIN))?; + log::debug!( + "Loaded external keys for wallet {id}. Next external index is {next_external_index}." + ); + let internal_key = db.get_with( + &keys::account_key(account, constants::INTERNAL_KEYCHAIN), + backwards_compatible_keypair_decoding, + )?; let next_internal_index = db.get_or_default(&keys::account_next_index( account, constants::INTERNAL_KEYCHAIN, ))?; + log::debug!( + "Loaded internal keys for wallet {id}. Next internal index is {next_internal_index}." + ); let keychains = [external_key, internal_key]; let epoch_constants = params.epoch_constants; let birth_date = db.get(&keys::birth_date()).unwrap_or(CheckpointBeacon { @@ -2275,6 +2298,15 @@ fn vtt_to_outputs( .collect::>() } +#[inline] +fn backwards_compatible_keypair_decoding(bytes: &[u8]) -> Vec { + let skip = bytes + .len() + .saturating_sub(8 + 2 * secp256k1::constants::SECRET_KEY_SIZE); + + bytes[skip..].to_vec() +} + #[cfg(test)] impl Wallet where @@ -2361,3 +2393,16 @@ fn test_get_tx_ranges_exceed() { assert_eq!(pending_range, None); assert_eq!(db_range, Some(0..5)); } + +#[test] +fn test_backwards_compatible_keypair_decoding() { + // Test that the old keypair prefix is detected and omitted + let old = backwards_compatible_keypair_decoding(&hex::decode("20000000000000001837c1be8e2995ec11cda2b066151be2cfb48adf9e47b151d46adab3a21cdf6720000000000000007923408dadd3c7b56eed15567707ae5e5dca089de972e07f3b860450e2a3b70e").unwrap()); + let new = backwards_compatible_keypair_decoding(&hex::decode("1837c1be8e2995ec11cda2b066151be2cfb48adf9e47b151d46adab3a21cdf6720000000000000007923408dadd3c7b56eed15567707ae5e5dca089de972e07f3b860450e2a3b70e").unwrap()); + assert_eq!(old, new); + + // Test that shorter strings don't get clipped or panic + let short = hex::decode("fabadaacabadaa").unwrap(); + let compatible = backwards_compatible_keypair_decoding(&short); + assert_eq!(short, compatible); +} diff --git a/wallet/src/types.rs b/wallet/src/types.rs index 3f8e73a29b..4286895ee7 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -122,6 +122,7 @@ pub struct Account { pub internal: ExtendedSK, } +#[derive(Debug)] pub struct WalletData { pub id: String, pub name: Option, From c131f74fc8cd4e6c4d847255fe1f5bf428212d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Thu, 15 Feb 2024 13:11:56 +0100 Subject: [PATCH 25/32] feat(data_structures): incorporate stakes tracker into ChainState --- Cargo.lock | 22 +++++++-------- data_structures/src/capabilities.rs | 4 ++- data_structures/src/chain/mod.rs | 4 +++ data_structures/src/staking/aux.rs | 40 ++++++++++++++++++++++----- data_structures/src/staking/stake.rs | 8 ++++-- data_structures/src/staking/stakes.rs | 31 ++++++++++++--------- node/src/actors/chain_manager/mod.rs | 4 +++ wallet/src/db/encrypted/engine.rs | 12 ++++++++ wallet/src/db/encrypted/mod.rs | 4 ++- 9 files changed, 93 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ad887b104b..4ecf423003 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -422,9 +422,9 @@ checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" [[package]] name = "byteorder" @@ -741,9 +741,9 @@ dependencies = [ [[package]] name = "curl" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8e5123ab8c31200ce725939049ecd4a090b242608f24048131dedf9dd195aed" +checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6" dependencies = [ "curl-sys", "libc", @@ -3549,9 +3549,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "secp256k1" -version = "0.28.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f622567e3b4b38154fb8190bcf6b160d7a4301d70595a49195b48c116007a27" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ "secp256k1-sys", "serde", @@ -3948,10 +3948,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.60", - "quote 1.0.28", + "proc-macro2 1.0.78", + "quote 1.0.35", "rustversion", - "syn 2.0.18", + "syn 2.0.48", ] [[package]] @@ -5135,9 +5135,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.39" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5389a154b01683d28c77f8f68f49dea75f0a4da32557a58f68ee51ebba472d29" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" dependencies = [ "memchr", ] diff --git a/data_structures/src/capabilities.rs b/data_structures/src/capabilities.rs index 80cd8257b8..c3bee6efba 100644 --- a/data_structures/src/capabilities.rs +++ b/data_structures/src/capabilities.rs @@ -1,3 +1,5 @@ +use serde::{Deserialize, Serialize}; + #[repr(u8)] #[derive(Clone, Copy, Debug)] pub enum Capability { @@ -7,7 +9,7 @@ pub enum Capability { Witnessing = 1, } -#[derive(Copy, Clone, Debug, Default, PartialEq)] +#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct CapabilityMap where T: Default, diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index c8250f45c4..cad5d63628 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -43,6 +43,7 @@ use crate::{ versioning::{ProtocolVersion, Versioned}, ProtobufConvert, }, + staking::prelude::*, superblock::SuperBlockState, transaction::{ CommitTransaction, DRTransaction, DRTransactionBody, Memoized, MintTransaction, @@ -54,6 +55,7 @@ use crate::{ }, utxo_pool::{OldUnspentOutputsPool, OwnUnspentOutputsPool, UnspentOutputsPool}, vrf::{BlockEligibilityClaim, DataRequestEligibilityClaim}, + wit::Wit, }; /// Keeps track of priority being used by transactions included in recent blocks, and provides @@ -3776,6 +3778,8 @@ pub struct ChainState { /// Unspent Outputs Pool #[serde(skip)] pub unspent_outputs_pool: UnspentOutputsPool, + /// Tracks stakes for every validator in the network + pub stakes: Stakes, } impl ChainState { diff --git a/data_structures/src/staking/aux.rs b/data_structures/src/staking/aux.rs index 65543d05d0..3a6c706eac 100644 --- a/data_structures/src/staking/aux.rs +++ b/data_structures/src/staking/aux.rs @@ -1,17 +1,43 @@ use std::{rc::Rc, str::FromStr, sync::RwLock}; -use super::prelude::*; +use serde::{Deserialize, Serialize}; -/// Type alias for a reference-counted and read-write-locked instance of `Stake`. -pub type SyncStake = Rc>>; +use super::prelude::*; /// The resulting type for all the fallible functions in this module. -pub type Result = - std::result::Result>; +pub type StakingResult = Result>; + +/// Newtype for a reference-counted and read-write-locked instance of `Stake`. +/// +/// This newtype is needed for implementing `PartialEq` manually on the locked data, which cannot be done directly +/// because those are externally owned types. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct SyncStake +where + Address: Default, + Epoch: Default, +{ + /// The lock itself. + pub value: Rc>>, +} + +impl PartialEq for SyncStake +where + Address: Default, + Epoch: Default + PartialEq, + Coins: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + let self_stake = self.value.read().unwrap(); + let other_stake = other.value.read().unwrap(); + + self_stake.coins.eq(&other_stake.coins) && other_stake.epochs.eq(&other_stake.epochs) + } +} /// Couples a validator address with a withdrawer address together. This is meant to be used in `Stakes` as the index /// for the `by_key` index. -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub struct StakeKey
{ /// A validator address. pub validator: Address, @@ -46,7 +72,7 @@ where /// Couples an amount of coins, a validator address and a withdrawer address together. This is meant to be used in /// `Stakes` as the index of the `by_coins` index. -#[derive(Eq, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub struct CoinsAndAddresses { /// An amount of coins. pub coins: Coins, diff --git a/data_structures/src/staking/stake.rs b/data_structures/src/staking/stake.rs index 38fff6ae4c..4752c5e130 100644 --- a/data_structures/src/staking/stake.rs +++ b/data_structures/src/staking/stake.rs @@ -1,10 +1,12 @@ use std::marker::PhantomData; +use serde::{Deserialize, Serialize}; + 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)] +#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct Stake where Address: Default, @@ -48,7 +50,7 @@ where coins: Coins, epoch: Epoch, minimum_stakeable: Option, - ) -> Result { + ) -> StakingResult { // 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 { @@ -96,7 +98,7 @@ where &mut self, coins: Coins, minimum_stakeable: Option, - ) -> Result { + ) -> StakingResult { let coins_after = self.coins.sub(coins); if coins_after > Coins::zero() { diff --git a/data_structures/src/staking/stakes.rs b/data_structures/src/staking/stakes.rs index 29d5e72757..192bec52fe 100644 --- a/data_structures/src/staking/stakes.rs +++ b/data_structures/src/staking/stakes.rs @@ -1,7 +1,7 @@ -use std::collections::btree_map::Entry; -use std::collections::BTreeMap; +use std::collections::{btree_map::Entry, BTreeMap}; use itertools::Itertools; +use serde::{Deserialize, Serialize}; use super::prelude::*; @@ -9,10 +9,11 @@ use super::prelude::*; /// /// 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)] +#[derive(Clone, Debug, Deserialize, Default, PartialEq, Serialize)] pub struct Stakes where - Address: Default, + Address: Default + Ord, + Coins: Ord, Epoch: Default, { /// A listing of all the stakers, indexed by their address. @@ -54,17 +55,18 @@ where key: ISK, coins: Coins, epoch: Epoch, - ) -> Result, Address, Coins, Epoch> + ) -> StakingResult, Address, Coins, Epoch> where ISK: Into>, { let key = key.into(); // Find or create a matching stake entry - let stake_arc = self.by_key.entry(key.clone()).or_default(); + let stake = self.by_key.entry(key.clone()).or_default(); // Actually increase the number of coins - stake_arc + stake + .value .write()? .add_stake(coins, epoch, self.minimum_stakeable)?; @@ -75,9 +77,9 @@ where addresses: key, }; self.by_coins.remove(&key); - self.by_coins.insert(key, stake_arc.clone()); + self.by_coins.insert(key, stake.clone()); - Ok(stake_arc.read()?.clone()) + Ok(stake.value.read()?.clone()) } /// Obtain a list of stakers, conveniently ordered by one of several strategies. @@ -116,7 +118,7 @@ where key: ISK, capability: Capability, epoch: Epoch, - ) -> Result + ) -> StakingResult where ISK: Into>, { @@ -126,6 +128,7 @@ where .by_key .get(&key) .ok_or(StakesError::EntryNotFound { key })? + .value .read()? .power(capability, epoch)) } @@ -141,6 +144,7 @@ where .iter() .flat_map(move |(CoinsAndAddresses { addresses, .. }, stake)| { stake + .value .read() .map(move |stake| (addresses.clone(), stake.power(capability, current_epoch))) }) @@ -153,7 +157,7 @@ where &mut self, key: ISK, coins: Coins, - ) -> Result + ) -> StakingResult where ISK: Into>, { @@ -161,7 +165,7 @@ where if let Entry::Occupied(mut by_address_entry) = self.by_key.entry(key.clone()) { let (initial_coins, final_coins) = { - let mut stake = by_address_entry.get_mut().write()?; + let mut stake = by_address_entry.get_mut().value.write()?; // Check the former amount of stake let initial_coins = stake.coins; @@ -194,7 +198,7 @@ where key: ISK, capability: Capability, current_epoch: Epoch, - ) -> Result<(), Address, Coins, Epoch> + ) -> StakingResult<(), Address, Coins, Epoch> where ISK: Into>, { @@ -204,6 +208,7 @@ where .by_key .get_mut(&key) .ok_or(StakesError::EntryNotFound { key })? + .value .write()?; stake.epochs.update(capability, current_epoch); diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index 47e7379f4e..789d5a3dc8 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -71,6 +71,7 @@ use witnet_data_structures::{ data_request::DataRequestPool, get_environment, radon_report::{RadonReport, ReportContext}, + staking::prelude::*, superblock::{ARSIdentities, AddSuperBlockVote, SuperBlockConsensus}, transaction::{RevealTransaction, TallyTransaction, Transaction}, types::{ @@ -79,6 +80,7 @@ use witnet_data_structures::{ }, utxo_pool::{Diff, OwnUnspentOutputsPool, UnspentOutputsPool, UtxoWriteBatch}, vrf::VrfCtx, + wit::Wit, }; use witnet_rad::types::RadonTypes; use witnet_util::timestamp::seconds_to_human_string; @@ -247,6 +249,8 @@ pub struct ChainManager { import: Force>, /// Signals that a chain snapshot export is due. export: Force, + /// Tracks stakes for every validator in the network. + stakes: Stakes, } impl ChainManager { diff --git a/wallet/src/db/encrypted/engine.rs b/wallet/src/db/encrypted/engine.rs index 5982fd2773..d11b42dbd5 100644 --- a/wallet/src/db/encrypted/engine.rs +++ b/wallet/src/db/encrypted/engine.rs @@ -32,4 +32,16 @@ impl CryptoEngine { Ok(value) } + + pub fn decrypt_with(&self, bytes: &[u8], with: F) -> Result + where + T: DeserializeOwned, + F: Fn(&[u8]) -> Vec, + { + let decrypted = cipher::decrypt_aes_cbc(self.key.as_ref(), bytes, &self.iv)?; + let with_bytes = with(&decrypted); + let value = bincode::deserialize(&with_bytes)?; + + Ok(value) + } } diff --git a/wallet/src/db/encrypted/mod.rs b/wallet/src/db/encrypted/mod.rs index 35f060ae5a..24a7e990f4 100644 --- a/wallet/src/db/encrypted/mod.rs +++ b/wallet/src/db/encrypted/mod.rs @@ -4,7 +4,9 @@ use std::sync::Arc; use witnet_crypto::cipher; use super::*; -use crate::{db::encrypted::write_batch::EncryptedWriteBatch, types}; +use crate::{ + db::{encrypted::write_batch::EncryptedWriteBatch, GetWith}, types, +}; mod engine; mod prefix; From 4cc3a8cd02bd9b37c3705fe7fd2312a04b3fd3cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Thu, 22 Feb 2024 19:31:19 +0100 Subject: [PATCH 26/32] feat(node): process stake transactions when consolidating blocks --- data_structures/build.rs | 2 + data_structures/src/chain/mod.rs | 26 ++++++++- data_structures/src/staking/aux.rs | 21 ++++++- data_structures/src/staking/errors.rs | 3 + data_structures/src/staking/stake.rs | 21 +++---- data_structures/src/staking/stakes.rs | 80 +++++++++++++++++++++----- data_structures/src/transaction.rs | 22 +++++-- data_structures/src/wit.rs | 61 ++++++++++++++++++-- node/src/actors/chain_manager/actor.rs | 8 ++- node/src/actors/chain_manager/mod.rs | 10 +++- node/src/actors/json_rpc/api.rs | 7 +++ schemas/witnet/witnet.proto | 10 +++- 12 files changed, 230 insertions(+), 41 deletions(-) diff --git a/data_structures/build.rs b/data_structures/build.rs index 2a85b75c3a..576450cb49 100644 --- a/data_structures/build.rs +++ b/data_structures/build.rs @@ -14,6 +14,8 @@ fn create_path_to_protobuf_schema_env() { } fn main() { + println!("cargo:rerun-if-changed=../schemas/witnet/witnet.proto"); + create_path_to_protobuf_schema_env(); exonum_build::protobuf_generate( diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index cad5d63628..a92645ce63 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -25,7 +25,7 @@ use witnet_crypto::{ secp256k1::{ self, ecdsa::{RecoverableSignature, RecoveryId, Signature as Secp256k1_Signature}, - PublicKey as Secp256k1_PublicKey, SecretKey as Secp256k1_SecretKey, + Message, PublicKey as Secp256k1_PublicKey, SecretKey as Secp256k1_SecretKey, }, }; use witnet_protected::Protected; @@ -1012,6 +1012,25 @@ impl Signature { } } } + + pub fn verify( + &self, + msg: &Message, + public_key: &Secp256k1_PublicKey, + ) -> Result<(), failure::Error> { + match self { + Secp256k1(x) => { + let signature = Secp256k1_Signature::from_der(x.der.as_slice()) + .map_err(|_| Secp256k1ConversionError::FailSignatureConversion)?; + + signature + .verify(msg, public_key) + .map_err(|inner| Secp256k1ConversionError::Secp256k1 { inner })?; + + Ok(()) + } + } + } } /// ECDSA (over secp256k1) signature @@ -1498,11 +1517,12 @@ impl DataRequestOutput { } } -#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash)] +#[derive(Debug, Default, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, ProtobufConvert)] #[protobuf_convert(pb = "crate::proto::schema::witnet::StakeOutput")] pub struct StakeOutput { - pub value: u64, pub authorization: KeyedSignature, + pub key: StakeKey, + pub value: u64, } impl StakeOutput { diff --git a/data_structures/src/staking/aux.rs b/data_structures/src/staking/aux.rs index 3a6c706eac..2b4da95d97 100644 --- a/data_structures/src/staking/aux.rs +++ b/data_structures/src/staking/aux.rs @@ -1,9 +1,15 @@ use std::{rc::Rc, str::FromStr, sync::RwLock}; +use failure::Error; use serde::{Deserialize, Serialize}; +use crate::{chain::PublicKeyHash, proto::ProtobufConvert}; + use super::prelude::*; +/// Just a type alias for consistency of using the same data type to represent power. +pub type Power = u64; + /// The resulting type for all the fallible functions in this module. pub type StakingResult = Result>; @@ -37,7 +43,7 @@ where /// Couples a validator address with a withdrawer address together. This is meant to be used in `Stakes` as the index /// for the `by_key` index. -#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] pub struct StakeKey
{ /// A validator address. pub validator: Address, @@ -45,6 +51,19 @@ pub struct StakeKey
{ pub withdrawer: Address, } +impl ProtobufConvert for StakeKey { + type ProtoStruct = crate::proto::schema::witnet::StakeKey; + + fn to_pb(&self) -> Self::ProtoStruct { + let _proto = Self::ProtoStruct::new(); + todo!() + } + + fn from_pb(_pb: Self::ProtoStruct) -> Result { + todo!() + } +} + impl From<(T, T)> for StakeKey
where T: Into
, diff --git a/data_structures/src/staking/errors.rs b/data_structures/src/staking/errors.rs index 0c4bc69a39..7b270a92ef 100644 --- a/data_structures/src/staking/errors.rs +++ b/data_structures/src/staking/errors.rs @@ -34,6 +34,9 @@ pub enum StakesError { }, /// Tried to obtain a lock on a write-locked piece of data that is already locked. PoisonedLock, + /// The authentication signature contained within a stake transaction is not valid for the given validator and + /// withdrawer addresses. + InvalidAuthentication, } impl From> for StakesError { diff --git a/data_structures/src/staking/stake.rs b/data_structures/src/staking/stake.rs index 4752c5e130..0915df2b68 100644 --- a/data_structures/src/staking/stake.rs +++ b/data_structures/src/staking/stake.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use std::{marker::PhantomData, ops::*}; use serde::{Deserialize, Serialize}; @@ -28,14 +28,13 @@ where + 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, + + Add + + Sub + + Mul + + Mul, + Epoch: Copy + Default + num_traits::Saturating + Sub + From, + Power: Add + Div, + u64: From + From, { /// Increase the amount of coins staked by a certain staker. /// @@ -67,7 +66,9 @@ where let product_added = coins * epoch; let coins_after = coins_before + coins; - let epoch_after = (product_before + product_added) / coins_after; + let epoch_after = Epoch::from( + (u64::from(product_before + product_added) / u64::from(coins_after)) as u32, + ); self.coins = coins_after; self.epochs.update_all(epoch_after); diff --git a/data_structures/src/staking/stakes.rs b/data_structures/src/staking/stakes.rs index 192bec52fe..61385c4157 100644 --- a/data_structures/src/staking/stakes.rs +++ b/data_structures/src/staking/stakes.rs @@ -1,8 +1,13 @@ -use std::collections::{btree_map::Entry, BTreeMap}; +use std::{ + collections::{btree_map::Entry, BTreeMap}, + ops::{Add, Div, Mul, Sub}, +}; use itertools::Itertools; use serde::{Deserialize, Serialize}; +use crate::{chain::PublicKeyHash, transaction::StakeTransaction, wit::Wit}; + use super::prelude::*; /// The main data structure that provides the "stakes tracker" functionality. @@ -34,20 +39,16 @@ where + Default + Ord + From + + Into + num_traits::Zero - + std::ops::Add - + std::ops::Sub - + std::ops::Mul - + std::ops::Mul, + + Add + + Sub + + Mul + + 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, + Epoch: Copy + Default + num_traits::Saturating + Sub + From, + Power: Copy + Default + Ord + Add + Div, + u64: From + From, { /// Register a certain amount of additional stake for a certain address and epoch. pub fn add_stake( @@ -224,6 +225,59 @@ where } } +/// Adds stake, based on the data from a stake transaction. +/// +/// This function was made static instead of adding it to `impl Stakes` because it is not generic over `Address` and +/// `Coins`. +pub fn process_stake_transaction( + stakes: &mut Stakes, + transaction: &StakeTransaction, + epoch: Epoch, +) -> StakingResult<(), PublicKeyHash, Wit, Epoch> +where + Epoch: Copy + Default + Sub + num_traits::Saturating + From, + Power: + Add + Copy + Default + Div + Ord, + Wit: Mul, + u64: From + From, +{ + // This line would check that the authorization message is valid for the provided validator and withdrawer + // address. But it is commented out here because stake transactions should be validated upfront (when + // considering block candidates). The line is reproduced here for later reference when implementing those + // validations. Once those are in place, we're ok to remove this comment. + //transaction.body.authorization_is_valid().map_err(|_| StakesError::InvalidAuthentication)?; + + let key = transaction.body.output.key.clone(); + let coins = Wit::from_nanowits(transaction.body.output.value); + + stakes.add_stake(key, coins, epoch)?; + + Ok(()) +} + +/// Adds stakes, based on the data from multiple stake transactions. +/// +/// This function was made static instead of adding it to `impl Stakes` because it is not generic over `Address` and +/// `Coins`. +pub fn process_stake_transactions<'a, Epoch, Power>( + stakes: &mut Stakes, + transactions: impl Iterator, + epoch: Epoch, +) -> Result<(), StakesError> +where + Epoch: Copy + Default + Sub + num_traits::Saturating + From, + Power: + Add + Copy + Default + Div + Ord, + Wit: Mul, + u64: From + From, +{ + for transaction in transactions { + process_stake_transaction(stakes, transaction, epoch)?; + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/data_structures/src/transaction.rs b/data_structures/src/transaction.rs index e300ddfc98..4c838eaa71 100644 --- a/data_structures/src/transaction.rs +++ b/data_structures/src/transaction.rs @@ -3,7 +3,11 @@ use std::sync::{Arc, RwLock}; use protobuf::Message; use serde::{Deserialize, Serialize}; -use witnet_crypto::{hash::calculate_sha256, merkle::FullMerkleTree}; + +use witnet_crypto::{ + hash::calculate_sha256, merkle::FullMerkleTree, secp256k1::Message as Secp256k1Message, + signature::PublicKey, +}; use crate::{ chain::{ @@ -754,6 +758,16 @@ pub struct StakeTransactionBody { } impl StakeTransactionBody { + pub fn authorization_is_valid(&self) -> Result<(), failure::Error> { + let msg = Secp256k1Message::from_digest(self.output.key.withdrawer.as_secp256k1_msg()); + let public_key = PublicKey::from_slice(&self.output.authorization.public_key.bytes)?; + + self.output + .authorization + .signature + .verify(&msg, &public_key) + } + /// Construct a `StakeTransactionBody` from a list of inputs and one `StakeOutput`. pub fn new( inputs: Vec, @@ -823,7 +837,7 @@ impl UnstakeTransaction { #[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash)] #[protobuf_convert(pb = "witnet::UnstakeTransactionBody")] pub struct UnstakeTransactionBody { - pub operator: PublicKeyHash, + pub validator: PublicKeyHash, pub withdrawal: ValueTransferOutput, #[protobuf_convert(skip)] @@ -833,9 +847,9 @@ pub struct UnstakeTransactionBody { impl UnstakeTransactionBody { /// Creates a new stake transaction body. - pub fn new(operator: PublicKeyHash, withdrawal: ValueTransferOutput) -> Self { + pub fn new(validator: PublicKeyHash, withdrawal: ValueTransferOutput) -> Self { UnstakeTransactionBody { - operator, + validator, withdrawal, ..Default::default() } diff --git a/data_structures/src/wit.rs b/data_structures/src/wit.rs index d9066550e9..c9974003b1 100644 --- a/data_structures/src/wit.rs +++ b/data_structures/src/wit.rs @@ -1,7 +1,9 @@ -use std::fmt; +use std::{fmt, ops::*}; use serde::{Deserialize, Serialize}; +use crate::{chain::Epoch, staking::aux::Power}; + /// 1 nanowit is the minimal unit of value /// 1 wit = 10^9 nanowits pub const NANOWITS_PER_WIT: u64 = 1_000_000_000; @@ -19,7 +21,7 @@ impl Wit { /// Create from wits #[inline] pub fn from_wits(wits: u64) -> Self { - Self(wits.checked_mul(NANOWITS_PER_WIT).expect("overflow")) + Self::from_nanowits(wits.checked_mul(NANOWITS_PER_WIT).expect("overflow")) } /// Create from nanowits @@ -59,21 +61,46 @@ impl fmt::Display for Wit { } } -impl std::ops::Add for Wit { +impl Add for Wit { type Output = Self; #[inline] fn add(self, rhs: Self) -> Self::Output { - Self(self.nanowits() + rhs.nanowits()) + Self::from_nanowits(self.nanowits() + rhs.nanowits()) } } -impl std::ops::Sub for Wit { +impl Div for Wit { + type Output = Self; + + fn div(self, rhs: Self) -> Self::Output { + Self::from_nanowits(self.nanowits() / rhs.nanowits()) + } +} + +impl Mul for Wit { + type Output = Self; + + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Self::from_nanowits(self.nanowits() * rhs.nanowits()) + } +} + +impl Mul for Wit { + type Output = Power; + + fn mul(self, rhs: Epoch) -> Self::Output { + Power::from(self.nanowits() * u64::from(rhs)) + } +} + +impl Sub for Wit { type Output = Self; #[inline] fn sub(self, rhs: Self) -> Self::Output { - Self(self.nanowits() - rhs.nanowits()) + Self::from_nanowits(self.nanowits() - rhs.nanowits()) } } @@ -89,6 +116,28 @@ impl num_traits::Zero for Wit { } } +impl num_traits::ops::saturating::Saturating for Wit { + fn saturating_add(self, v: Self) -> Self { + Self::from_nanowits(self.nanowits().saturating_add(v.nanowits())) + } + + fn saturating_sub(self, v: Self) -> Self { + Self::from_nanowits(self.nanowits().saturating_sub(v.nanowits())) + } +} + +impl From for Wit { + fn from(value: u64) -> Self { + Self::from_nanowits(value) + } +} + +impl From for u64 { + fn from(value: Wit) -> Self { + value.0 + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/node/src/actors/chain_manager/actor.rs b/node/src/actors/chain_manager/actor.rs index 5187188d5f..b37a4968d4 100644 --- a/node/src/actors/chain_manager/actor.rs +++ b/node/src/actors/chain_manager/actor.rs @@ -13,10 +13,12 @@ use witnet_data_structures::{ }, data_request::DataRequestPool, get_environment, + staking::prelude::*, superblock::SuperBlockState, types::LastBeacon, utxo_pool::{OldUnspentOutputsPool, OwnUnspentOutputsPool, UtxoWriteBatch}, vrf::VrfCtx, + wit::Wit, }; use witnet_util::timestamp::pretty_print; @@ -223,9 +225,12 @@ impl ChainManager { } // Create a new ChainInfo let bootstrap_hash = consensus_constants.bootstrap_hash; - let reputation_engine = ReputationEngine::new(consensus_constants.activity_period as usize); let hash_prev_block = bootstrap_hash; + // Initialize configurable data structures + let reputation_engine = ReputationEngine::new(consensus_constants.activity_period as usize); + let stakes = Stakes::with_minimum(Wit::from_wits(MINIMUM_STAKEABLE_AMOUNT_WITS)); + let chain_info = ChainInfo { environment, consensus_constants: consensus_constants.clone(), @@ -257,6 +262,7 @@ impl ChainManager { own_utxos: OwnUnspentOutputsPool::new(), data_request_pool: DataRequestPool::new(consensus_constants.extra_rounds), superblock_state, + stakes, ..ChainState::default() } } diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index 789d5a3dc8..6dce3733da 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -910,6 +910,7 @@ impl ChainManager { ChainState { chain_info: Some(ref mut chain_info), reputation_engine: Some(ref mut reputation_engine), + ref mut stakes, .. } => { let block_hash = block.hash(); @@ -974,7 +975,7 @@ impl ChainManager { let miner_pkh = block.block_header.proof.proof.pkh(); - // Do not update reputation when consolidating genesis block + // Do not update reputation or stakes when consolidating genesis block if block_hash != chain_info.consensus_constants.genesis_hash { update_reputation( reputation_engine, @@ -986,6 +987,13 @@ impl ChainManager { block_epoch, self.own_pkh.unwrap_or_default(), ); + + let _ = process_stake_transactions( + stakes, + block.txns.stake_txns.iter(), + block_epoch, + ); + //process_unstake_transactions(stakes, block.txns.unstake_txns.iter(), block_epoch); } // Update bn256 public keys with block information diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index 9fae1df67e..417bb0af6b 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -27,6 +27,7 @@ use witnet_data_structures::{ PublicKeyHash, RADType, StakeOutput, StateMachine, SyncStatus, }, get_environment, + staking::prelude::*, transaction::Transaction, vrf::VrfMessage, }; @@ -2001,6 +2002,11 @@ pub async fn stake(params: Result) -> JsonRpcResult { signature }; + let key = StakeKey { + validator, + withdrawer, + }; + // Construct a BuildStake message that we can relay to the ChainManager for creation of the Stake transaction let build_stake = BuildStake { dry_run: params.dry_run, @@ -2008,6 +2014,7 @@ pub async fn stake(params: Result) -> JsonRpcResult { utxo_strategy: params.utxo_strategy, stake_output: StakeOutput { authorization, + key, value: params.value, }, }; diff --git a/schemas/witnet/witnet.proto b/schemas/witnet/witnet.proto index f110eda14c..2d6d83f3aa 100644 --- a/schemas/witnet/witnet.proto +++ b/schemas/witnet/witnet.proto @@ -287,9 +287,15 @@ message MintTransaction { repeated ValueTransferOutput outputs = 2; } +message StakeKey { + PublicKeyHash validator = 1; + PublicKeyHash withdrawer = 2; +} + message StakeOutput { uint64 value = 1; - KeyedSignature authorization = 2; + StakeKey key = 2; + KeyedSignature authorization = 3; } message StakeTransactionBody { @@ -304,7 +310,7 @@ message StakeTransaction { } message UnstakeTransactionBody { - PublicKeyHash operator = 1; + PublicKeyHash validator = 1; ValueTransferOutput withdrawal = 2; ValueTransferOutput change = 3; } From cf3885f4391991399d548e4768543fffa4136a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Mon, 26 Feb 2024 17:15:17 +0100 Subject: [PATCH 27/32] feat(node): complete stake transaction implementation --- config/src/config.rs | 2 +- data_structures/src/chain/mod.rs | 10 +++++++++ data_structures/src/staking/aux.rs | 17 ++++++++++++---- data_structures/src/staking/stakes.rs | 27 ++++++++++++++++++------- node/src/actors/chain_manager/mining.rs | 14 +++++++++---- node/src/actors/chain_manager/mod.rs | 18 +++++++++-------- validations/src/validations.rs | 3 ++- wallet/src/db/encrypted/mod.rs | 3 ++- 8 files changed, 68 insertions(+), 26 deletions(-) diff --git a/config/src/config.rs b/config/src/config.rs index dbc5fc5eb5..d18e5e8cf2 100644 --- a/config/src/config.rs +++ b/config/src/config.rs @@ -131,7 +131,7 @@ pub struct Config { pub witnessing: Witnessing, /// Configuration related with protocol versions - #[partial_struct(skip)] + #[partial_struct(ty = "Protocol")] #[partial_struct(serde(default))] pub protocol: Protocol, } diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index a92645ce63..ea6d437923 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -1218,6 +1218,16 @@ impl Hash { (d_hash, m_u64) } + + /// Obtains the bytes that represent the hash digest. + /// + /// This allows for compatibility with functions that take hashes and other data as raw bytes without a newtype or + /// any other kind of wrapper. + pub fn data(&self) -> [u8; 32] { + match self { + Hash::SHA256(bytes) => *bytes, + } + } } /// Error when parsing hash from string diff --git a/data_structures/src/staking/aux.rs b/data_structures/src/staking/aux.rs index 2b4da95d97..6898f8ecb2 100644 --- a/data_structures/src/staking/aux.rs +++ b/data_structures/src/staking/aux.rs @@ -55,12 +55,21 @@ impl ProtobufConvert for StakeKey { type ProtoStruct = crate::proto::schema::witnet::StakeKey; fn to_pb(&self) -> Self::ProtoStruct { - let _proto = Self::ProtoStruct::new(); - todo!() + let mut proto = Self::ProtoStruct::new(); + proto.set_validator(self.validator.to_pb()); + proto.set_withdrawer(self.withdrawer.to_pb()); + + proto } - fn from_pb(_pb: Self::ProtoStruct) -> Result { - todo!() + fn from_pb(mut pb: Self::ProtoStruct) -> Result { + let validator = PublicKeyHash::from_pb(pb.take_validator())?; + let withdrawer = PublicKeyHash::from_pb(pb.take_withdrawer())?; + + Ok(Self { + validator, + withdrawer, + }) } } diff --git a/data_structures/src/staking/stakes.rs b/data_structures/src/staking/stakes.rs index 61385c4157..426df952f4 100644 --- a/data_structures/src/staking/stakes.rs +++ b/data_structures/src/staking/stakes.rs @@ -1,12 +1,13 @@ use std::{ collections::{btree_map::Entry, BTreeMap}, + fmt::Debug, ops::{Add, Div, Mul, Sub}, }; use itertools::Itertools; use serde::{Deserialize, Serialize}; -use crate::{chain::PublicKeyHash, transaction::StakeTransaction, wit::Wit}; +use crate::{chain::PublicKeyHash, get_environment, transaction::StakeTransaction, wit::Wit}; use super::prelude::*; @@ -29,6 +30,10 @@ where /// 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. + /// TODO: reconsider whether this should be here, taking into account that it hinders the possibility of adjusting + /// the minimum through TAPI or whatever. Maybe what we can do is set a skip directive for the Serialize macro so + /// it never gets persisted and rather always read from constants, or hide the field and the related method + /// behind a #[test] thing. minimum_stakeable: Option, } @@ -235,9 +240,8 @@ pub fn process_stake_transaction( epoch: Epoch, ) -> StakingResult<(), PublicKeyHash, Wit, Epoch> where - Epoch: Copy + Default + Sub + num_traits::Saturating + From, - Power: - Add + Copy + Default + Div + Ord, + Epoch: Copy + Default + Sub + num_traits::Saturating + From + Debug, + Power: Add + Copy + Default + Div + Ord + Debug, Wit: Mul, u64: From + From, { @@ -250,8 +254,18 @@ where let key = transaction.body.output.key.clone(); let coins = Wit::from_nanowits(transaction.body.output.value); + let environment = get_environment(); + log::debug!( + "{} added {} Wit more stake on validator {}", + key.withdrawer.bech32(environment), + coins.wits_and_nanowits().0, + key.validator.bech32(environment) + ); + stakes.add_stake(key, coins, epoch)?; + log::debug!("Current state of the stakes tracker: {:#?}", stakes); + Ok(()) } @@ -265,9 +279,8 @@ pub fn process_stake_transactions<'a, Epoch, Power>( epoch: Epoch, ) -> Result<(), StakesError> where - Epoch: Copy + Default + Sub + num_traits::Saturating + From, - Power: - Add + Copy + Default + Div + Ord, + Epoch: Copy + Default + Sub + num_traits::Saturating + From + Debug, + Power: Add + Copy + Default + Div + Ord + Debug, Wit: Mul, u64: From + From, { diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 69560baeeb..bd66d510ec 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -30,12 +30,13 @@ use witnet_data_structures::{ }, error::TransactionError, get_environment, get_protocol_version, - proto::versioning::ProtocolVersion, + proto::versioning::{ProtocolVersion, VersionedHashable}, radon_error::RadonError, radon_report::{RadonReport, ReportContext, TypeLike}, transaction::{ CommitTransaction, CommitTransactionBody, DRTransactionBody, MintTransaction, - RevealTransaction, RevealTransactionBody, TallyTransaction, VTTransactionBody, + RevealTransaction, RevealTransactionBody, StakeTransactionBody, TallyTransaction, + VTTransactionBody, }, transaction_factory::{build_commit_collateral, check_commit_collateral}, utxo_pool::{UnspentOutputsPool, UtxoDiff}, @@ -52,7 +53,7 @@ use witnet_util::timestamp::get_timestamp; use witnet_validations::validations::{ block_reward, calculate_liars_and_errors_count_from_tally, calculate_mining_probability, calculate_randpoe_threshold, calculate_reppoe_threshold, dr_transaction_fee, merkle_tree_root, - tally_bytes_on_encode_error, update_utxo_diff, vt_transaction_fee, + st_transaction_fee, tally_bytes_on_encode_error, update_utxo_diff, vt_transaction_fee, }; use crate::{ @@ -248,7 +249,10 @@ impl ChainManager { ); // Sign the block hash - signature_mngr::sign(&block_header) + let protocol = get_protocol_version(Some(block_header.beacon.checkpoint)); + let block_header_data = block_header.versioned_hash(protocol).data(); + + signature_mngr::sign_data(block_header_data) .map(|res| { res.map_err(|e| log::error!("Couldn't sign beacon: {}", e)) .map(|block_sig| Block::new(block_header, block_sig, txns)) @@ -1010,8 +1014,10 @@ pub fn build_block( let protocol = get_protocol_version(Some(beacon.checkpoint)); let stake_hash_merkle_root = if protocol == ProtocolVersion::V1_7 { + log::debug!("Legacy protocol: the default stake hash merkle root will be used"); Hash::default() } else { + log::debug!("Pseudo-2.0 protocol: a merkle tree will be built for the stake transactions"); merkle_tree_root(&stake_txns) }; diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index 6dce3733da..02d822f551 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -80,7 +80,6 @@ use witnet_data_structures::{ }, utxo_pool::{Diff, OwnUnspentOutputsPool, UnspentOutputsPool, UtxoWriteBatch}, vrf::VrfCtx, - wit::Wit, }; use witnet_rad::types::RadonTypes; use witnet_util::timestamp::seconds_to_human_string; @@ -249,8 +248,6 @@ pub struct ChainManager { import: Force>, /// Signals that a chain snapshot export is due. export: Force, - /// Tracks stakes for every validator in the network. - stakes: Stakes, } impl ChainManager { @@ -988,11 +985,16 @@ impl ChainManager { self.own_pkh.unwrap_or_default(), ); - let _ = process_stake_transactions( - stakes, - block.txns.stake_txns.iter(), - block_epoch, - ); + let stake_txns_count = block.txns.stake_txns.len(); + if stake_txns_count > 0 { + log::debug!("Processing {stake_txns_count} stake transactions"); + + let _ = process_stake_transactions( + stakes, + block.txns.stake_txns.iter(), + block_epoch, + ); + } //process_unstake_transactions(stakes, block.txns.unstake_txns.iter(), block_epoch); } diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 3a8b5247a1..a0595d3c4b 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -8,6 +8,7 @@ use std::{ use itertools::Itertools; use witnet_config::defaults::{ + PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS, PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO, PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE, }; @@ -56,7 +57,7 @@ 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 MIN_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS; const MAX_UNSTAKE_BLOCK_WEIGHT: u32 = 5_000; const UNSTAKING_DELAY_SECONDS: u32 = 1_209_600; diff --git a/wallet/src/db/encrypted/mod.rs b/wallet/src/db/encrypted/mod.rs index 24a7e990f4..a7fb1d283c 100644 --- a/wallet/src/db/encrypted/mod.rs +++ b/wallet/src/db/encrypted/mod.rs @@ -5,7 +5,8 @@ use witnet_crypto::cipher; use super::*; use crate::{ - db::{encrypted::write_batch::EncryptedWriteBatch, GetWith}, types, + db::{encrypted::write_batch::EncryptedWriteBatch, GetWith}, + types, }; mod engine; From e6c963eb4070bf0f1696e46a6f87784eec71f167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Mon, 26 Feb 2024 18:47:25 +0100 Subject: [PATCH 28/32] feat(node): add stake transaction block weight checks close #2434 --- node/src/actors/chain_manager/mining.rs | 63 +++++++++++++++++-- node/src/actors/inventory_manager/handlers.rs | 2 + 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index bd66d510ec..ab08278c12 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -16,7 +16,10 @@ use actix::{ use ansi_term::Color::{White, Yellow}; use futures::future::{try_join_all, FutureExt}; -use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE; +use witnet_config::defaults::{ + PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT, + PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE, +}; use witnet_data_structures::{ chain::{ tapi::{after_second_hard_fork, ActiveWips}, @@ -115,6 +118,7 @@ impl ChainManager { let chain_info = self.chain_state.chain_info.as_mut().unwrap(); let max_vt_weight = chain_info.consensus_constants.max_vt_weight; let max_dr_weight = chain_info.consensus_constants.max_dr_weight; + let max_st_weight = PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT; let mining_bf = chain_info.consensus_constants.mining_backup_factor; let mining_rf = chain_info.consensus_constants.mining_replication_factor; let collateral_minimum = chain_info.consensus_constants.collateral_minimum; @@ -232,6 +236,7 @@ impl ChainManager { ), max_vt_weight, max_dr_weight, + max_st_weight, beacon, eligibility_claim, &tally_transactions, @@ -814,11 +819,13 @@ impl ChainManager { /// Build a new Block using the supplied leadership proof and by filling transactions from the /// `transaction_pool` /// Returns an unsigned block! +/// TODO: simplify function signature, e.g. through merging multiple related fields into new data structures. #[allow(clippy::too_many_arguments)] pub fn build_block( pools_ref: (&mut TransactionsPool, &UnspentOutputsPool, &DataRequestPool), max_vt_weight: u32, max_dr_weight: u32, + max_st_weight: u32, beacon: CheckpointBeacon, proof: BlockEligibilityClaim, tally_transactions: &[TallyTransaction], @@ -842,18 +849,21 @@ pub fn build_block( let mut transaction_fees: u64 = 0; let mut vt_weight: u32 = 0; let mut dr_weight: u32 = 0; + let mut st_weight: u32 = 0; 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 mut stake_txns = Vec::new(); // TODO: handle unstake tx let unstake_txns = Vec::new(); + // Calculate the base weight for different types of transactions, to know when to give up trying to fit more + // transactions into a block let min_vt_weight = VTTransactionBody::new(vec![Input::default()], vec![ValueTransferOutput::default()]) .weight(); - // Currently only value transfer transactions weight is taking into account + let min_st_weight = + StakeTransactionBody::new(vec![Input::default()], Default::default(), None).weight(); for vt_tx in transactions_pool.vt_iter() { let transaction_weight = vt_tx.weight(); @@ -868,7 +878,7 @@ pub fn build_block( } }; - let new_vt_weight = vt_weight.saturating_add(transaction_weight); + let new_vt_weight = st_weight.saturating_add(transaction_weight); if new_vt_weight <= max_vt_weight { update_utxo_diff( &mut utxo_diff, @@ -994,6 +1004,39 @@ pub fn build_block( } } + for st_tx in transactions_pool.st_iter() { + let transaction_weight = st_tx.weight(); + let transaction_fee = match st_transaction_fee(st_tx, &utxo_diff, epoch, epoch_constants) { + Ok(x) => x, + Err(e) => { + log::warn!( + "Error when calculating transaction fee for transaction: {}", + e + ); + continue; + } + }; + + let new_st_weight = st_weight.saturating_add(transaction_weight); + if new_st_weight <= max_st_weight { + update_utxo_diff( + &mut utxo_diff, + st_tx.body.inputs.iter(), + st_tx.body.change.iter(), + st_tx.hash(), + ); + stake_txns.push(st_tx.clone()); + transaction_fees = transaction_fees.saturating_add(transaction_fee); + st_weight = new_st_weight; + } + + // The condition to stop is if the free space in the block for VTTransactions + // is less than the minimum stake transaction weight + if st_weight > max_st_weight.saturating_sub(min_st_weight) { + break; + } + } + // Include Mint Transaction by miner let reward = block_reward(epoch, initial_block_reward, halving_period) + transaction_fees; let mint = MintTransaction::with_external_address( @@ -1096,6 +1139,7 @@ mod tests { // Set `max_vt_weight` and `max_dr_weight` to zero (no transaction should be included) let max_vt_weight = 0; let max_dr_weight = 0; + let max_st_weight = 0; // Fields required to mine a block let block_beacon = CheckpointBeacon::default(); @@ -1109,6 +1153,7 @@ mod tests { (&mut transaction_pool, &unspent_outputs_pool, &dr_pool), max_vt_weight, max_dr_weight, + max_st_weight, block_beacon, block_proof, &[], @@ -1277,6 +1322,7 @@ mod tests { // Set `max_vt_weight` to fit only `transaction_1` weight let max_vt_weight = vt_tx1.weight(); let max_dr_weight = 0; + let max_st_weight = 0; // Insert transactions into `transactions_pool` let mut transaction_pool = TransactionsPool::default(); @@ -1313,6 +1359,7 @@ mod tests { (&mut transaction_pool, &unspent_outputs_pool, &dr_pool), max_vt_weight, max_dr_weight, + max_st_weight, block_beacon, block_proof, &[], @@ -1378,6 +1425,7 @@ mod tests { // Set `max_vt_weight` to fit only 1 transaction weight let max_vt_weight = vt_tx2.weight(); let max_dr_weight = 0; + let max_st_weight = 0; // Insert transactions into `transactions_pool` let mut transaction_pool = TransactionsPool::default(); @@ -1414,6 +1462,7 @@ mod tests { (&mut transaction_pool, &unspent_outputs_pool, &dr_pool), max_vt_weight, max_dr_weight, + max_st_weight, block_beacon, block_proof, &[], @@ -1493,6 +1542,7 @@ mod tests { // Set `max_vt_weight` to fit only `transaction_1` weight let max_vt_weight = 0; let max_dr_weight = dr_tx1.weight(); + let max_st_weight = 0; // Insert transactions into `transactions_pool` let mut transaction_pool = TransactionsPool::default(); @@ -1529,6 +1579,7 @@ mod tests { (&mut transaction_pool, &unspent_outputs_pool, &dr_pool), max_vt_weight, max_dr_weight, + max_st_weight, block_beacon, block_proof, &[], @@ -1591,6 +1642,7 @@ mod tests { // Set `max_vt_weight` to fit only `transaction_1` weight let max_vt_weight = 0; let max_dr_weight = dr_tx2.weight(); + let max_st_weight = 0; // Insert transactions into `transactions_pool` let mut transaction_pool = TransactionsPool::default(); @@ -1627,6 +1679,7 @@ mod tests { (&mut transaction_pool, &unspent_outputs_pool, &dr_pool), max_vt_weight, max_dr_weight, + max_st_weight, block_beacon, block_proof, &[], diff --git a/node/src/actors/inventory_manager/handlers.rs b/node/src/actors/inventory_manager/handlers.rs index 7b64841046..d2a7983723 100644 --- a/node/src/actors/inventory_manager/handlers.rs +++ b/node/src/actors/inventory_manager/handlers.rs @@ -447,6 +447,7 @@ mod tests { // Set `max_vt_weight` to fit only `transaction_1` weight let max_vt_weight = vt_tx1.weight(); let max_dr_weight = 0; + let max_st_weight = 0; // Insert transactions into `transactions_pool` let mut transaction_pool = TransactionsPool::default(); @@ -480,6 +481,7 @@ mod tests { (&mut transaction_pool, &unspent_outputs_pool, &dr_pool), max_vt_weight, max_dr_weight, + max_st_weight, block_beacon, block_proof, &[], From 2e84cff877086bb3071bf7266f1a2cbbc7023e64 Mon Sep 17 00:00:00 2001 From: tommytrg Date: Thu, 29 Feb 2024 17:42:14 +0100 Subject: [PATCH 29/32] feat(jsonrpc): implement query stakes method This method allows to query the stakes of the specified argument. The argument can contain: - A validator and a withdrawer - A validator - A withdrawer - Empty argument, uses the node's address as validator. The type of the argument is an address as a string. --- data_structures/src/staking/aux.rs | 14 ++ data_structures/src/staking/errors.rs | 69 +++++++- data_structures/src/staking/stake.rs | 19 ++- data_structures/src/staking/stakes.rs | 183 ++++++++++++++++++++-- node/src/actors/chain_manager/handlers.rs | 14 +- node/src/actors/json_rpc/api.rs | 63 +++++++- node/src/actors/messages.rs | 46 ++++++ 7 files changed, 387 insertions(+), 21 deletions(-) diff --git a/data_structures/src/staking/aux.rs b/data_structures/src/staking/aux.rs index 6898f8ecb2..1ebb5c28c5 100644 --- a/data_structures/src/staking/aux.rs +++ b/data_structures/src/staking/aux.rs @@ -1,3 +1,4 @@ +use std::fmt::{Debug, Display, Formatter}; use std::{rc::Rc, str::FromStr, sync::RwLock}; use failure::Error; @@ -98,6 +99,19 @@ where } } +impl
Display for StakeKey
+where + Address: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "validator: {} withdrawer: {}", + self.validator, self.withdrawer + ) + } +} + /// Couples an amount of coins, a validator address and a withdrawer address together. This is meant to be used in /// `Stakes` as the index of the `by_coins` index. #[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] diff --git a/data_structures/src/staking/errors.rs b/data_structures/src/staking/errors.rs index 7b270a92ef..869536de34 100644 --- a/data_structures/src/staking/errors.rs +++ b/data_structures/src/staking/errors.rs @@ -1,12 +1,25 @@ -use std::sync::PoisonError; - use crate::staking::aux::StakeKey; +use failure::Fail; +use std::{ + convert::From, + fmt::{Debug, Display}, + sync::PoisonError, +}; /// All errors related to the staking functionality. -#[derive(Debug, PartialEq)] -pub enum StakesError { +#[derive(Debug, PartialEq, Fail)] +pub enum StakesError +where + Address: Debug + Display + Sync + Send + 'static, + Coins: Debug + Display + Sync + Send + 'static, + Epoch: Debug + Display + Sync + Send + 'static, +{ /// The amount of coins being staked or the amount that remains after unstaking is below the /// minimum stakeable amount. + #[fail( + display = "The amount of coins being staked ({}) or the amount that remains after unstaking is below the minimum stakeable amount ({})", + amount, minimum + )] AmountIsBelowMinimum { /// The number of coins being staked or remaining after staking. amount: Coins, @@ -14,6 +27,10 @@ pub enum StakesError { minimum: Coins, }, /// Tried to query `Stakes` for information that belongs to the past. + #[fail( + display = "Tried to query `Stakes` for information that belongs to the past. Query Epoch: {} Latest Epoch: {}", + epoch, latest + )] EpochInThePast { /// The Epoch being referred. epoch: Epoch, @@ -21,6 +38,10 @@ pub enum StakesError { latest: Epoch, }, /// An operation thrown an Epoch value that overflows. + #[fail( + display = "An operation thrown an Epoch value that overflows. Computed Epoch: {} Maximum Epoch: {}", + computed, maximum + )] EpochOverflow { /// The computed Epoch value. computed: u64, @@ -28,18 +49,56 @@ pub enum StakesError { maximum: Epoch, }, /// Tried to query for a stake entry that is not registered in `Stakes`. + #[fail( + display = "Tried to query for a stake entry that is not registered in Stakes {}", + key + )] EntryNotFound { /// A validator and withdrawer address pair. key: StakeKey
, }, /// Tried to obtain a lock on a write-locked piece of data that is already locked. + #[fail( + display = "The authentication signature contained within a stake transaction is not valid for the given validator and withdrawer addresses" + )] PoisonedLock, /// The authentication signature contained within a stake transaction is not valid for the given validator and /// withdrawer addresses. + #[fail( + display = "The authentication signature contained within a stake transaction is not valid for the given validator and withdrawer addresses" + )] InvalidAuthentication, + /// Tried to query for a stake entry by validator that is not registered in `Stakes`. + #[fail( + display = "Tried to query for a stake entry by validator ({}) that is not registered in Stakes", + validator + )] + ValidatorNotFound { + /// A validator address. + validator: Address, + }, + /// Tried to query for a stake entry by withdrawer that is not registered in `Stakes`. + #[fail( + display = "Tried to query for a stake entry by withdrawer ({}) that is not registered in Stakes", + withdrawer + )] + WithdrawerNotFound { + /// A withdrawer address. + withdrawer: Address, + }, + /// Tried to query for a stake entry without providing a validator or a withdrawer address. + #[fail( + display = "Tried to query a stake entry without providing a validator or a withdrawer address" + )] + EmptyQuery, } -impl From> for StakesError { +impl From> for StakesError +where + Address: Debug + Display + Sync + Send + 'static, + Coins: Debug + Display + Sync + Send + 'static, + Epoch: Debug + Display + Sync + Send + 'static, +{ fn from(_value: PoisonError) -> Self { StakesError::PoisonedLock } diff --git a/data_structures/src/staking/stake.rs b/data_structures/src/staking/stake.rs index 0915df2b68..ea1926da92 100644 --- a/data_structures/src/staking/stake.rs +++ b/data_structures/src/staking/stake.rs @@ -3,6 +3,7 @@ use std::{marker::PhantomData, ops::*}; use serde::{Deserialize, Serialize}; use super::prelude::*; +use std::fmt::{Debug, Display}; /// A data structure that keeps track of a staker's staked coins and the epochs for different /// capabilities. @@ -23,7 +24,7 @@ where impl Stake where - Address: Default, + Address: Default + Debug + Display + Sync + Send, Coins: Copy + From + PartialOrd @@ -31,8 +32,20 @@ where + Add + Sub + Mul - + Mul, - Epoch: Copy + Default + num_traits::Saturating + Sub + From, + + Mul + + Debug + + Display + + Send + + Sync, + Epoch: Copy + + Default + + num_traits::Saturating + + Sub + + From + + Debug + + Display + + Sync + + Send, Power: Add + Div, u64: From + From, { diff --git a/data_structures/src/staking/stakes.rs b/data_structures/src/staking/stakes.rs index 426df952f4..a5e22a6f82 100644 --- a/data_structures/src/staking/stakes.rs +++ b/data_structures/src/staking/stakes.rs @@ -1,6 +1,6 @@ use std::{ collections::{btree_map::Entry, BTreeMap}, - fmt::Debug, + fmt::{Debug, Display}, ops::{Add, Div, Mul, Sub}, }; @@ -11,6 +11,47 @@ use crate::{chain::PublicKeyHash, get_environment, transaction::StakeTransaction use super::prelude::*; +/// Message for querying stakes +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum QueryStakesKey { + /// Query stakes by validator address + Validator(Address), + /// Query stakes by withdrawer address + Withdrawer(Address), + /// Query stakes by validator and withdrawer addresses + Key(StakeKey
), +} + +impl
Default for QueryStakesKey
+where + Address: Default + Ord, +{ + fn default() -> Self { + QueryStakesKey::Validator(Address::default()) + } +} + +impl TryFrom<(Option, Option)> for QueryStakesKey
+where + Address: Default + Ord, + T: Into
, +{ + type Error = String; + fn try_from(val: (Option, Option)) -> Result { + match val { + (Some(validator), Some(withdrawer)) => Ok(QueryStakesKey::Key(StakeKey { + validator: validator.into(), + withdrawer: withdrawer.into(), + })), + (Some(validator), _) => Ok(QueryStakesKey::Validator(validator.into())), + (_, Some(withdrawer)) => Ok(QueryStakesKey::Withdrawer(withdrawer.into())), + _ => Err(String::from( + "Either a validator address, a withdrawer address or both must be provided.", + )), + } + } +} + /// The main data structure that provides the "stakes tracker" functionality. /// /// This structure holds indexes of stake entries. Because the entries themselves are reference @@ -24,6 +65,10 @@ where { /// A listing of all the stakers, indexed by their address. by_key: BTreeMap, SyncStake>, + /// A listing of all the stakers, indexed by validator. + by_validator: BTreeMap>, + /// A listing of all the stakers, indexed by withdrawer. + by_withdrawer: 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 @@ -39,7 +84,7 @@ where impl Stakes where - Address: Default, + Address: Default + Send + Sync + Display, Coins: Copy + Default + Ord @@ -49,9 +94,21 @@ where + Add + Sub + Mul - + Mul, - Address: Clone + Ord + 'static, - Epoch: Copy + Default + num_traits::Saturating + Sub + From, + + Mul + + Debug + + Send + + Sync + + Display, + Address: Clone + Ord + 'static + Debug, + Epoch: Copy + + Default + + num_traits::Saturating + + Sub + + From + + Debug + + Display + + Send + + Sync, Power: Copy + Default + Ord + Add + Div, u64: From + From, { @@ -78,12 +135,21 @@ where // 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 = CoinsAndAddresses { + let coins_and_addresses = CoinsAndAddresses { coins, addresses: key, }; - self.by_coins.remove(&key); - self.by_coins.insert(key, stake.clone()); + self.by_coins.remove(&coins_and_addresses); + self.by_coins + .insert(coins_and_addresses.clone(), stake.clone()); + + let validator_key = coins_and_addresses.clone().addresses.validator; + self.by_validator.remove(&validator_key); + self.by_validator.insert(validator_key, stake.clone()); + + let withdrawer_key = coins_and_addresses.addresses.withdrawer; + self.by_withdrawer.remove(&withdrawer_key); + self.by_withdrawer.insert(withdrawer_key, stake.clone()); Ok(stake.value.read()?.clone()) } @@ -228,6 +294,64 @@ where ..Default::default() } } + + /// Query stakes based on different keys. + pub fn query_stakes( + &mut self, + query: TIQSK, + ) -> StakingResult + where + TIQSK: TryInto>, + { + match query.try_into() { + Ok(QueryStakesKey::Key(key)) => self.query_by_key(key), + Ok(QueryStakesKey::Validator(validator)) => self.query_by_validator(validator), + Ok(QueryStakesKey::Withdrawer(withdrawer)) => self.query_by_withdrawer(withdrawer), + Err(_) => Err(StakesError::EmptyQuery), + } + } + + /// Query stakes by stake key. + #[inline(always)] + fn query_by_key(&self, key: StakeKey
) -> StakingResult { + Ok(self + .by_key + .get(&key) + .ok_or(StakesError::EntryNotFound { key })? + .value + .read()? + .coins) + } + + /// Query stakes by validator address. + #[inline(always)] + fn query_by_validator( + &self, + validator: Address, + ) -> StakingResult { + Ok(self + .by_validator + .get(&validator) + .ok_or(StakesError::ValidatorNotFound { validator })? + .value + .read()? + .coins) + } + + /// Query stakes by withdrawer address. + #[inline(always)] + fn query_by_withdrawer( + &self, + withdrawer: Address, + ) -> StakingResult { + Ok(self + .by_withdrawer + .get(&withdrawer) + .ok_or(StakesError::WithdrawerNotFound { withdrawer })? + .value + .read()? + .coins) + } } /// Adds stake, based on the data from a stake transaction. @@ -240,7 +364,15 @@ pub fn process_stake_transaction( epoch: Epoch, ) -> StakingResult<(), PublicKeyHash, Wit, Epoch> where - Epoch: Copy + Default + Sub + num_traits::Saturating + From + Debug, + Epoch: Copy + + Default + + Sub + + num_traits::Saturating + + From + + Debug + + Display + + Send + + Sync, Power: Add + Copy + Default + Div + Ord + Debug, Wit: Mul, u64: From + From, @@ -279,7 +411,15 @@ pub fn process_stake_transactions<'a, Epoch, Power>( epoch: Epoch, ) -> Result<(), StakesError> where - Epoch: Copy + Default + Sub + num_traits::Saturating + From + Debug, + Epoch: Copy + + Default + + Sub + + num_traits::Saturating + + From + + Debug + + Send + + Sync + + Display, Power: Add + Copy + Default + Div + Ord + Debug, Wit: Mul, u64: From + From, @@ -591,4 +731,27 @@ mod tests { ] ); } + + #[test] + fn test_query_stakes() { + // First, lets create a setup with a few stakers + let mut stakes = Stakes::::with_minimum(5); + let alice = "Alice"; + let bob = "Bob"; + let charlie = "Charlie"; + let david = "David"; + let erin = "Erin"; + + let alice_charlie = (alice, charlie); + let bob_david = (bob, david); + let charlie_erin = (charlie, erin); + + stakes.add_stake(alice_charlie, 10, 0).unwrap(); + stakes.add_stake(bob_david, 20, 20).unwrap(); + stakes.add_stake(charlie_erin, 30, 30).unwrap(); + + let result = stakes.query_stakes(QueryStakesKey::Key(alice_charlie.into())); + + assert_eq!(result, Ok(10)) + } } diff --git a/node/src/actors/chain_manager/handlers.rs b/node/src/actors/chain_manager/handlers.rs index ec28d633f9..eb489c60ae 100644 --- a/node/src/actors/chain_manager/handlers.rs +++ b/node/src/actors/chain_manager/handlers.rs @@ -19,6 +19,7 @@ use witnet_data_structures::{ Hashable, NodeStats, PublicKeyHash, SuperBlockVote, SupplyInfo, }, error::{ChainInfoError, TransactionError::DataRequestNotFound}, + staking::errors::StakesError, transaction::{DRTransaction, StakeTransaction, Transaction, VTTransaction}, transaction_factory::{self, NodeBalance}, types::LastBeacon, @@ -37,7 +38,7 @@ use crate::{ GetDataRequestInfo, GetHighestCheckpointBeacon, GetMemoryTransaction, GetMempool, GetMempoolResult, GetNodeStats, GetReputation, GetReputationResult, GetSignalingInfo, GetState, GetSuperBlockVotes, GetSupplyInfo, GetUtxoInfo, IsConfirmedBlock, - PeersBeacons, ReputationStats, Rewind, SendLastBeacon, SessionUnitResult, + PeersBeacons, QueryStake, ReputationStats, Rewind, SendLastBeacon, SessionUnitResult, SetLastBeacon, SetPeersLimits, SignalingInfo, SnapshotExport, SnapshotImport, TryMineBlock, }, @@ -1356,6 +1357,17 @@ impl Handler for ChainManager { } } +impl Handler for ChainManager { + type Result = ::Result; + + fn handle(&mut self, msg: QueryStake, _ctx: &mut Self::Context) -> Self::Result { + // build address from public key hash + let stakes = self.chain_state.stakes.query_stakes(msg.key); + + stakes.map_err(StakesError::from).map_err(Into::into) + } +} + impl Handler for ChainManager { type Result = ResponseActFuture>; diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index 417bb0af6b..76c4a1d6c3 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -45,8 +45,8 @@ use crate::{ GetConsolidatedPeers, GetDataRequestInfo, GetEpoch, GetHighestCheckpointBeacon, GetItemBlock, GetItemSuperblock, GetItemTransaction, GetKnownPeers, GetMemoryTransaction, GetMempool, GetNodeStats, GetReputation, GetSignalingInfo, - GetState, GetSupplyInfo, GetUtxoInfo, InitializePeers, IsConfirmedBlock, Rewind, - SnapshotExport, SnapshotImport, StakeAuthorization, + GetState, GetSupplyInfo, GetUtxoInfo, InitializePeers, IsConfirmedBlock, QueryStake, + QueryStakesParams, Rewind, SnapshotExport, SnapshotImport, StakeAuthorization, }, peers_manager::PeersManager, sessions_manager::SessionsManager, @@ -139,6 +139,9 @@ pub fn attach_regular_methods( Box::pin(signaling_info()) }); server.add_actix_method(system, "priority", |_params: Params| Box::pin(priority())); + server.add_actix_method(system, "queryStakes", |params: Params| { + Box::pin(query_stakes(params.parse())) + }); } /// Attach the sensitive JSON-RPC methods to a multi-transport server. @@ -2094,6 +2097,62 @@ pub async fn authorize_stake(params: Result) -> JsonRpcRe .await } +/// Param for query_stakes +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum QueryStakesArgument { + /// To query by stake validator + Validator(String), + /// To query by stake withdrawer + Withdrawer(String), + /// To query by stake validator and withdrawer + Key((String, String)), +} + +/// Query the amount of nanowits staked by an address. +pub async fn query_stakes(params: Result, Error>) -> JsonRpcResult { + // Short-circuit if parameters are wrong + let params = params?; + + // If a withdrawer address is not specified, default to local node address + let key: QueryStakesParams = if let Some(address) = params { + match address { + QueryStakesArgument::Validator(validator) => QueryStakesParams::Validator( + PublicKeyHash::from_bech32(get_environment(), &validator) + .map_err(internal_error)?, + ), + QueryStakesArgument::Withdrawer(withdrawer) => QueryStakesParams::Withdrawer( + PublicKeyHash::from_bech32(get_environment(), &withdrawer) + .map_err(internal_error)?, + ), + QueryStakesArgument::Key((validator, withdrawer)) => QueryStakesParams::Key(( + PublicKeyHash::from_bech32(get_environment(), &validator) + .map_err(internal_error)?, + PublicKeyHash::from_bech32(get_environment(), &withdrawer) + .map_err(internal_error)?, + )), + } + } else { + let pk = signature_mngr::public_key().await.map_err(internal_error)?; + + QueryStakesParams::Validator(PublicKeyHash::from_public_key(&pk)) + }; + + ChainManager::from_registry() + .send(QueryStake { key }) + .map(|res| match res { + Ok(Ok(staked_amount)) => serde_json::to_value(staked_amount).map_err(internal_error), + Ok(Err(e)) => { + let err = internal_error_s(e); + Err(err) + } + Err(e) => { + let err = internal_error_s(e); + Err(err) + } + }) + .await +} + #[cfg(test)] mod mock_actix { use actix::{MailboxError, Message}; diff --git a/node/src/actors/messages.rs b/node/src/actors/messages.rs index 7cf75f5110..fe605488de 100644 --- a/node/src/actors/messages.rs +++ b/node/src/actors/messages.rs @@ -27,6 +27,7 @@ use witnet_data_structures::{ }, fee::{deserialize_fee_backwards_compatible, Fee}, radon_report::RadonReport, + staking::{aux::StakeKey, stakes::QueryStakesKey}, transaction::{ CommitTransaction, DRTransaction, RevealTransaction, StakeTransaction, Transaction, VTTransaction, @@ -34,6 +35,7 @@ use witnet_data_structures::{ transaction_factory::NodeBalance, types::LastBeacon, utxo_pool::{UtxoInfo, UtxoSelectionStrategy}, + wit::Wit, }; use witnet_p2p::{ error::SessionsError, @@ -305,6 +307,50 @@ impl Message for StakeAuthorization { type Result = Result; } +/// Stake key for quering stakes +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum QueryStakesParams { + /// To search by the validator public key hash + Validator(PublicKeyHash), + /// To search by the withdrawer public key hash + Withdrawer(PublicKeyHash), + /// To search by validator and withdrawer public key hashes + Key((PublicKeyHash, PublicKeyHash)), +} + +impl Default for QueryStakesParams { + fn default() -> Self { + QueryStakesParams::Validator(PublicKeyHash::default()) + } +} + +/// Message for querying stakes +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct QueryStake { + /// stake key used to search the stake + pub key: QueryStakesParams, +} + +impl Message for QueryStake { + type Result = Result; +} + +impl
From for QueryStakesKey
+where + Address: Default + Ord + From, +{ + fn from(query: QueryStakesParams) -> Self { + match query { + QueryStakesParams::Key(key) => QueryStakesKey::Key(StakeKey { + validator: key.0.into(), + withdrawer: key.1.into(), + }), + QueryStakesParams::Validator(v) => QueryStakesKey::Validator(v.into()), + QueryStakesParams::Withdrawer(w) => QueryStakesKey::Withdrawer(w.into()), + } + } +} + /// Builds a `DataRequestTransaction` from a `DataRequestOutput` #[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct BuildDrt { From ef7bf95b2f5e1e7af351f658ff30ad9d4894e301 Mon Sep 17 00:00:00 2001 From: tommytrg Date: Wed, 6 Mar 2024 13:41:27 +0100 Subject: [PATCH 30/32] feat(CLI): implement method for querying stakes Please enter the commit message for your changes. Lines starting --- src/cli/node/json_rpc_client.rs | 32 +++++++++++++++++++++++++++++++- src/cli/node/with_node.rs | 14 ++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/cli/node/json_rpc_client.rs b/src/cli/node/json_rpc_client.rs index b022a14b4e..cd345e139a 100644 --- a/src/cli/node/json_rpc_client.rs +++ b/src/cli/node/json_rpc_client.rs @@ -41,7 +41,9 @@ use witnet_data_structures::{ }; use witnet_node::actors::{ chain_manager::run_dr_locally, - json_rpc::api::{AddrType, GetBlockChainParams, GetTransactionOutput, PeersResult}, + json_rpc::api::{ + AddrType, GetBlockChainParams, GetTransactionOutput, PeersResult, QueryStakesArgument, + }, messages::{ AuthorizeStake, BuildDrt, BuildStakeParams, BuildStakeResponse, BuildVtt, GetBalanceTarget, GetReputationResult, MagicEither, SignalingInfo, StakeAuthorization, @@ -1826,6 +1828,34 @@ pub fn priority(addr: SocketAddr, json: bool) -> Result<(), failure::Error> { Ok(()) } +pub fn query_stakes( + addr: SocketAddr, + validator: Option, + withdrawer: Option, +) -> Result<(), failure::Error> { + let mut stream = start_client(addr)?; + + let params = match (validator, withdrawer) { + (Some(validator), Some(withdrawer)) => { + Some(QueryStakesArgument::Key((validator, withdrawer))) + } + (Some(validator), _) => Some(QueryStakesArgument::Validator(validator)), + (_, Some(withdrawer)) => Some(QueryStakesArgument::Withdrawer(withdrawer)), + (None, None) => None, + }; + + let response = send_request( + &mut stream, + &format!( + r#"{{"jsonrpc": "2.0","method": "queryStakes", "params": {}, "id": 1}}"#, + serde_json::to_string(¶ms).unwrap() + ), + )?; + log::info!("{}", response); + + Ok(()) +} + #[derive(Serialize, Deserialize)] struct SignatureWithData { address: String, diff --git a/src/cli/node/with_node.rs b/src/cli/node/with_node.rs index 567e32f918..0a45cbd3ab 100644 --- a/src/cli/node/with_node.rs +++ b/src/cli/node/with_node.rs @@ -290,6 +290,11 @@ pub fn exec_cmd( Command::AuthorizeStake { node, withdrawer } => { rpc::authorize_st(node.unwrap_or(default_jsonrpc), withdrawer) } + Command::QueryStakes { + node, + withdrawer, + validator, + } => rpc::query_stakes(node.unwrap_or(default_jsonrpc), withdrawer, validator), } } @@ -785,6 +790,15 @@ pub enum Command { #[structopt(long = "withdrawer")] withdrawer: Option, }, + QueryStakes { + /// Socket address of the Witnet node to query + #[structopt(short = "n", long = "node")] + node: Option, + #[structopt(short = "v", long = "validator")] + validator: Option, + #[structopt(short = "w", long = "withdrawer")] + withdrawer: Option, + }, } #[derive(Debug, StructOpt)] From 4c5b3685db88455accf3b2871db43f140a2bbdff Mon Sep 17 00:00:00 2001 From: drcpu Date: Sun, 14 Apr 2024 20:06:17 +0200 Subject: [PATCH 31/32] feat(stakes): track the last epoch when a validator was active --- data_structures/src/staking/stakes.rs | 39 ++++++++++++++++++++++++--- node/src/actors/chain_manager/mod.rs | 18 +++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/data_structures/src/staking/stakes.rs b/data_structures/src/staking/stakes.rs index a5e22a6f82..728c18c2c4 100644 --- a/data_structures/src/staking/stakes.rs +++ b/data_structures/src/staking/stakes.rs @@ -1,5 +1,6 @@ use std::{ - collections::{btree_map::Entry, BTreeMap}, + cmp::PartialOrd, + collections::{btree_map::Entry, BTreeMap, HashSet}, fmt::{Debug, Display}, ops::{Add, Div, Mul, Sub}, }; @@ -80,6 +81,9 @@ where /// it never gets persisted and rather always read from constants, or hide the field and the related method /// behind a #[test] thing. minimum_stakeable: Option, + /// A listing of all active validators (mined a block or solved a data request) indexed by the validator address + /// and the epoch of when it was active as value + by_active: BTreeMap, } impl Stakes @@ -108,7 +112,8 @@ where + Debug + Display + Send - + Sync, + + Sync + + PartialOrd, Power: Copy + Default + Ord + Add + Div, u64: From + From, { @@ -251,6 +256,7 @@ where // No need to keep the entry if the stake has gone to zero if final_coins.is_zero() { by_address_entry.remove(); + self.by_active.remove(&key.validator); self.by_coins.remove(&CoinsAndAddresses { coins: initial_coins, addresses: key, @@ -311,6 +317,29 @@ where } } + /// Return the total number of validators. + pub fn validator_count(&self, active_since: Option) -> usize { + match active_since { + None => self.by_active.len(), + Some(epoch) => { + let mut count = 0; + for (_, active) in self.by_active.iter() { + if *active >= epoch { + count += 1; + } + } + count + } + } + } + + /// Update the active epoch of the validators passed as an argument + pub fn update_active_epoch(&mut self, validators: HashSet
, active: Epoch) { + for validator in validators { + self.by_active.entry(validator).and_modify(|validator| *validator = active).or_insert(active); + } + } + /// Query stakes by stake key. #[inline(always)] fn query_by_key(&self, key: StakeKey
) -> StakingResult { @@ -372,7 +401,8 @@ where + Debug + Display + Send - + Sync, + + Sync + + PartialOrd, Power: Add + Copy + Default + Div + Ord + Debug, Wit: Mul, u64: From + From, @@ -419,7 +449,8 @@ where + Debug + Send + Sync - + Display, + + Display + + PartialOrd, Power: Add + Copy + Default + Div + Ord + Debug, Wit: Mul, u64: From + From, diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index 02d822f551..bb88785122 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -972,6 +972,24 @@ impl ChainManager { let miner_pkh = block.block_header.proof.proof.pkh(); + // Track all active addresses to update the active epoch of all validators + let mut active_addresses: HashSet = HashSet::new(); + active_addresses.insert(miner_pkh); + + for ta_tx in &block.txns.tally_txns { + // Only update active epoch of honest validators + // If there was a liar or error committer, the last output is the creator of the data request + // This is not guaranteed to be a validator, so do not update its active epoch + let data_requester_output = ta_tx.out_of_consensus.len() + ta_tx.error_committers.len() > 0; + for (i, output) in ta_tx.outputs.iter().enumerate() { + if data_requester_output && i == ta_tx.outputs.len() - 1 { + break; + } + active_addresses.insert(output.pkh); + } + } + stakes.update_active_epoch(active_addresses, block_epoch); + // Do not update reputation or stakes when consolidating genesis block if block_hash != chain_info.consensus_constants.genesis_hash { update_reputation( From f8bf45d6d2399e69f6857814052ed289fad8bf91 Mon Sep 17 00:00:00 2001 From: drcpu Date: Mon, 15 Apr 2024 20:35:36 +0200 Subject: [PATCH 32/32] feat(superblock) sample superblock voters from a list of active validators --- data_structures/src/chain/mod.rs | 14 +- data_structures/src/staking/stakes.rs | 14 +- data_structures/src/superblock.rs | 220 +++++++++++++------------- node/src/actors/chain_manager/mod.rs | 32 +--- 4 files changed, 137 insertions(+), 143 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index ea6d437923..95c75c811e 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -4592,7 +4592,7 @@ mod tests { use crate::{ proto::versioning::{ProtocolVersion, VersionedHashable}, - superblock::{mining_build_superblock, ARSIdentities}, + superblock::{mining_build_superblock, ValidatorIdentities}, transaction::{CommitTransactionBody, RevealTransactionBody, VTTransactionBody}, }; @@ -6473,11 +6473,11 @@ mod tests { let expected_order = vec![p1_bls, p2_bls, p3_bls]; let ordered_identities = rep_engine.get_rep_ordered_ars_list(); - let ars_identities = ARSIdentities::new(ordered_identities); + let validator_identities = ValidatorIdentities::new(ordered_identities); assert_eq!( expected_order, - ars_identities.get_rep_ordered_bn256_list(&alt_keys) + validator_identities.get_ordered_bn256_list(&alt_keys) ); } @@ -6515,11 +6515,11 @@ mod tests { let expected_order = vec![p1_bls, p2_bls, p3_bls]; let ordered_identities = rep_engine.get_rep_ordered_ars_list(); - let ars_identities = ARSIdentities::new(ordered_identities); + let validator_identities = ValidatorIdentities::new(ordered_identities); assert_eq!( expected_order, - ars_identities.get_rep_ordered_bn256_list(&alt_keys) + validator_identities.get_ordered_bn256_list(&alt_keys) ); } @@ -6573,11 +6573,11 @@ mod tests { let expected_order = vec![p1_bls, p2_bls, p4_bls, p5_bls, p3_bls]; let ordered_identities = rep_engine.get_rep_ordered_ars_list(); - let ars_identities = ARSIdentities::new(ordered_identities); + let validator_identities = ValidatorIdentities::new(ordered_identities); assert_eq!( expected_order, - ars_identities.get_rep_ordered_bn256_list(&alt_keys) + validator_identities.get_ordered_bn256_list(&alt_keys) ); } diff --git a/data_structures/src/staking/stakes.rs b/data_structures/src/staking/stakes.rs index 728c18c2c4..8d10c27ab8 100644 --- a/data_structures/src/staking/stakes.rs +++ b/data_structures/src/staking/stakes.rs @@ -333,10 +333,22 @@ where } } + /// Get a vector of all validator that were active since a given epoch + pub fn get_active_validators(&self, active_since: Epoch) -> Vec
{ + self.by_active + .iter() + .filter(|(_, epoch)| **epoch >= active_since) + .map(|(address, _)| address.clone()) + .collect() + } + /// Update the active epoch of the validators passed as an argument pub fn update_active_epoch(&mut self, validators: HashSet
, active: Epoch) { for validator in validators { - self.by_active.entry(validator).and_modify(|validator| *validator = active).or_insert(active); + self.by_active + .entry(validator) + .and_modify(|validator| *validator = active) + .or_insert(active); } } diff --git a/data_structures/src/superblock.rs b/data_structures/src/superblock.rs index 8557596ed7..94e22a3f2f 100644 --- a/data_structures/src/superblock.rs +++ b/data_structures/src/superblock.rs @@ -52,16 +52,16 @@ pub enum SuperBlockConsensus { Unknown, } -/// ARS identities +/// Validator identities #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -pub struct ARSIdentities { - // HashSet of the identities in a specific ARS +pub struct ValidatorIdentities { + // HashSet of the validator identities at a specific block height identities: HashSet, - // Ordered vector of the identities in a specific ARS + // Ordered vector of the most recently active validator identities at a specific block height ordered_identities: Vec, } -impl ARSIdentities { +impl ValidatorIdentities { pub fn len(&self) -> usize { self.identities.len() } @@ -71,13 +71,13 @@ impl ARSIdentities { } pub fn new(ordered_identities: Vec) -> Self { - ARSIdentities { + ValidatorIdentities { identities: ordered_identities.iter().cloned().collect(), ordered_identities, } } - pub fn get_rep_ordered_bn256_list(&self, alt_keys: &AltKeys) -> Vec { + pub fn get_ordered_bn256_list(&self, alt_keys: &AltKeys) -> Vec { self.ordered_identities .iter() .filter_map(|pkh| alt_keys.get_bn256(pkh).cloned()) @@ -225,10 +225,10 @@ impl SuperBlockVotesMempool { /// State related to superblocks #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] pub struct SuperBlockState { - // Structure of the current Active Reputation Set identities - ars_current_identities: ARSIdentities, - // Structure of the previous Active Reputation Set identities - ars_previous_identities: ARSIdentities, + // Structure of the current set of validator identities + validator_current_identities: ValidatorIdentities, + // Structure of the previous set of validator identities + validator_previous_identities: ValidatorIdentities, /// The most recently created superblock. This one is yet to be voted and decided upon. current_superblock: Option, // Current superblock beacon including the superblock hash created by this node @@ -246,8 +246,8 @@ impl SuperBlockState { pub fn new(superblock_genesis_hash: Hash, bootstrap_committee: Vec) -> Self { Self { signing_committee: bootstrap_committee.clone().into_iter().collect(), - ars_previous_identities: ARSIdentities::new(bootstrap_committee.clone()), - ars_current_identities: ARSIdentities::new(bootstrap_committee), + validator_previous_identities: ValidatorIdentities::new(bootstrap_committee.clone()), + validator_current_identities: ValidatorIdentities::new(bootstrap_committee), current_superblock_beacon: CheckpointBeacon { checkpoint: 0, hash_prev_block: superblock_genesis_hash, @@ -306,7 +306,7 @@ impl SuperBlockState { Some(true) => self.insert_vote(sbv.clone()), Some(false) => { if sbv.superblock_index == current_superblock_index - || self.ars_previous_identities.is_empty() + || self.validator_previous_identities.is_empty() { AddSuperBlockVote::NotInSigningCommittee } else { @@ -402,13 +402,13 @@ impl SuperBlockState { } } - fn update_ars_identities(&mut self, new_identities: ARSIdentities) { - self.ars_previous_identities = std::mem::take(&mut self.ars_current_identities); - self.ars_current_identities = new_identities; + fn update_validator_identities(&mut self, new_identities: ValidatorIdentities) { + self.validator_previous_identities = std::mem::take(&mut self.validator_current_identities); + self.validator_current_identities = new_identities; } /// Produces a `SuperBlock` that includes the blocks in `block_headers` if there is at least one of them. - /// `ars_identities` will be used to validate all the superblock votes received for the + /// `validator_identities` will be used to validate all the superblock votes received for the /// next superblock. The votes for the current superblock must be validated using them /// to calculate the superblock_signing_committee. /// The ordered bn256 keys will be merkelized and appended to the superblock @@ -416,7 +416,7 @@ impl SuperBlockState { pub fn build_superblock( &mut self, block_headers: &[BlockHeader], - ars_identities: ARSIdentities, + validator_identities: ValidatorIdentities, signing_committee_size: u32, superblock_index: u32, last_block_in_previous_superblock: Hash, @@ -424,9 +424,9 @@ impl SuperBlockState { sync_superblock: Option, block_epoch: Epoch, ) -> SuperBlock { - let key_leaves = hash_key_leaves(&ars_identities.get_rep_ordered_bn256_list(alt_keys)); + let key_leaves = hash_key_leaves(&validator_identities.get_ordered_bn256_list(alt_keys)); - self.update_ars_identities(ars_identities); + self.update_validator_identities(validator_identities); // During synchronization we use the superblock received as consensus by our outbounds // to have the right value of the signing committee size. From now on, we have all the values @@ -434,7 +434,7 @@ impl SuperBlockState { let superblock = if let Some(sb) = sync_superblock { // Before updating the superblock_beacon, calculate the signing committee let signing_committee = calculate_superblock_signing_committee( - self.ars_previous_identities.clone(), + self.validator_previous_identities.clone(), sb.signing_committee_length, superblock_index, self.current_superblock_beacon.hash_prev_block, @@ -453,7 +453,7 @@ impl SuperBlockState { } else { // Before updating the superblock_beacon, calculate the signing committee let signing_committee = calculate_superblock_signing_committee( - self.ars_previous_identities.clone(), + self.validator_previous_identities.clone(), signing_committee_size, superblock_index, self.current_superblock_beacon.hash_prev_block, @@ -562,18 +562,18 @@ impl SuperBlockState { /// Calculates the superblock signing committee for a given superblock hash and ars #[allow(clippy::cast_possible_truncation)] pub fn calculate_superblock_signing_committee( - ars_identities: ARSIdentities, + validator_identities: ValidatorIdentities, signing_committee_size: u32, current_superblock_index: u32, superblock_hash: Hash, block_epoch: Epoch, ) -> HashSet { // If the number of identities is lower than committee_size all the members of the ARS sign the superblock - if ars_identities.len() < usize::try_from(signing_committee_size).unwrap() { - ars_identities.identities + if validator_identities.len() < usize::try_from(signing_committee_size).unwrap() { + validator_identities.identities } else if after_second_hard_fork(block_epoch, get_environment()) { // Start counting the members of the subset from: - // sha256(superblock_hash || superblock_index) % ars_identities.len() + // sha256(superblock_hash || superblock_index) % validator_identities.len() let superblock_hash_and_index_bytes = [ superblock_hash.as_ref(), current_superblock_index.to_be_bytes().as_ref(), @@ -582,12 +582,12 @@ pub fn calculate_superblock_signing_committee( let superblock_hash_and_index_bytes_hashed = Hash::from(calculate_sha256(&superblock_hash_and_index_bytes)); let first = superblock_hash_and_index_bytes_hashed - .div_mod(ars_identities.len() as u64) + .div_mod(validator_identities.len() as u64) .1 as usize; // Get the subset let subset = magic_partition_2( - &ars_identities.ordered_identities, + &validator_identities.ordered_identities, first, signing_committee_size.try_into().unwrap(), superblock_hash_and_index_bytes_hashed.as_ref(), @@ -603,11 +603,11 @@ pub fn calculate_superblock_signing_committee( // Start counting the members of the subset from the superblock_hash plus superblock index hash let mut first = u32::from(first_byte_sb_hash) + u32::from(first_byte_index_hash); // We need to choose a first member from all the potential ARS members - first %= ars_identities.len() as u32; + first %= validator_identities.len() as u32; // Get the subset let subset = magic_partition( - &ars_identities.ordered_identities.to_vec(), + &validator_identities.ordered_identities.to_vec(), first.try_into().unwrap(), signing_committee_size.try_into().unwrap(), ); @@ -956,7 +956,7 @@ mod tests { let sb1 = sbs.build_superblock( &block_headers, - ARSIdentities::new(ars2), + ValidatorIdentities::new(ars2), 100, 0, Hash::default(), @@ -1005,12 +1005,12 @@ mod tests { let block_headers = vec![BlockHeader::default()]; let pkhs = vec![create_pkh(1)]; let keys = vec![create_bn256(1)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let genesis_hash = Hash::default(); let sb1 = sbs.build_superblock( &block_headers, - ars_identities, + validator_identities, 100, 0, genesis_hash, @@ -1035,12 +1035,12 @@ mod tests { let genesis_hash = Hash::default(); let pkhs = vec![create_pkh(1)]; let keys = vec![create_bn256(1)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let first_superblock = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 0, genesis_hash, @@ -1052,7 +1052,7 @@ mod tests { let expected_superblock = SuperBlock::new( 0, hash_merkle_tree_root(&hash_key_leaves( - &ars_identities.get_rep_ordered_bn256_list(&alt_keys), + &validator_identities.get_ordered_bn256_list(&alt_keys), )), Hash::default(), 0, @@ -1068,13 +1068,13 @@ mod tests { .unwrap(); let expected_sbs = SuperBlockState { - ars_current_identities: ars_identities, + ars_current_identities: validator_identities, signing_committee: HashSet::new(), current_superblock_beacon: CheckpointBeacon { checkpoint: 0, hash_prev_block: expected_superblock_hash, }, - ars_previous_identities: ARSIdentities::default(), + ars_previous_identities: ValidatorIdentities::default(), ..Default::default() }; assert_eq!(sbs, expected_sbs); @@ -1087,14 +1087,14 @@ mod tests { let block_headers = vec![BlockHeader::default()]; let pkhs = vec![create_pkh(1)]; let keys = vec![create_bn256(1)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let genesis_hash = Hash::default(); sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 0, genesis_hash, @@ -1106,7 +1106,7 @@ mod tests { let expected_second_superblock = SuperBlock::new( 1, hash_merkle_tree_root(&hash_key_leaves( - &ars_identities.get_rep_ordered_bn256_list(&alt_keys), + &validator_identities.get_ordered_bn256_list(&alt_keys), )), Hash::default(), 1, @@ -1118,7 +1118,7 @@ mod tests { assert_eq!( sbs.build_superblock( &[], - ars_identities.clone(), + validator_identities.clone(), 100, 1, genesis_hash, @@ -1135,8 +1135,8 @@ mod tests { checkpoint: 1, hash_prev_block: expected_second_superblock.hash(), }; - expected_sbs.signing_committee = ars_identities.identities.iter().cloned().collect(); - expected_sbs.ars_previous_identities = ars_identities; + expected_sbs.signing_committee = validator_identities.identities.iter().cloned().collect(); + expected_sbs.ars_previous_identities = validator_identities; assert_eq!(sbs, expected_sbs); } @@ -1156,13 +1156,13 @@ mod tests { let block_headers = vec![BlockHeader::default()]; let pkhs = vec![create_pkh(1)]; let keys = vec![create_bn256(1)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let genesis_hash = Hash::default(); let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 0, genesis_hash, @@ -1179,7 +1179,7 @@ mod tests { let _sb2 = sbs.build_superblock( &block_headers, - ars_identities, + validator_identities, 100, 1, genesis_hash, @@ -1213,9 +1213,9 @@ mod tests { let p1 = PublicKey::from_bytes([1; 33]); let pkhs = vec![p1.pkh()]; let keys = vec![create_bn256(1)]; - let ars0 = ARSIdentities::new(vec![]); - let ars1 = ARSIdentities::new(pkhs.clone()); - let ars2 = ARSIdentities::new(pkhs.clone()); + let ars0 = ValidatorIdentities::new(vec![]); + let ars1 = ValidatorIdentities::new(pkhs.clone()); + let ars2 = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); @@ -1276,9 +1276,9 @@ mod tests { let p1 = PublicKey::from_bytes([1; 33]); let pkhs = vec![p1.pkh()]; let keys = vec![create_bn256(1)]; - let ars0 = ARSIdentities::new(vec![]); - let ars1 = ARSIdentities::new(pkhs.clone()); - let ars2 = ARSIdentities::new(pkhs.clone()); + let ars0 = ValidatorIdentities::new(vec![]); + let ars1 = ValidatorIdentities::new(pkhs.clone()); + let ars2 = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); // Superblock votes for index 0 cannot be validated because we do not know the ARS for index -1 @@ -1339,9 +1339,9 @@ mod tests { let p1 = PublicKey::from_bytes([1; 33]); let pkhs = vec![p1.pkh()]; let keys = vec![create_bn256(1)]; - let ars0 = ARSIdentities::new(vec![]); - let ars1 = ARSIdentities::new(pkhs.clone()); - let ars2 = ARSIdentities::new(pkhs.clone()); + let ars0 = ValidatorIdentities::new(vec![]); + let ars1 = ValidatorIdentities::new(pkhs.clone()); + let ars2 = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); // Superblock votes for index 0 cannot be validated because we do not know the ARS for index -1 @@ -1402,9 +1402,9 @@ mod tests { let p1 = PublicKey::from_bytes([1; 33]); let pkhs = vec![p1.pkh()]; let keys = vec![create_bn256(1)]; - let ars0 = ARSIdentities::new(vec![]); - let ars1 = ARSIdentities::new(pkhs.clone()); - let ars2 = ARSIdentities::new(pkhs.clone()); + let ars0 = ValidatorIdentities::new(vec![]); + let ars1 = ValidatorIdentities::new(pkhs.clone()); + let ars2 = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); // Superblock votes for index 0 cannot be validated because we do not know the ARS for index -1 @@ -1479,11 +1479,11 @@ mod tests { let p2 = PublicKey::from_bytes([2; 33]); let p3 = PublicKey::from_bytes([3; 33]); - let ars0 = ARSIdentities::new(vec![]); - let ars1 = ARSIdentities::new(vec![p1.pkh()]); - let ars2 = ARSIdentities::new(vec![p2.pkh()]); - let ars3 = ARSIdentities::new(vec![p3.pkh()]); - let ars4 = ARSIdentities::new(vec![]); + let ars0 = ValidatorIdentities::new(vec![]); + let ars1 = ValidatorIdentities::new(vec![p1.pkh()]); + let ars2 = ValidatorIdentities::new(vec![p2.pkh()]); + let ars3 = ValidatorIdentities::new(vec![p3.pkh()]); + let ars4 = ValidatorIdentities::new(vec![]); let pkhs = vec![p1.pkh(), p2.pkh(), p3.pkh()]; let keys = vec![create_bn256(1), create_bn256(2), create_bn256(3)]; let alt_keys = create_alt_keys(pkhs, keys); @@ -1634,7 +1634,7 @@ mod tests { let pkhs = vec![p1.pkh(), p2.pkh(), p3.pkh()]; let keys = vec![create_bn256(1), create_bn256(2), create_bn256(3)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let create_votes = |superblock_hash, superblock_index| { @@ -1659,7 +1659,7 @@ mod tests { // (because it does not exist). When adding a vote it will return NotInSigningCommittee let sb0 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 0, genesis_hash, @@ -1687,10 +1687,10 @@ mod tests { AddSuperBlockVote::NotInSigningCommittee ); - // Create a superblock with the ars_identities + // Create a superblock with the validator_identities let sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 1, genesis_hash, @@ -1719,7 +1719,7 @@ mod tests { let sb2 = sbs.build_superblock( &block_headers, - ars_identities, + validator_identities, 100, 2, genesis_hash, @@ -1765,9 +1765,9 @@ mod tests { ]; let alt_keys = create_alt_keys(pkhs1.clone(), keys); - let ars0 = ARSIdentities::new(pkhs0); - let ars1 = ARSIdentities::new(pkhs1); - let ars2 = ARSIdentities::new(pkhs2); + let ars0 = ValidatorIdentities::new(pkhs0); + let ars1 = ValidatorIdentities::new(pkhs1); + let ars2 = ValidatorIdentities::new(pkhs2); let create_votes = |superblock_hash, superblock_index| { let mut v1 = SuperBlockVote::new_unsigned(superblock_hash, superblock_index); @@ -1894,14 +1894,14 @@ mod tests { let pkhs = vec![p1.pkh(), p2.pkh(), p3.pkh()]; let keys = vec![create_bn256(1), create_bn256(2), create_bn256(3)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let block_headers = vec![BlockHeader::default()]; let genesis_hash = Hash::default(); let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 0, genesis_hash, @@ -1912,7 +1912,7 @@ mod tests { let expected_sb2 = mining_build_superblock( &block_headers, - &hash_key_leaves(&ars_identities.get_rep_ordered_bn256_list(&alt_keys)), + &hash_key_leaves(&validator_identities.get_ordered_bn256_list(&alt_keys)), 1, genesis_hash, 3, @@ -1929,7 +1929,7 @@ mod tests { // Create the second superblock afterwards let sb2 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 1, genesis_hash, @@ -1954,7 +1954,7 @@ mod tests { // set as "InvalidIndex" let _sb3 = sbs.build_superblock( &block_headers, - ars_identities, + validator_identities, 100, 2, genesis_hash, @@ -1982,7 +1982,7 @@ mod tests { let pkhs = vec![p1.pkh(), p2.pkh(), p3.pkh()]; let keys = vec![create_bn256(1), create_bn256(2), create_bn256(3)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let block_headers = vec![BlockHeader::default()]; @@ -1990,7 +1990,7 @@ mod tests { // superblock with index 0. let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 2, 0, genesis_hash, @@ -2001,7 +2001,7 @@ mod tests { // superblock with index 1 let sb2 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 2, 1, genesis_hash, @@ -2012,7 +2012,7 @@ mod tests { let expected_sb2 = mining_build_superblock( &block_headers, - &hash_key_leaves(&ars_identities.get_rep_ordered_bn256_list(&alt_keys)), + &hash_key_leaves(&validator_identities.get_ordered_bn256_list(&alt_keys)), 1, genesis_hash, 2, @@ -2060,13 +2060,13 @@ mod tests { let block_headers = vec![BlockHeader::default()]; let pkhs = vec![create_pkh(1)]; let keys = vec![create_bn256(1)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let genesis_hash = Hash::default(); let _sb1 = sbs.build_superblock( &block_headers, - ars_identities, + validator_identities, 100, 2, genesis_hash, @@ -2089,13 +2089,13 @@ mod tests { let block_headers = vec![BlockHeader::default()]; let pkhs = vec![create_pkh(1)]; let keys = vec![create_bn256(1)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let genesis_hash = Hash::default(); let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 2, genesis_hash, @@ -2106,7 +2106,7 @@ mod tests { let _sb2 = sbs.build_superblock( &block_headers, - ars_identities, + validator_identities, 2, 5, genesis_hash, @@ -2142,7 +2142,7 @@ mod tests { create_bn256(3), create_bn256(4), ]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let block_headers = vec![BlockHeader::default()]; @@ -2150,7 +2150,7 @@ mod tests { // superblock with index 0. let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 2, 0, genesis_hash, @@ -2161,7 +2161,7 @@ mod tests { // superblock with index 1 let sb2 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 2, 1, genesis_hash, @@ -2172,7 +2172,7 @@ mod tests { let expected_sb2 = mining_build_superblock( &block_headers, - &hash_key_leaves(&ars_identities.get_rep_ordered_bn256_list(&alt_keys)), + &hash_key_leaves(&validator_identities.get_ordered_bn256_list(&alt_keys)), 1, genesis_hash, 2, @@ -2223,14 +2223,14 @@ mod tests { let pkhs = vec![p1.pkh(), p2.pkh(), p3.pkh()]; let keys = vec![create_bn256(1), create_bn256(2), create_bn256(3)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let block_headers = vec![BlockHeader::default()]; let genesis_hash = Hash::default(); let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 0, genesis_hash, @@ -2238,7 +2238,7 @@ mod tests { None, 1, ); - sbs.ars_previous_identities = ars_identities.clone(); + sbs.ars_previous_identities = validator_identities.clone(); let committee_size = 4; let subset = calculate_superblock_signing_committee( sbs.ars_previous_identities, @@ -2247,7 +2247,7 @@ mod tests { sbs.current_superblock_beacon.hash_prev_block, 1, ); - assert_eq!(ars_identities.len(), subset.len()); + assert_eq!(validator_identities.len(), subset.len()); } #[test] fn test_build_superblock_with_optional_superblock() { @@ -2268,7 +2268,7 @@ mod tests { create_bn256(4), create_bn256(5), ]; - let ars = ARSIdentities::new(pkhs.clone()); + let ars = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); sbs.ars_current_identities = ars.clone(); @@ -2342,14 +2342,14 @@ mod tests { p7.pkh(), p8.pkh(), ]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, vec![]); let block_headers = vec![BlockHeader::default()]; let genesis_hash = Hash::default(); let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 4, 0, genesis_hash, @@ -2357,7 +2357,7 @@ mod tests { None, 1, ); - sbs.ars_previous_identities = ars_identities; + sbs.ars_previous_identities = validator_identities; let committee_size = 4; let subset = calculate_superblock_signing_committee( sbs.ars_previous_identities, @@ -2387,14 +2387,14 @@ mod tests { let p3 = PublicKey::from_bytes([3; 33]); let pkhs = vec![p1.pkh(), p2.pkh(), p3.pkh()]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, vec![]); let block_headers = vec![BlockHeader::default()]; let genesis_hash = Hash::default(); let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 2, 0, genesis_hash, @@ -2402,7 +2402,7 @@ mod tests { None, 1, ); - sbs.ars_previous_identities = ars_identities.clone(); + sbs.ars_previous_identities = validator_identities.clone(); let committee_size = 2; let subset = calculate_superblock_signing_committee( sbs.ars_previous_identities, @@ -2416,7 +2416,7 @@ mod tests { assert_eq!(subset, vec![p1.pkh(), p3.pkh()].into_iter().collect()); assert_eq!(usize::try_from(committee_size).unwrap(), subset.len()); - sbs.ars_previous_identities = ars_identities; + sbs.ars_previous_identities = validator_identities; let committee_size = 2; let subset_2 = calculate_superblock_signing_committee( sbs.ars_previous_identities.clone(), @@ -2568,12 +2568,12 @@ mod tests { #[test] fn test_get_beacon_2() { let superblock_state = SuperBlockState { - ars_current_identities: ARSIdentities::default(), + ars_current_identities: ValidatorIdentities::default(), current_superblock_beacon: CheckpointBeacon { checkpoint: 0, hash_prev_block: Hash::SHA256([1; 32]), }, - ars_previous_identities: ARSIdentities::default(), + ars_previous_identities: ValidatorIdentities::default(), ..Default::default() }; let beacon = superblock_state.get_beacon(); @@ -2590,12 +2590,12 @@ mod tests { #[test] fn test_get_beacon_3() { let superblock_state = SuperBlockState { - ars_current_identities: ARSIdentities::default(), + ars_current_identities: ValidatorIdentities::default(), current_superblock_beacon: CheckpointBeacon { checkpoint: 1, hash_prev_block: Hash::default(), }, - ars_previous_identities: ARSIdentities::default(), + ars_previous_identities: ValidatorIdentities::default(), ..Default::default() }; let beacon = superblock_state.get_beacon(); @@ -2645,9 +2645,9 @@ mod tests { create_bn256(4), create_bn256(5), ]; - let ars0 = ARSIdentities::new(vec![]); - let ars1 = ARSIdentities::new(pkhs.clone()); - let ars2 = ARSIdentities::new(pkhs.clone()); + let ars0 = ValidatorIdentities::new(vec![]); + let ars1 = ValidatorIdentities::new(pkhs.clone()); + let ars2 = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index bb88785122..99d17efbd3 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -61,7 +61,7 @@ use witnet_data_structures::{ penalize_factor, priority::{Priorities, PriorityEngine, PriorityVisitor}, reputation_issuance, - tapi::{after_second_hard_fork, current_active_wips, in_emergency_period, ActiveWips}, + tapi::{after_second_hard_fork, current_active_wips, ActiveWips}, Alpha, AltKeys, Block, BlockHeader, Bn256PublicKey, ChainImport, ChainInfo, ChainState, CheckpointBeacon, CheckpointVRF, ConsensusConstants, DataRequestInfo, DataRequestOutput, DataRequestStage, Epoch, EpochConstants, Hash, Hashable, InventoryEntry, InventoryItem, @@ -72,7 +72,7 @@ use witnet_data_structures::{ get_environment, radon_report::{RadonReport, ReportContext}, staking::prelude::*, - superblock::{ARSIdentities, AddSuperBlockVote, SuperBlockConsensus}, + superblock::{AddSuperBlockVote, SuperBlockConsensus, ValidatorIdentities}, transaction::{RevealTransaction, TallyTransaction, Transaction}, types::{ visitor::{StatefulVisitor, Visitor}, @@ -980,7 +980,8 @@ impl ChainManager { // Only update active epoch of honest validators // If there was a liar or error committer, the last output is the creator of the data request // This is not guaranteed to be a validator, so do not update its active epoch - let data_requester_output = ta_tx.out_of_consensus.len() + ta_tx.error_committers.len() > 0; + let data_requester_output = + ta_tx.out_of_consensus.len() + ta_tx.error_committers.len() > 0; for (i, output) in ta_tx.outputs.iter().enumerate() { if data_requester_output && i == ta_tx.outputs.len() - 1 { break; @@ -1894,30 +1895,11 @@ impl ChainManager { } let chain_info = act.chain_state.chain_info.as_ref().unwrap(); - let reputation_engine = act.chain_state.reputation_engine.as_ref().unwrap(); let last_superblock_signed_by_bootstrap = last_superblock_signed_by_bootstrap(&chain_info.consensus_constants); - let ars_members = - // Before reaching the epoch activity_period + collateral_age the bootstrap committee signs the superblock - // collateral_age is measured in blocks instead of epochs, but this only means that the period in which - // the bootstrap committee signs is at least epoch activity_period + collateral_age - if let Some(ars_members) = in_emergency_period(superblock_index, get_environment()) { - // Bootstrap committee - ars_members - } else if superblock_index >= last_superblock_signed_by_bootstrap { - reputation_engine.get_rep_ordered_ars_list() - } else { - chain_info - .consensus_constants - .bootstrapping_committee - .iter() - .map(|add| add.parse().expect("Malformed bootstrapping committee")) - .collect() - }; - - // Get the list of members of the ARS with reputation greater than 0 - // the list itself is ordered by decreasing reputation - let ars_identities = ARSIdentities::new(ars_members); + // Get the list of validators sorted by most recently active + let validators = act.chain_state.stakes.get_active_validators(block_epoch - 2000); + let ars_identities = ValidatorIdentities::new(validators); // After the second hard fork, the superblock committee size must be at least 50 let min_committee_size = if after_second_hard_fork(block_epoch, get_environment()) {