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/83] 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 0b83c5cc9..6a991c7d2 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 000000000..44d2c5127 --- /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/83] 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 264649bcd..29e57caeb 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 000000000..8bbee63f8 --- /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 000000000..80cd8257b --- /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 6a991c7d2..e14acd6e5 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 000000000..424158164 --- /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 000000000..d461b0560 --- /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 000000000..6169073f4 --- /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 44d2c5127..1a5b21418 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 000000000..e69de29bb diff --git a/data_structures/src/staking/stake.rs b/data_structures/src/staking/stake.rs new file mode 100644 index 000000000..38fff6ae4 --- /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 000000000..13d0896d2 --- /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/83] 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 8d309b31e..05e14a689 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 8f905f885..bc9e37656 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 18e807352..5c190877a 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 c627f84f7..07f430d66 100644 --- a/data_structures/src/superblock.rs +++ b/data_structures/src/superblock.rs @@ -806,6 +806,7 @@ mod tests { commit_hash_merkle_root: default_hash, reveal_hash_merkle_root: default_hash, tally_hash_merkle_root: tally_merkle_root_1, + stake_hash_merkle_root: default_hash, }, proof: default_proof, bn256_public_key: None, @@ -855,6 +856,7 @@ mod tests { commit_hash_merkle_root: default_hash, reveal_hash_merkle_root: default_hash, tally_hash_merkle_root: tally_merkle_root_1, + stake_hash_merkle_root: default_hash, }, proof: default_proof.clone(), bn256_public_key: None, @@ -870,6 +872,7 @@ mod tests { commit_hash_merkle_root: default_hash, reveal_hash_merkle_root: default_hash, tally_hash_merkle_root: tally_merkle_root_2, + stake_hash_merkle_root: default_hash, }, proof: default_proof, bn256_public_key: None, diff --git a/data_structures/src/transaction.rs b/data_structures/src/transaction.rs index 4c98820e6..5ac3054d1 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 31296b267..5c835096d 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 fa6b0cd1b..04370407e 100644 --- a/data_structures/src/types.rs +++ b/data_structures/src/types.rs @@ -75,6 +75,7 @@ impl fmt::Display for Command { Transaction::Reveal(_) => f.write_str("REVEAL_TRANSACTION")?, Transaction::Tally(_) => f.write_str("TALLY_TRANSACTION")?, Transaction::Mint(_) => f.write_str("MINT_TRANSACTION")?, + Transaction::Stake(_) => f.write_str("STAKE_TRANSACTION")?, } write!(f, ": {}", tx.hash()) } diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 231bdfb9c..e57632750 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -839,6 +839,8 @@ pub fn build_block( let mut value_transfer_txns = Vec::new(); let mut data_request_txns = Vec::new(); let mut tally_txns = Vec::new(); + // TODO: handle stake tx + let stake_txns = Vec::new(); let min_vt_weight = VTTransactionBody::new(vec![Input::default()], vec![ValueTransferOutput::default()]) @@ -1000,6 +1002,7 @@ pub fn build_block( let commit_hash_merkle_root = merkle_tree_root(&commit_txns); let reveal_hash_merkle_root = merkle_tree_root(&reveal_txns); let tally_hash_merkle_root = merkle_tree_root(&tally_txns); + let stake_hash_merkle_root = merkle_tree_root(&stake_txns); let merkle_roots = BlockMerkleRoots { mint_hash: mint.hash(), vt_hash_merkle_root, @@ -1007,6 +1010,7 @@ pub fn build_block( commit_hash_merkle_root, reveal_hash_merkle_root, tally_hash_merkle_root, + stake_hash_merkle_root, }; let block_header = BlockHeader { @@ -1024,6 +1028,7 @@ pub fn build_block( commit_txns, reveal_txns, tally_txns, + stake_txns, }; (block_header, txns) diff --git a/schemas/witnet/witnet.proto b/schemas/witnet/witnet.proto index 64b1b04e0..d08283acb 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 eb00b2011..6cf51b7b3 100644 --- a/validations/Cargo.toml +++ b/validations/Cargo.toml @@ -8,7 +8,7 @@ workspace = ".." [dependencies] failure = "0.1.8" -itertools = "0.8.2" +itertools = "0.11.0" log = "0.4.8" url = "2.2.2" diff --git a/validations/src/tests/mod.rs b/validations/src/tests/mod.rs index b14bb19c3..c1e27ae9a 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 2ed59872c..28ab20029 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 ec8ac954b..35ef8bdba 100644 --- a/wallet/src/model.rs +++ b/wallet/src/model.rs @@ -187,6 +187,8 @@ pub enum TransactionData { Mint(MintData), #[serde(rename = "commit")] Commit(VtData), + // #[serde(rename = "stake")] + // Stake(StakeData), } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] diff --git a/wallet/src/repository/wallet/mod.rs b/wallet/src/repository/wallet/mod.rs index 451586ad0..20e9f826a 100644 --- a/wallet/src/repository/wallet/mod.rs +++ b/wallet/src/repository/wallet/mod.rs @@ -1490,6 +1490,7 @@ where Transaction::Reveal(_) => None, Transaction::Tally(_) => None, Transaction::Mint(_) => None, + Transaction::Stake(tx) => Some(&tx.body.inputs), }; let empty_hashset = HashSet::default(); diff --git a/wallet/src/types.rs b/wallet/src/types.rs index 84c9a9513..63924f646 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -22,7 +22,7 @@ use witnet_data_structures::{ fee::Fee, transaction::{ CommitTransaction, DRTransaction, DRTransactionBody, MintTransaction, RevealTransaction, - TallyTransaction, Transaction, VTTransaction, VTTransactionBody, + StakeTransaction, TallyTransaction, Transaction, VTTransaction, VTTransactionBody, }, utxo_pool::UtxoSelectionStrategy, }; @@ -322,6 +322,7 @@ pub enum TransactionHelper { Reveal(RevealTransaction), Tally(TallyTransaction), Mint(MintTransaction), + Stake(StakeTransaction), } impl From for TransactionHelper { @@ -337,6 +338,7 @@ impl From for TransactionHelper { Transaction::Reveal(revealtransaction) => TransactionHelper::Reveal(revealtransaction), Transaction::Tally(tallytransaction) => TransactionHelper::Tally(tallytransaction), Transaction::Mint(minttransaction) => TransactionHelper::Mint(minttransaction), + Transaction::Stake(staketransaction) => TransactionHelper::Stake(staketransaction), } } } @@ -354,6 +356,7 @@ impl From for Transaction { TransactionHelper::Reveal(revealtransaction) => Transaction::Reveal(revealtransaction), TransactionHelper::Tally(tallytransaction) => Transaction::Tally(tallytransaction), TransactionHelper::Mint(minttransaction) => Transaction::Mint(minttransaction), + TransactionHelper::Stake(staketransaction) => Transaction::Stake(staketransaction), } } } From 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/83] 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 f90390484..6d7d92482 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 5f8293444..478d114d2 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 bc9e37656..81592e294 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 fc3823037..4e74ad0b8 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 5ac3054d1..3d8826c1d 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 5c835096d..94acf3a67 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 bf8c975b1..e3f490d8e 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 e57632750..481485eff 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 a408bc48d..95992fa25 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 c1e27ae9a..1c43c1971 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 28ab20029..4d590a760 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 20e9f826a..b2e469e5a 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 63924f646..8dd1d59d2 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/83] 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 81592e294..d0f8530cf 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 5c190877a..ee2471dbf 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 07f430d66..46f8d1706 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 3d8826c1d..e300ddfc9 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 94acf3a67..1731e1d23 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 04370407e..6feda901e 100644 --- a/data_structures/src/types.rs +++ b/data_structures/src/types.rs @@ -76,6 +76,7 @@ impl fmt::Display for Command { Transaction::Tally(_) => f.write_str("TALLY_TRANSACTION")?, Transaction::Mint(_) => f.write_str("MINT_TRANSACTION")?, Transaction::Stake(_) => f.write_str("STAKE_TRANSACTION")?, + Transaction::Unstake(_) => f.write_str("UNSTAKE_TRANSACTION")?, } write!(f, ": {}", tx.hash()) } diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 481485eff..1263987b5 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -841,6 +841,8 @@ pub fn build_block( let mut tally_txns = Vec::new(); // TODO: handle stake tx let stake_txns = Vec::new(); + // TODO: handle unstake tx + let unstake_txns = Vec::new(); let min_vt_weight = VTTransactionBody::new(vec![Input::default()], vec![ValueTransferOutput::default()]) @@ -1003,6 +1005,7 @@ pub fn build_block( let reveal_hash_merkle_root = merkle_tree_root(&reveal_txns); let tally_hash_merkle_root = merkle_tree_root(&tally_txns); let stake_hash_merkle_root = merkle_tree_root(&stake_txns); + let unstake_hash_merkle_root = merkle_tree_root(&unstake_txns); let merkle_roots = BlockMerkleRoots { mint_hash: mint.hash(), vt_hash_merkle_root, @@ -1011,6 +1014,7 @@ pub fn build_block( reveal_hash_merkle_root, tally_hash_merkle_root, stake_hash_merkle_root, + unstake_hash_merkle_root, }; let block_header = BlockHeader { @@ -1029,6 +1033,7 @@ pub fn build_block( reveal_txns, tally_txns, stake_txns, + unstake_txns, }; (block_header, txns) diff --git a/schemas/witnet/witnet.proto b/schemas/witnet/witnet.proto index d08283acb..237e472ae 100644 --- a/schemas/witnet/witnet.proto +++ b/schemas/witnet/witnet.proto @@ -60,6 +60,7 @@ message Block { Hash reveal_hash_merkle_root = 5; Hash tally_hash_merkle_root = 6; Hash stake_hash_merkle_root = 7; + Hash unstake_hash_merkle_root = 8; } uint32 signals = 1; CheckpointBeacon beacon = 2; @@ -75,6 +76,7 @@ message Block { repeated RevealTransaction reveal_txns = 5; repeated TallyTransaction tally_txns = 6; repeated StakeTransaction stake_txns = 7; + repeated UnstakeTransaction unstake_txns = 8; } BlockHeader block_header = 1; @@ -247,6 +249,17 @@ message StakeTransaction { repeated KeyedSignature signatures = 2; } +message UnstakeTransactionBody { + PublicKeyHash operator = 1; + ValueTransferOutput withdrawal = 2; + ValueTransferOutput change = 3; +} + +message UnstakeTransaction { + UnstakeTransactionBody body = 1 ; + KeyedSignature signature = 2; +} + message Transaction { oneof kind { VTTransaction ValueTransfer = 1; @@ -256,6 +269,7 @@ message Transaction { TallyTransaction Tally = 5; MintTransaction Mint = 6; StakeTransaction Stake = 7; + UnstakeTransaction Unstake = 8; } } diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 4d590a760..aae6c7afd 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 b2e469e5a..958feaea0 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 8dd1d59d2..3f8e73a29 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/83] 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 d0f8530cf..fd06513c5 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 4d681d8b9..6fc6ba0e5 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 46f8d1706..bfc46ae47 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 57ff40c32..047594534 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 2b4ef28c7..28f5bfde7 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 bc2c48602..f9b0e7c2d 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 237e472ae..f110eda14 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/83] 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 fd06513c5..168d0459d 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 95992fa25..afd3ee527 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 aae6c7afd..508532aa4 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/83] 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 9b127ae2f..c7aafa6e8 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/83] 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 168d0459d..31391ea01 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 e14acd6e5..c57f2386a 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 bfc46ae47..39a5a8da9 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 6feda901e..10afcc31c 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 28f5bfde7..8de51b1e5 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 e45a46bcb..3bc9e3060 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 f9b0e7c2d..47e7379f4 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 508532aa4..14c49c115 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/83] 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 000000000..622d9c920 --- /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/83] 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 3bc9e3060..95d19597d 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 afd3ee527..a31030b19 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 d11776e2b..1c2a6ab21 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/83] 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 05e14a689..f4af2675a 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 216d3f6ae..826fd49c8 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 31391ea01..6d752a6a0 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 a31030b19..4d8d9982a 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 1c2a6ab21..fa78be182 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 c1a9ee6c9..2a8b6ae3f 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 41321f29c..408ef70a0 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/83] 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 a1831753a..2b65b8da2 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 6d752a6a0..1a69aa3f0 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 4d8d9982a..7817931e1 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 fa78be182..2cb819af9 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 2a8b6ae3f..0b6b7cdd6 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/83] 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 1a69aa3f0..028f64259 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 c57f2386a..c43328c40 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 622d9c920..d0e76f8a5 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 39a5a8da9..8557596ed 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 8de51b1e5..df5467e6d 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 1263987b5..0f3724560 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 47e7379f4..d668929f7 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 14c49c115..30d33d009 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/83] 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 1c43c1971..4e395494a 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/83] 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 f0c5c6303..a4984d8b7 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 2a7eacde1..44b9b6a9e 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 c81da9eef..f0ccdc105 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/83] 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 f4af2675a..41a3ce131 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 2b65b8da2..4255422f7 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 9d99dc546..5e1def9e4 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 5da876b73..c980c47af 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 028f64259..cab521ca1 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 ee2471dbf..754d2f7e4 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 c43328c40..5524270a0 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 d0e76f8a5..3a441193d 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 95d19597d..ec28d633f 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 0f3724560..1263987b5 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 d668929f7..47e7379f4 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 7817931e1..0c104b1d0 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 2cb819af9..7cf75f511 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 0b6b7cdd6..d74e65203 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 408ef70a0..567e32f91 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 30d33d009..a566a6934 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/83] 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 41a3ce131..ad887b104 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 72e68f2d6..dbc5fc5eb 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 6d7d92482..7d0576316 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 29e57caeb..a8e0c4f39 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 cab521ca1..8ce6b3448 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 754d2f7e4..24dda7e78 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 5524270a0..67365e300 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 3a441193d..244c52037 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 0c104b1d0..9fae1df67 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 bf8ebd663..2e1eedc44 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 d74e65203..e35ef9c3a 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/83] 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 8bbee63f8..d2d96edc3 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 424158164..65543d05d 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 6169073f4..0c4bc69a3 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 e69de29bb..000000000 diff --git a/data_structures/src/staking/stakes.rs b/data_structures/src/staking/stakes.rs index 13d0896d2..29d5e7275 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/83] 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 1263987b5..4235664a8 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/83] 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 4235664a8..69560baee 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 e35ef9c3a..b022a14b4 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/83] 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 a566a6934..01721f807 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/83] 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 8ce6b3448..f37d06aa6 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 01721f807..7d99da6fd 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/83] 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 f37d06aa6..c8250f45c 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 7d99da6fd..3a8b5247a 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 268daf6cf..4904d775b 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 cf7fc4652..5982fd277 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 7309b428e..35f060ae5 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 c29a7a3f2..f5ac6370b 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 2d4275674..98bfeb3ee 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 958feaea0..5fde718d0 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 3f8e73a29..4286895ee 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/83] 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 ad887b104..4ecf42300 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 80cd8257b..c3bee6efb 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 c8250f45c..cad5d6362 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 65543d05d..3a6c706ea 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 38fff6ae4..4752c5e13 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 29d5e7275..192bec52f 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 47e7379f4..789d5a3dc 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 5982fd277..d11b42dbd 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 35f060ae5..24a7e990f 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/83] 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 2a85b75c3..576450cb4 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 cad5d6362..a92645ce6 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 3a6c706ea..2b4da95d9 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 0c4bc69a3..7b270a92e 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 4752c5e13..0915df2b6 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 192bec52f..61385c415 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 e300ddfc9..4c838eaa7 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 d9066550e..c9974003b 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 5187188d5..b37a4968d 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 789d5a3dc..6dce3733d 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 9fae1df67..417bb0af6 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 f110eda14..2d6d83f3a 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/83] 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 dbc5fc5eb..d18e5e8cf 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 a92645ce6..ea6d43792 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 2b4da95d9..6898f8ecb 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 61385c415..426df952f 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 69560baee..bd66d510e 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 6dce3733d..02d822f55 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 3a8b5247a..a0595d3c4 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 24a7e990f..a7fb1d283 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/83] 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 bd66d510e..ab08278c1 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 7b6484104..d2a798372 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/83] 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 6898f8ecb..1ebb5c28c 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 7b270a92e..869536de3 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 0915df2b6..ea1926da9 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 426df952f..a5e22a6f8 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 ec28d633f..eb489c60a 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 417bb0af6..76c4a1d6c 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 7cf75f511..fe605488d 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/83] 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 b022a14b4..cd345e139 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 567e32f91..0a45cbd3a 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 ed093ae2f797fc33b2466378dd1cb0ffe09667b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Tue, 27 Feb 2024 16:27:26 +0100 Subject: [PATCH 31/83] feat(validations): create eligibility module and isolate legacy mining logic --- node/src/actors/chain_manager/mining.rs | 23 ++- node/src/actors/chain_manager/mod.rs | 9 +- validations/src/eligibility/legacy.rs | 232 +++++++++++++++++++++++ validations/src/eligibility/mod.rs | 6 + validations/src/eligibility/pos.rs | 1 + validations/src/lib.rs | 5 +- validations/src/validations.rs | 233 +----------------------- 7 files changed, 264 insertions(+), 245 deletions(-) create mode 100644 validations/src/eligibility/legacy.rs create mode 100644 validations/src/eligibility/mod.rs create mode 100644 validations/src/eligibility/pos.rs diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index ab08278c1..4d6c8ec90 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,7 +14,7 @@ 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_POS_MAX_STAKE_BLOCK_WEIGHT, @@ -22,9 +22,9 @@ use witnet_config::defaults::{ }; 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::{ @@ -50,13 +50,16 @@ 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::{ - block_reward, calculate_liars_and_errors_count_from_tally, calculate_mining_probability, - calculate_randpoe_threshold, calculate_reppoe_threshold, dr_transaction_fee, merkle_tree_root, - st_transaction_fee, tally_bytes_on_encode_error, update_utxo_diff, vt_transaction_fee, +use witnet_validations::{ + eligibility::legacy::*, + validations::{ + block_reward, calculate_liars_and_errors_count_from_tally, dr_transaction_fee, + merkle_tree_root, st_transaction_fee, tally_bytes_on_encode_error, update_utxo_diff, + vt_transaction_fee, + }, }; use crate::{ diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index 02d822f55..db09d8b59 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -83,9 +83,12 @@ use witnet_data_structures::{ }; use witnet_rad::types::RadonTypes; use witnet_util::timestamp::seconds_to_human_string; -use witnet_validations::validations::{ - compare_block_candidates, validate_block, validate_block_transactions, - validate_new_transaction, validate_rad_request, verify_signatures, VrfSlots, +use witnet_validations::{ + eligibility::legacy::VrfSlots, + validations::{ + compare_block_candidates, validate_block, validate_block_transactions, + validate_new_transaction, validate_rad_request, verify_signatures, + }, }; use crate::{ diff --git a/validations/src/eligibility/legacy.rs b/validations/src/eligibility/legacy.rs new file mode 100644 index 000000000..23a0a55ba --- /dev/null +++ b/validations/src/eligibility/legacy.rs @@ -0,0 +1,232 @@ +use witnet_data_structures::chain::{tapi::ActiveWips, Hash, PublicKeyHash, ReputationEngine}; + +/// Calculate the target hash needed to create a valid VRF proof of eligibility used for block +/// mining. +pub fn calculate_randpoe_threshold( + total_identities: u32, + replication_factor: u32, + block_epoch: u32, + minimum_difficulty: u32, + epochs_with_minimum_difficulty: u32, + active_wips: &ActiveWips, +) -> (Hash, f64) { + let max = u64::max_value(); + let minimum_difficulty = std::cmp::max(1, minimum_difficulty); + let target = if block_epoch <= epochs_with_minimum_difficulty { + max / u64::from(minimum_difficulty) + } else if active_wips.wips_0009_0011_0012() { + let difficulty = std::cmp::max(total_identities, minimum_difficulty); + (max / u64::from(difficulty)).saturating_mul(u64::from(replication_factor)) + } else { + let difficulty = std::cmp::max(1, total_identities); + (max / u64::from(difficulty)).saturating_mul(u64::from(replication_factor)) + }; + let target = u32::try_from(target >> 32).unwrap(); + + let probability = f64::from(target) / f64::from(u32::try_from(max >> 32).unwrap()); + (Hash::with_first_u32(target), probability) +} + +/// Calculate the target hash needed to create a valid VRF proof of eligibility used for data +/// request witnessing. +pub fn calculate_reppoe_threshold( + rep_eng: &ReputationEngine, + pkh: &PublicKeyHash, + num_witnesses: u16, + minimum_difficulty: u32, + active_wips: &ActiveWips, +) -> (Hash, f64) { + // Set minimum total_active_reputation to 1 to avoid division by zero + let total_active_rep = std::cmp::max(rep_eng.total_active_reputation(), 1); + // Add 1 to reputation because otherwise a node with 0 reputation would + // never be eligible for a data request + let my_eligibility = u64::from(rep_eng.get_eligibility(pkh)) + 1; + + let max = u64::max_value(); + // Compute target eligibility and hard-cap it if required + let target = if active_wips.wip0016() { + let factor = u64::from(num_witnesses); + (max / std::cmp::max(total_active_rep, u64::from(minimum_difficulty))) + .saturating_mul(my_eligibility) + .saturating_mul(factor) + } else if active_wips.third_hard_fork() { + let factor = u64::from(rep_eng.threshold_factor(num_witnesses)); + // Eligibility must never be greater than (max/minimum_difficulty) + std::cmp::min( + max / u64::from(minimum_difficulty), + (max / total_active_rep).saturating_mul(my_eligibility), + ) + .saturating_mul(factor) + } else { + let factor = u64::from(rep_eng.threshold_factor(num_witnesses)); + // Check for overflow: when the probability is more than 100%, cap it to 100% + (max / total_active_rep) + .saturating_mul(my_eligibility) + .saturating_mul(factor) + }; + let target = u32::try_from(target >> 32).unwrap(); + + let probability = f64::from(target) / f64::from(u32::try_from(max >> 32).unwrap()); + (Hash::with_first_u32(target), probability) +} + +/// Used to classify VRF hashes into slots. +/// +/// When trying to mine a block, the node considers itself eligible if the hash of the VRF is lower +/// than `calculate_randpoe_threshold(total_identities, rf, 1001,0,0)` with `rf = mining_backup_factor`. +/// +/// However, in order to consolidate a block, the nodes choose the best block that is valid under +/// `rf = mining_replication_factor`. If there is no valid block within that range, it retries with +/// increasing values of `rf`. For example, with `mining_backup_factor = 4` and +/// `mining_replication_factor = 8`, there are 5 different slots: +/// `rf = 4, rf = 5, rf = 6, rf = 7, rf = 8`. Blocks in later slots can only be better candidates +/// if the previous slots have zero valid blocks. +#[derive(Clone, Debug, Default)] +pub struct VrfSlots { + target_hashes: Vec, +} + +impl VrfSlots { + /// Create new list of slots with the given target hashes. + /// + /// `target_hashes` must be sorted + pub fn new(target_hashes: Vec) -> Self { + Self { target_hashes } + } + + /// Create new list of slots with the given parameters + pub fn from_rf( + total_identities: u32, + replication_factor: u32, + backup_factor: u32, + block_epoch: u32, + minimum_difficulty: u32, + epochs_with_minimum_difficulty: u32, + active_wips: &ActiveWips, + ) -> Self { + Self::new( + (replication_factor..=backup_factor) + .map(|rf| { + calculate_randpoe_threshold( + total_identities, + rf, + block_epoch, + minimum_difficulty, + epochs_with_minimum_difficulty, + active_wips, + ) + .0 + }) + .collect(), + ) + } + + /// Return the slot number that contains the given hash + pub fn slot(&self, hash: &Hash) -> u32 { + let num_sections = self.target_hashes.len(); + u32::try_from( + self.target_hashes + .iter() + // The section is the index of the first section hash that is less + // than or equal to the provided hash + .position(|th| hash <= th) + // If the provided hash is greater than all of the section hashes, + // return the number of sections + .unwrap_or(num_sections), + ) + .unwrap() + } + + /// Return the target hash for each slot + pub fn target_hashes(&self) -> &[Hash] { + &self.target_hashes + } +} + +#[allow(clippy::many_single_char_names)] +fn internal_calculate_mining_probability( + rf: u32, + n: f64, + k: u32, // k: iterative rf until reach bf + m: i32, // M: nodes with reputation greater than me + l: i32, // L: nodes with reputation equal than me + r: i32, // R: nodes with reputation less than me +) -> f64 { + if k == rf { + let rf = f64::from(rf); + // Prob to mine is the probability that a node with the same reputation than me mine, + // divided by all the nodes with the same reputation: + // 1/L * (1 - ((N-RF)/N)^L) + let prob_to_mine = (1.0 / f64::from(l)) * (1.0 - ((n - rf) / n).powi(l)); + // Prob that a node with more reputation than me mine is: + // ((N-RF)/N)^M + let prob_greater_neg = ((n - rf) / n).powi(m); + + prob_to_mine * prob_greater_neg + } else { + let k = f64::from(k); + // Here we take into account that rf = 1 because is only a new slot + let prob_to_mine = (1.0 / f64::from(l)) * (1.0 - ((n - 1.0) / n).powi(l)); + // The same equation than before + let prob_bigger_neg = ((n - k) / n).powi(m); + // Prob that a node with less or equal reputation than me mine with a lower slot is: + // ((N+1-RF)/N)^(L+R-1) + let prob_lower_slot_neg = ((n + 1.0 - k) / n).powi(l + r - 1); + + prob_to_mine * prob_bigger_neg * prob_lower_slot_neg + } +} + +/// Calculate the probability that the block candidate proposed by this identity will be the +/// consolidated block selected by the network. +pub fn calculate_mining_probability( + rep_engine: &ReputationEngine, + own_pkh: PublicKeyHash, + rf: u32, + bf: u32, +) -> f64 { + let n = u32::try_from(rep_engine.ars().active_identities_number()).unwrap(); + + // In case of any active node, the probability is maximum + if n == 0 { + return 1.0; + } + + // First we need to know how many nodes have more or equal reputation than us + let own_rep = rep_engine.trs().get(&own_pkh); + let is_active_node = rep_engine.ars().contains(&own_pkh); + let mut greater = 0; + let mut equal = 0; + let mut less = 0; + for &active_id in rep_engine.ars().active_identities() { + let rep = rep_engine.trs().get(&active_id); + match (rep.0 > 0, own_rep.0 > 0) { + (true, false) => greater += 1, + (false, true) => less += 1, + _ => equal += 1, + } + } + // In case of not being active, the equal value is plus 1. + if !is_active_node { + equal += 1; + } + + if rf > n && greater == 0 { + // In case of replication factor exceed the active node number and being the most reputed + // we obtain the maximum probability divided in the nodes we share the same reputation + 1.0 / f64::from(equal) + } else if rf > n && greater > 0 { + // In case of replication factor exceed the active node number and not being the most reputed + // we obtain the minimum probability + 0.0 + } else { + let mut aux = + internal_calculate_mining_probability(rf, f64::from(n), rf, greater, equal, less); + let mut k = rf + 1; + while k <= bf && k <= n { + aux += internal_calculate_mining_probability(rf, f64::from(n), k, greater, equal, less); + k += 1; + } + aux + } +} diff --git a/validations/src/eligibility/mod.rs b/validations/src/eligibility/mod.rs new file mode 100644 index 000000000..a7a9673c3 --- /dev/null +++ b/validations/src/eligibility/mod.rs @@ -0,0 +1,6 @@ +/// Eligibility-related functions specific to former versions of the Witnet protocol. +pub mod legacy; + +/// Eligibility-related functions specific to the latest Proof-of-Stake version of the Witnet +/// protocol. +pub mod pos; diff --git a/validations/src/eligibility/pos.rs b/validations/src/eligibility/pos.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/validations/src/eligibility/pos.rs @@ -0,0 +1 @@ + diff --git a/validations/src/lib.rs b/validations/src/lib.rs index 01422f6e7..930efff2d 100644 --- a/validations/src/lib.rs +++ b/validations/src/lib.rs @@ -7,11 +7,14 @@ #![deny(unused_mut)] #![deny(missing_docs)] -/// Module containing validations +/// Module containing general purpose validations pub mod validations; /// Module containing validations specific to witnessing pub mod witnessing; +/// Module contaning validations specific to eligibility +pub mod eligibility; + #[cfg(test)] mod tests; diff --git a/validations/src/validations.rs b/validations/src/validations.rs index a0595d3c4..8ce16df2e 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -55,6 +55,8 @@ use witnet_rad::{ types::{serial_iter_decode, RadonTypes}, }; +use crate::eligibility::legacy::*; + // TODO: move to a configuration const MAX_STAKE_BLOCK_WEIGHT: u32 = 10_000_000; const MIN_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS; @@ -2191,237 +2193,6 @@ pub fn validate_new_transaction( } } -/// Calculate the target hash needed to create a valid VRF proof of eligibility used for block -/// mining. -pub fn calculate_randpoe_threshold( - total_identities: u32, - replication_factor: u32, - block_epoch: u32, - minimum_difficulty: u32, - epochs_with_minimum_difficulty: u32, - active_wips: &ActiveWips, -) -> (Hash, f64) { - let max = u64::max_value(); - let minimum_difficulty = std::cmp::max(1, minimum_difficulty); - let target = if block_epoch <= epochs_with_minimum_difficulty { - max / u64::from(minimum_difficulty) - } else if active_wips.wips_0009_0011_0012() { - let difficulty = std::cmp::max(total_identities, minimum_difficulty); - (max / u64::from(difficulty)).saturating_mul(u64::from(replication_factor)) - } else { - let difficulty = std::cmp::max(1, total_identities); - (max / u64::from(difficulty)).saturating_mul(u64::from(replication_factor)) - }; - let target = u32::try_from(target >> 32).unwrap(); - - let probability = f64::from(target) / f64::from(u32::try_from(max >> 32).unwrap()); - (Hash::with_first_u32(target), probability) -} - -/// Calculate the target hash needed to create a valid VRF proof of eligibility used for data -/// request witnessing. -pub fn calculate_reppoe_threshold( - rep_eng: &ReputationEngine, - pkh: &PublicKeyHash, - num_witnesses: u16, - minimum_difficulty: u32, - active_wips: &ActiveWips, -) -> (Hash, f64) { - // Set minimum total_active_reputation to 1 to avoid division by zero - let total_active_rep = std::cmp::max(rep_eng.total_active_reputation(), 1); - // Add 1 to reputation because otherwise a node with 0 reputation would - // never be eligible for a data request - let my_eligibility = u64::from(rep_eng.get_eligibility(pkh)) + 1; - - let max = u64::max_value(); - // Compute target eligibility and hard-cap it if required - let target = if active_wips.wip0016() { - let factor = u64::from(num_witnesses); - (max / std::cmp::max(total_active_rep, u64::from(minimum_difficulty))) - .saturating_mul(my_eligibility) - .saturating_mul(factor) - } else if active_wips.third_hard_fork() { - let factor = u64::from(rep_eng.threshold_factor(num_witnesses)); - // Eligibility must never be greater than (max/minimum_difficulty) - std::cmp::min( - max / u64::from(minimum_difficulty), - (max / total_active_rep).saturating_mul(my_eligibility), - ) - .saturating_mul(factor) - } else { - let factor = u64::from(rep_eng.threshold_factor(num_witnesses)); - // Check for overflow: when the probability is more than 100%, cap it to 100% - (max / total_active_rep) - .saturating_mul(my_eligibility) - .saturating_mul(factor) - }; - let target = u32::try_from(target >> 32).unwrap(); - - let probability = f64::from(target) / f64::from(u32::try_from(max >> 32).unwrap()); - (Hash::with_first_u32(target), probability) -} - -/// Used to classify VRF hashes into slots. -/// -/// When trying to mine a block, the node considers itself eligible if the hash of the VRF is lower -/// than `calculate_randpoe_threshold(total_identities, rf, 1001,0,0)` with `rf = mining_backup_factor`. -/// -/// However, in order to consolidate a block, the nodes choose the best block that is valid under -/// `rf = mining_replication_factor`. If there is no valid block within that range, it retries with -/// increasing values of `rf`. For example, with `mining_backup_factor = 4` and -/// `mining_replication_factor = 8`, there are 5 different slots: -/// `rf = 4, rf = 5, rf = 6, rf = 7, rf = 8`. Blocks in later slots can only be better candidates -/// if the previous slots have zero valid blocks. -#[derive(Clone, Debug, Default)] -pub struct VrfSlots { - target_hashes: Vec, -} - -impl VrfSlots { - /// Create new list of slots with the given target hashes. - /// - /// `target_hashes` must be sorted - pub fn new(target_hashes: Vec) -> Self { - Self { target_hashes } - } - - /// Create new list of slots with the given parameters - pub fn from_rf( - total_identities: u32, - replication_factor: u32, - backup_factor: u32, - block_epoch: u32, - minimum_difficulty: u32, - epochs_with_minimum_difficulty: u32, - active_wips: &ActiveWips, - ) -> Self { - Self::new( - (replication_factor..=backup_factor) - .map(|rf| { - calculate_randpoe_threshold( - total_identities, - rf, - block_epoch, - minimum_difficulty, - epochs_with_minimum_difficulty, - active_wips, - ) - .0 - }) - .collect(), - ) - } - - /// Return the slot number that contains the given hash - pub fn slot(&self, hash: &Hash) -> u32 { - let num_sections = self.target_hashes.len(); - u32::try_from( - self.target_hashes - .iter() - // The section is the index of the first section hash that is less - // than or equal to the provided hash - .position(|th| hash <= th) - // If the provided hash is greater than all of the section hashes, - // return the number of sections - .unwrap_or(num_sections), - ) - .unwrap() - } - - /// Return the target hash for each slot - pub fn target_hashes(&self) -> &[Hash] { - &self.target_hashes - } -} - -#[allow(clippy::many_single_char_names)] -fn internal_calculate_mining_probability( - rf: u32, - n: f64, - k: u32, // k: iterative rf until reach bf - m: i32, // M: nodes with reputation greater than me - l: i32, // L: nodes with reputation equal than me - r: i32, // R: nodes with reputation less than me -) -> f64 { - if k == rf { - let rf = f64::from(rf); - // Prob to mine is the probability that a node with the same reputation than me mine, - // divided by all the nodes with the same reputation: - // 1/L * (1 - ((N-RF)/N)^L) - let prob_to_mine = (1.0 / f64::from(l)) * (1.0 - ((n - rf) / n).powi(l)); - // Prob that a node with more reputation than me mine is: - // ((N-RF)/N)^M - let prob_greater_neg = ((n - rf) / n).powi(m); - - prob_to_mine * prob_greater_neg - } else { - let k = f64::from(k); - // Here we take into account that rf = 1 because is only a new slot - let prob_to_mine = (1.0 / f64::from(l)) * (1.0 - ((n - 1.0) / n).powi(l)); - // The same equation than before - let prob_bigger_neg = ((n - k) / n).powi(m); - // Prob that a node with less or equal reputation than me mine with a lower slot is: - // ((N+1-RF)/N)^(L+R-1) - let prob_lower_slot_neg = ((n + 1.0 - k) / n).powi(l + r - 1); - - prob_to_mine * prob_bigger_neg * prob_lower_slot_neg - } -} - -/// Calculate the probability that the block candidate proposed by this identity will be the -/// consolidated block selected by the network. -pub fn calculate_mining_probability( - rep_engine: &ReputationEngine, - own_pkh: PublicKeyHash, - rf: u32, - bf: u32, -) -> f64 { - let n = u32::try_from(rep_engine.ars().active_identities_number()).unwrap(); - - // In case of any active node, the probability is maximum - if n == 0 { - return 1.0; - } - - // First we need to know how many nodes have more or equal reputation than us - let own_rep = rep_engine.trs().get(&own_pkh); - let is_active_node = rep_engine.ars().contains(&own_pkh); - let mut greater = 0; - let mut equal = 0; - let mut less = 0; - for &active_id in rep_engine.ars().active_identities() { - let rep = rep_engine.trs().get(&active_id); - match (rep.0 > 0, own_rep.0 > 0) { - (true, false) => greater += 1, - (false, true) => less += 1, - _ => equal += 1, - } - } - // In case of not being active, the equal value is plus 1. - if !is_active_node { - equal += 1; - } - - if rf > n && greater == 0 { - // In case of replication factor exceed the active node number and being the most reputed - // we obtain the maximum probability divided in the nodes we share the same reputation - 1.0 / f64::from(equal) - } else if rf > n && greater > 0 { - // In case of replication factor exceed the active node number and not being the most reputed - // we obtain the minimum probability - 0.0 - } else { - let mut aux = - internal_calculate_mining_probability(rf, f64::from(n), rf, greater, equal, less); - let mut k = rf + 1; - while k <= bf && k <= n { - aux += internal_calculate_mining_probability(rf, f64::from(n), k, greater, equal, less); - k += 1; - } - aux - } -} - /// Function to calculate a merkle tree from a transaction vector pub fn merkle_tree_root(transactions: &[T]) -> Hash where From 508bb00ebf8f5945d3084135f778d69b13740a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Mon, 18 Mar 2024 12:35:35 +0100 Subject: [PATCH 32/83] feat(validations): implement new eligibility functions --- Cargo.lock | 1 + data_structures/src/staking/aux.rs | 51 +++- data_structures/src/staking/stake.rs | 4 +- data_structures/src/staking/stakes.rs | 18 +- node/src/actors/chain_manager/mining.rs | 12 +- src/cli/mod.rs | 1 + validations/Cargo.toml | 1 + validations/src/eligibility/current.rs | 270 ++++++++++++++++++ validations/src/eligibility/mod.rs | 2 +- validations/src/eligibility/pos.rs | 1 - .../src/tests/compare_block_candidates.rs | 2 +- validations/src/tests/mod.rs | 6 +- validations/src/tests/randpoe.rs | 2 +- validations/src/tests/reppoe.rs | 2 +- 14 files changed, 348 insertions(+), 25 deletions(-) create mode 100644 validations/src/eligibility/current.rs delete mode 100644 validations/src/eligibility/pos.rs diff --git a/Cargo.lock b/Cargo.lock index 4ecf42300..94f549a6d 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -5482,6 +5482,7 @@ dependencies = [ "hex", "itertools 0.11.0", "log 0.4.20", + "num-traits", "url", "witnet_config", "witnet_crypto", diff --git a/data_structures/src/staking/aux.rs b/data_structures/src/staking/aux.rs index 1ebb5c28c..c32ea04cf 100644 --- a/data_structures/src/staking/aux.rs +++ b/data_structures/src/staking/aux.rs @@ -2,23 +2,23 @@ use std::fmt::{Debug, Display, Formatter}; use std::{rc::Rc, str::FromStr, sync::RwLock}; use failure::Error; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::{chain::PublicKeyHash, proto::ProtobufConvert}; -use super::prelude::*; +use crate::staking::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>; +pub type StakesResult = 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)] +#[derive(Clone, Debug, Default)] pub struct SyncStake where Address: Default, @@ -28,6 +28,20 @@ where pub value: Rc>>, } +impl From> + for SyncStake +where + Address: Default, + Epoch: Default, +{ + #[inline] + fn from(value: Stake) -> Self { + SyncStake { + value: Rc::new(RwLock::new(value)), + } + } +} + impl PartialEq for SyncStake where Address: Default, @@ -42,6 +56,35 @@ where } } +impl<'de, Address, Coins, Epoch, Power> Deserialize<'de> for SyncStake +where + Address: Default, + Epoch: Default, + Stake: Deserialize<'de>, +{ + #[inline] + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + >::deserialize(deserializer).map(SyncStake::from) + } +} + +impl Serialize for SyncStake +where + Address: Default, + Epoch: Default, + Stake: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.value.read().unwrap().serialize(serializer) + } +} + /// 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, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] diff --git a/data_structures/src/staking/stake.rs b/data_structures/src/staking/stake.rs index ea1926da9..3f58e40be 100644 --- a/data_structures/src/staking/stake.rs +++ b/data_structures/src/staking/stake.rs @@ -62,7 +62,7 @@ where coins: Coins, epoch: Epoch, minimum_stakeable: Option, - ) -> StakingResult { + ) -> StakesResult { // 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 { @@ -112,7 +112,7 @@ where &mut self, coins: Coins, minimum_stakeable: Option, - ) -> StakingResult { + ) -> StakesResult { 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 a5e22a6f8..f36ebe020 100644 --- a/data_structures/src/staking/stakes.rs +++ b/data_structures/src/staking/stakes.rs @@ -118,7 +118,7 @@ where key: ISK, coins: Coins, epoch: Epoch, - ) -> StakingResult, Address, Coins, Epoch> + ) -> StakesResult, Address, Coins, Epoch> where ISK: Into>, { @@ -154,6 +154,11 @@ where Ok(stake.value.read()?.clone()) } + /// Quickly count how many stake entries are recorded into this data structure. + pub fn stakes_count(&self) -> usize { + self.by_key.len() + } + /// Obtain a list of stakers, conveniently ordered by one of several strategies. /// /// ## Strategies @@ -190,7 +195,7 @@ where key: ISK, capability: Capability, epoch: Epoch, - ) -> StakingResult + ) -> StakesResult where ISK: Into>, { @@ -207,6 +212,9 @@ where /// For a given capability, obtain the full list of stakers ordered by their power in that /// capability. + /// TODO: we may memoize the rank by keeping the last one in a non-serializable field in `Self` that keeps a boxed + /// iterator, so that this method doesn't have to sort multiple times if we are calling the `rank` method several + /// times in the same epoch. pub fn rank( &self, capability: Capability, @@ -229,7 +237,7 @@ where &mut self, key: ISK, coins: Coins, - ) -> StakingResult + ) -> StakesResult where ISK: Into>, { @@ -270,7 +278,7 @@ where key: ISK, capability: Capability, current_epoch: Epoch, - ) -> StakingResult<(), Address, Coins, Epoch> + ) -> StakesResult<(), Address, Coins, Epoch> where ISK: Into>, { @@ -362,7 +370,7 @@ pub fn process_stake_transaction( stakes: &mut Stakes, transaction: &StakeTransaction, epoch: Epoch, -) -> StakingResult<(), PublicKeyHash, Wit, Epoch> +) -> StakesResult<(), PublicKeyHash, Wit, Epoch> where Epoch: Copy + Default diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 4d6c8ec90..a6949f692 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,7 +14,7 @@ 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_POS_MAX_STAKE_BLOCK_WEIGHT, @@ -22,9 +22,9 @@ use witnet_config::defaults::{ }; 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::{ @@ -50,7 +50,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::{ diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 2e1eedc44..a48344572 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -57,6 +57,7 @@ pub fn exec( let _guard = init_logger(log_opts); witnet_data_structures::set_environment(config.environment); + log::debug!("{:#?}", config); for (version, epoch) in config.protocol.iter() { if let Some(epoch) = epoch { register_protocol_version(version, epoch); diff --git a/validations/Cargo.toml b/validations/Cargo.toml index 6cf51b7b3..2178fdf7a 100644 --- a/validations/Cargo.toml +++ b/validations/Cargo.toml @@ -16,6 +16,7 @@ witnet_config = { path = "../config" } witnet_crypto = { path = "../crypto" } witnet_data_structures = { path = "../data_structures" } witnet_rad = { path = "../rad" } +num-traits = "0.2.18" [dev-dependencies] approx = "0.5.0" diff --git a/validations/src/eligibility/current.rs b/validations/src/eligibility/current.rs new file mode 100644 index 000000000..9c7e5c2c4 --- /dev/null +++ b/validations/src/eligibility/current.rs @@ -0,0 +1,270 @@ +use std::ops::{Add, Div, Mul, Sub}; +use witnet_data_structures::staking::prelude::*; + +const MINING_REPLICATION_FACTOR: usize = 4; +const WITNESSING_MAX_ROUNDS: usize = 4; + +/// Different reasons for ineligibility of a validator, stake entry or eligibility proof. +#[derive(Copy, Debug, Clone, PartialEq)] +pub enum IneligibilityReason { + /// The stake entry has no power enough to perform such action. + InsufficientPower, + /// No matching stake entry has been found. + NotStaking, +} + +/// Signals whether a validator, stake entry or eligibility proof is eligible or not, and in the negative case, it also +/// provides a reason for ineligibility. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Eligible { + /// It is eligible. + Yes, + /// It is not eligible (provides a reason). + No(IneligibilityReason), +} + +impl From for Eligible { + #[inline] + fn from(reason: IneligibilityReason) -> Self { + Eligible::No(reason) + } +} + +/// Trait providing eligibility calculation for multiple protocol capabilities. +pub trait Eligibility { + /// Tells whether a VRF proof meets the requirements to become eligible for mining. Unless an error occurs, returns + /// an `Eligibility` structure signaling eligibility or lack thereof (in which case you also get an + /// `IneligibilityReason`. + fn mining_eligibility( + &self, + key: ISK, + epoch: Epoch, + ) -> StakesResult + where + ISK: Into>; + + /// Tells whether a VRF proof meets the requirements to become eligible for mining. Because this function returns a + /// simple `bool`, it is best-effort: both lack of eligibility and any error cases are mapped to `false`. + fn mining_eligibility_bool(&self, key: ISK, epoch: Epoch) -> bool + where + ISK: Into>, + { + matches!(self.mining_eligibility(key, epoch), Ok(Eligible::Yes)) + } + + /// Tells whether a VRF proof meets the requirements to become eligible for witnessing. Unless an error occurs, + /// returns an `Eligibility` structure signaling eligibility or lack thereof (in which case you also get an + /// `IneligibilityReason`. + fn witnessing_eligibility( + &self, + key: ISK, + epoch: Epoch, + witnesses: u8, + round: u8, + ) -> StakesResult + where + ISK: Into>; + + /// Tells whether a VRF proof meets the requirements to become eligible for witnessing. Because this function + /// returns a simple `bool`, it is best-effort: both lack of eligibility and any error cases are mapped to `false`. + fn witnessing_eligibility_bool(&self, key: ISK, epoch: Epoch, witnesses: u8, round: u8) -> bool + where + ISK: Into>, + { + matches!(self.witnessing_eligibility(key, epoch, witnesses, round), Ok(Eligible::Yes)) + } +} + +impl Eligibility + for Stakes +where + Address: Default, + Coins: Copy + + Default + + Ord + + From + + Into + + num_traits::Zero + + Add + + Sub + + Mul + + Mul, + Address: Clone + Ord + 'static, + Epoch: Copy + Default + num_traits::Saturating + Sub + From, + Power: Copy + Default + Ord + Add + Sub + Mul + Div + From, + u64: From + From, +{ + fn mining_eligibility( + &self, + key: ISK, + epoch: Epoch, + ) -> StakesResult + where + ISK: Into>, + { + let power = match self.query_power(key, Capability::Mining, epoch) { + Ok(p) => p, + Err(e) => { + // Early exit if the stake key does not exist + return match e { + StakesError::EntryNotFound { .. } => Ok(IneligibilityReason::NotStaking.into()), + e => Err(e), + }; + } + }; + + let mut rank = self.rank(Capability::Mining, epoch); + + // Requirement no. 3 from the WIP: + // "the mining power of the block proposer is greater than `max_power / rf`" + // (This goes before no. 2 because it is cheaper, computation-wise, and we want validations to exit ASAP) + // TODO: verify if defaulting to 0 makes sense + let (_, max_power) = rank.next().unwrap_or_default(); + let threshold = max_power / Power::from(MINING_REPLICATION_FACTOR as u64); + if power <= threshold { + return Ok(IneligibilityReason::InsufficientPower.into()); + } + + // Requirement no. 2 from the WIP: + // "the mining power of the block proposer is in the `rf / stakers`th quantile among the mining powers of all + // the stakers" + let stakers = self.stakes_count(); + let quantile = stakers / MINING_REPLICATION_FACTOR; + // TODO: verify if defaulting to 0 makes sense + let (_, threshold) = rank.nth(quantile).unwrap_or_default(); + if power <= threshold { + return Ok(IneligibilityReason::InsufficientPower.into()); + } + + // If all the requirements are met, we can deem it as eligible + Ok(Eligible::Yes) + } + + fn witnessing_eligibility(&self, key: ISK, epoch: Epoch, witnesses: u8, round: u8) -> StakesResult where ISK: Into> { + let power = match self.query_power(key, Capability::Witnessing, epoch) { + Ok(p) => p, + Err(e) => { + // Early exit if the stake key does not exist + return match e { + StakesError::EntryNotFound { .. } => Ok(IneligibilityReason::NotStaking.into()), + e => Err(e), + }; + } + }; + + let mut rank = self.rank(Capability::Mining, epoch); + let rf = 2usize.pow(round as u32) * witnesses as usize; + + // Requirement no. 2 from the WIP: + // "the witnessing power of the block proposer is in the `rf / stakers`th quantile among the witnessing powers + // of all the stakers" + let stakers = self.stakes_count(); + let quantile = stakers / MINING_REPLICATION_FACTOR; + // TODO: verify if defaulting to 0 makes sense + let (_, threshold) = rank.nth(quantile).unwrap_or_default(); + if power <= threshold { + return Ok(IneligibilityReason::InsufficientPower.into()); + } + + // Requirement no. 3 from the WIP: + // "the big-endian value of the VRF output is less than + // `max_rounds * own_power / (max_power * (rf - max_rounds) - rf * threshold_power)`" + // TODO: verify if defaulting to 0 makes sense + let (_, max_power) = rank.next().unwrap_or_default(); + let stakers = self.stakes_count(); + let quantile = stakers / rf; + // TODO: verify if defaulting to 0 makes sense + let (_, threshold_power) = rank.nth(quantile).unwrap_or_default() ; + let dividend = Power::from(WITNESSING_MAX_ROUNDS as u64) * power; + let divisor = max_power * Power::from((rf - WITNESSING_MAX_ROUNDS) as u64) - Power::from(rf as u64) * threshold_power; + let threshold = dividend / divisor; + println!("{}", u64::from(power)); + println!("{}", u64::from(threshold)); + if power <= threshold { + return Ok(IneligibilityReason::InsufficientPower.into()) + } + + Ok(Eligible::Yes) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mining_eligibility_no_stakers() { + let stakes = >::with_minimum(100u64); + let isk = ("validator", "withdrawer"); + + assert_eq!( + stakes.mining_eligibility(isk, 0), + Ok(Eligible::No(IneligibilityReason::NotStaking)) + ); + assert_eq!(stakes.mining_eligibility_bool(isk, 0), false); + + assert_eq!( + stakes.mining_eligibility(isk, 100), + Ok(Eligible::No(IneligibilityReason::NotStaking)) + ); + assert_eq!(stakes.mining_eligibility_bool(isk, 100), false); + } + + #[test] + fn test_mining_eligibility_absolute_power() { + let mut stakes = >::with_minimum(100u64); + let isk = ("validator", "withdrawer"); + + stakes.add_stake(isk, 1_000, 0).unwrap(); + + assert_eq!( + stakes.mining_eligibility(isk, 0), + Ok(Eligible::No(IneligibilityReason::InsufficientPower)) + ); + assert_eq!(stakes.mining_eligibility_bool(isk, 0), false); + + assert_eq!( + stakes.mining_eligibility(isk, 100), + Ok(Eligible::Yes) + ); + assert_eq!(stakes.mining_eligibility_bool(isk, 100), true); + } + + #[test] + fn test_witnessing_eligibility_no_stakers() { + let stakes = >::with_minimum(100u64); + let isk = ("validator", "withdrawer"); + + assert_eq!( + stakes.witnessing_eligibility(isk, 0, 10, 0), + Ok(Eligible::No(IneligibilityReason::NotStaking)) + ); + assert_eq!(stakes.witnessing_eligibility_bool(isk, 0, 10, 0), false); + + assert_eq!( + stakes.witnessing_eligibility(isk, 100, 10, 0), + Ok(Eligible::No(IneligibilityReason::NotStaking)) + ); + assert_eq!(stakes.witnessing_eligibility_bool(isk, 100, 10, 0), false); + } + + #[test] + fn test_witnessing_eligibility_absolute_power() { + let mut stakes = >::with_minimum(100u64); + let isk = ("validator", "withdrawer"); + + stakes.add_stake(isk, 1_000, 0).unwrap(); + + assert_eq!( + stakes.witnessing_eligibility(isk, 0, 10, 0), + Ok(Eligible::No(IneligibilityReason::InsufficientPower)) + ); + assert_eq!(stakes.witnessing_eligibility_bool(isk, 0, 10, 0), false); + + assert_eq!( + stakes.witnessing_eligibility(isk, 100, 10, 0), + Ok(Eligible::Yes) + ); + assert_eq!(stakes.witnessing_eligibility_bool(isk, 100, 10, 0), true); + } +} diff --git a/validations/src/eligibility/mod.rs b/validations/src/eligibility/mod.rs index a7a9673c3..689cbbe12 100644 --- a/validations/src/eligibility/mod.rs +++ b/validations/src/eligibility/mod.rs @@ -3,4 +3,4 @@ pub mod legacy; /// Eligibility-related functions specific to the latest Proof-of-Stake version of the Witnet /// protocol. -pub mod pos; +pub mod current; diff --git a/validations/src/eligibility/pos.rs b/validations/src/eligibility/pos.rs deleted file mode 100644 index 8b1378917..000000000 --- a/validations/src/eligibility/pos.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/validations/src/tests/compare_block_candidates.rs b/validations/src/tests/compare_block_candidates.rs index df1707abe..ce95f6ade 100644 --- a/validations/src/tests/compare_block_candidates.rs +++ b/validations/src/tests/compare_block_candidates.rs @@ -2,7 +2,7 @@ use witnet_data_structures::chain::{tapi::current_active_wips, Hash, Reputation} use std::cmp::Ordering; -use crate::validations::*; +use crate::{validations::*, eligibility::legacy::*}; #[test] fn test_compare_candidate_same_section() { diff --git a/validations/src/tests/mod.rs b/validations/src/tests/mod.rs index 4e395494a..5b51b0991 100644 --- a/validations/src/tests/mod.rs +++ b/validations/src/tests/mod.rs @@ -8482,7 +8482,7 @@ fn st_no_inputs() { // Try to create a stake tx with no inputs let st_output = StakeOutput { value: MIN_STAKE_NANOWITS + 1, - authorization: KeyedSignature::default(), + ..Default::default() }; let st_body = StakeTransactionBody::new(vec![], st_output, None); @@ -8516,8 +8516,8 @@ fn st_one_input_but_no_signature() { // No signatures but 1 input let stake_output = StakeOutput { - authorization: KeyedSignature::default(), value: MIN_STAKE_NANOWITS + 1, + ..Default::default() }; let stake_tx_body = StakeTransactionBody::new(vec![vti], stake_output, None); @@ -8552,8 +8552,8 @@ fn st_below_min_stake() { // No signatures but 1 input let stake_output = StakeOutput { - authorization: KeyedSignature::default(), value: 1, + ..Default::default() }; let stake_tx_body = StakeTransactionBody::new(vec![vti], stake_output, None); diff --git a/validations/src/tests/randpoe.rs b/validations/src/tests/randpoe.rs index 4eb9b3c1b..f0c3fb32d 100644 --- a/validations/src/tests/randpoe.rs +++ b/validations/src/tests/randpoe.rs @@ -5,7 +5,7 @@ use witnet_data_structures::chain::{ Alpha, Hash, PublicKeyHash, Reputation, ReputationEngine, }; -use crate::validations::*; +use crate::eligibility::legacy::*; #[test] fn target_randpoe() { diff --git a/validations/src/tests/reppoe.rs b/validations/src/tests/reppoe.rs index a11c9c135..fcdac1805 100644 --- a/validations/src/tests/reppoe.rs +++ b/validations/src/tests/reppoe.rs @@ -8,7 +8,7 @@ use witnet_data_structures::{ transaction::DRTransaction, }; -use crate::validations::*; +use crate::eligibility::legacy::*; fn calculate_reppoe_threshold_v1( rep_eng: &ReputationEngine, From e84fb24e697ee8ea269017495873410e2ceac03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Wed, 24 Apr 2024 15:29:51 +0200 Subject: [PATCH 33/83] feat(node): integrate PoS eligibility into block proposing --- data_structures/src/chain/mod.rs | 10 +- data_structures/src/staking/errors.rs | 2 +- data_structures/src/staking/stakes.rs | 1 + node/src/actors/chain_manager/handlers.rs | 14 +- node/src/actors/chain_manager/mining.rs | 194 +++++++++------------- node/src/actors/chain_manager/mod.rs | 11 +- node/src/storage_mngr/node_migrations.rs | 27 ++- 7 files changed, 134 insertions(+), 125 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index ea6d43792..1ed721de9 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -1228,6 +1228,11 @@ impl Hash { Hash::SHA256(bytes) => *bytes, } } + + /// Creates an instance of Hash where all bytes are set to their max value. + pub fn max() -> Self { + Self::SHA256([u8::MAX; 32]) + } } /// Error when parsing hash from string @@ -3805,11 +3810,12 @@ pub struct ChainState { pub superblock_state: SuperBlockState, /// TAPI Engine pub tapi_engine: TapiEngine, + /// Tracks stakes for every validator in the network + #[serde(default)] + pub stakes: Stakes, /// 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/errors.rs b/data_structures/src/staking/errors.rs index 869536de3..d0c26600b 100644 --- a/data_structures/src/staking/errors.rs +++ b/data_structures/src/staking/errors.rs @@ -7,7 +7,7 @@ use std::{ }; /// All errors related to the staking functionality. -#[derive(Debug, PartialEq, Fail)] +#[derive(Debug, Eq, PartialEq, Fail)] pub enum StakesError where Address: Debug + Display + Sync + Send + 'static, diff --git a/data_structures/src/staking/stakes.rs b/data_structures/src/staking/stakes.rs index f36ebe020..faa7e3abc 100644 --- a/data_structures/src/staking/stakes.rs +++ b/data_structures/src/staking/stakes.rs @@ -79,6 +79,7 @@ where /// 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. + #[serde(skip)] minimum_stakeable: Option, } diff --git a/node/src/actors/chain_manager/handlers.rs b/node/src/actors/chain_manager/handlers.rs index eb489c60a..2c9e23c95 100644 --- a/node/src/actors/chain_manager/handlers.rs +++ b/node/src/actors/chain_manager/handlers.rs @@ -1739,7 +1739,19 @@ impl Handler for ChainManager { type Result = (); fn handle(&mut self, _msg: TryMineBlock, ctx: &mut Self::Context) -> Self::Result { - self.try_mine_block(ctx); + if let Err(e) = self.try_mine_block(ctx) { + match e { + // Lack of eligibility is logged as debug + e @ ChainManagerError::NotEligible => { + log::debug!("{}", e); + } + // Any other errors are logged as warning (considering that this is a best-effort + // method) + e => { + log::warn!("{}", e); + } + } + } } } diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index a6949f692..127fc3800 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -36,6 +36,7 @@ use witnet_data_structures::{ proto::versioning::{ProtocolVersion, VersionedHashable}, radon_error::RadonError, radon_report::{RadonReport, ReportContext, TypeLike}, + staking::prelude::*, transaction::{ CommitTransaction, CommitTransactionBody, DRTransactionBody, MintTransaction, RevealTransaction, RevealTransactionBody, StakeTransactionBody, TallyTransaction, @@ -54,17 +55,18 @@ use witnet_rad::{ }; use witnet_util::timestamp::get_timestamp; use witnet_validations::{ - eligibility::legacy::*, + eligibility::{current::Eligibility, legacy::*}, validations::{ block_reward, calculate_liars_and_errors_count_from_tally, dr_transaction_fee, merkle_tree_root, st_transaction_fee, tally_bytes_on_encode_error, update_utxo_diff, vt_transaction_fee, }, }; +use witnet_validations::eligibility::current::Eligible; use crate::{ actors::{ - chain_manager::{ChainManager, StateMachine}, + chain_manager::{ChainManager, ChainManagerError, StateMachine}, messages::{AddCommitReveal, ResolveRA, RunTally}, rad_manager::RadManager, }, @@ -73,137 +75,97 @@ use crate::{ impl ChainManager { /// Try to mine a block - pub fn try_mine_block(&mut self, ctx: &mut Context) { + pub fn try_mine_block(&mut self, ctx: &mut Context) -> Result<(), ChainManagerError> { if !self.mining_enabled { - log::debug!("Mining is disabled in the configuration"); - return; + return Err(ChainManagerError::MiningIsDisabled); } // We only want to mine in Synced state if self.sm_state != StateMachine::Synced { - log::debug!( - "Not mining because node is not in Synced state (current state is {:?})", - self.sm_state - ); - return; - } - - if self.current_epoch.is_none() { - log::warn!("Cannot mine a block because current epoch is unknown"); - - return; + return Err(ChainManagerError::NotSynced { current_state: self.sm_state }); } - if self.own_pkh.is_none() { - log::warn!("PublicKeyHash is not set. All mined wits will be lost!"); - } - - if self.chain_state.reputation_engine.is_none() { - log::warn!("Reputation engine is not set"); - return; - } - if self.epoch_constants.is_none() { - log::warn!("EpochConstants is not set"); - - return; - } - if self.chain_state.chain_info.is_none() { - log::warn!("ChainInfo is not set"); + let current_epoch = self.current_epoch.ok_or(ChainManagerError::ChainNotReady)?; + let own_pkh = self.own_pkh.ok_or(ChainManagerError::ChainNotReady)?; + let epoch_constants = self.epoch_constants.ok_or(ChainManagerError::ChainNotReady)?; + let chain_info = self.chain_state.chain_info.clone().ok_or(ChainManagerError::ChainNotReady)?; - return; - } - let epoch_constants = self.epoch_constants.unwrap(); - let rep_engine = self.chain_state.reputation_engine.as_ref().unwrap().clone(); - let total_identities = u32::try_from(rep_engine.ars().active_identities_number()).unwrap(); - - let current_epoch = self.current_epoch.unwrap(); - - 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; let minimum_difficulty = chain_info.consensus_constants.minimum_difficulty; let initial_block_reward = chain_info.consensus_constants.initial_block_reward; let halving_period = chain_info.consensus_constants.halving_period; - let epochs_with_minimum_difficulty = chain_info - .consensus_constants - .epochs_with_minimum_difficulty; let mut beacon = chain_info.highest_block_checkpoint; let mut vrf_input = chain_info.highest_vrf_output; - if beacon.checkpoint >= current_epoch { - // We got a block from the future - // Due to block consolidation from epoch N is done in epoch N+1, - // and chain beacon is the same that the last block known. - // Our chain beacon always come from the past epoch. So, a chain beacon - // with the current epoch is the same error if it is come from the future - log::error!( - "The current highest checkpoint beacon is from the future ({:?} >= {:?})", - beacon.checkpoint, - current_epoch - ); - return; + if get_protocol_version(self.current_epoch) == ProtocolVersion::V2_0 { + let key = StakeKey::from((own_pkh, own_pkh)); + let eligibility = self.chain_state.stakes.mining_eligibility(key, current_epoch).map_err(ChainManagerError::Staking)?; + + match eligibility { + Eligible::Yes => { + log::info!("Hurray! Found eligibility for proposing a block candidate!"); + } + Eligible::No(_) => { + log::debug!("No eligibility for proposing a block candidate.") + } + } } + // The highest checkpoint beacon should contain the current epoch beacon.checkpoint = current_epoch; vrf_input.checkpoint = current_epoch; - let own_pkh = self.own_pkh.unwrap_or_default(); - let is_ars_member = rep_engine.is_ars_member(&own_pkh); - let active_wips = ActiveWips { - active_wips: self.chain_state.tapi_engine.wip_activation.clone(), - block_epoch: current_epoch, + let target_hash = if get_protocol_version(self.current_epoch) == ProtocolVersion::V2_0 { + Hash::max() + } else { + let rep_engine = self.chain_state.reputation_engine.as_ref().unwrap().clone(); + let total_identities = u32::try_from(rep_engine.ars().active_identities_number()).unwrap(); + let epochs_with_minimum_difficulty = chain_info + .consensus_constants + .epochs_with_minimum_difficulty; + + let active_wips = ActiveWips { + active_wips: self.chain_state.tapi_engine.wip_activation.clone(), + block_epoch: current_epoch, + }; + + // invalid: vrf_hash > target_hash + let (target_hash, _probability) = calculate_randpoe_threshold( + total_identities, + mining_bf, + current_epoch, + minimum_difficulty, + epochs_with_minimum_difficulty, + &active_wips, + ); + + target_hash }; - // Create a VRF proof and if eligible build block signature_mngr::vrf_prove(VrfMessage::block_mining(vrf_input)) .map(move |res| { res.map_err(|e| log::error!("Failed to create block eligibility proof: {}", e)) .map(move |(vrf_proof, vrf_proof_hash)| { - // invalid: vrf_hash > target_hash - let (target_hash, probability) = calculate_randpoe_threshold( - total_identities, - mining_bf, - current_epoch, - minimum_difficulty, - epochs_with_minimum_difficulty, - &active_wips, - ); - let proof_invalid = vrf_proof_hash > target_hash; + // For legacy protocol versions, check if our proof meets some thresholds + + if vrf_proof_hash > target_hash { + Err(())?; + } log::info!( - "Probability to create a valid mining proof: {:.6}%", - probability * 100_f64 + "{} Discovered eligibility for mining a block for epoch #{}", + Yellow.bold().paint("[Mining]"), + Yellow.bold().paint(beacon.checkpoint.to_string()) ); - log::trace!("Target hash: {}", target_hash); - log::trace!("Our proof: {}", vrf_proof_hash); - if proof_invalid { - log::debug!("No eligibility for mining a block"); - Err(()) - } else { - log::info!( - "{} Discovered eligibility for mining a block for epoch #{}", - Yellow.bold().paint("[Mining]"), - Yellow.bold().paint(beacon.checkpoint.to_string()) - ); - let mining_prob = calculate_mining_probability( - &rep_engine, - own_pkh, - mining_rf, - mining_bf, - ); - // Discount the already reached probability - let mining_prob = mining_prob / probability * 100.0; - log::info!( - "Probability that the mined block will be selected: {:.6}%", - mining_prob - ); - Ok(vrf_proof) - } + + // TODO: figure out if mining probability estimates make any sense in the context of PoS + + Ok::<_, ()>(vrf_proof) }) }) .flatten_err() @@ -216,12 +178,8 @@ impl ChainManager { .and_then(move |(vrf_proof, tally_transactions), act, _ctx| { let eligibility_claim = BlockEligibilityClaim { proof: vrf_proof }; - // If pkh is in ARS, no need to send bn256 public key - let bn256_public_key = if is_ars_member { - None - } else { - act.bn256_public_key.clone() - }; + // TODO: assess if bn256 keys are redundant + let bn256_public_key = act.bn256_public_key.clone(); let tapi_version = act.tapi_signals_mask(current_epoch); @@ -275,25 +233,27 @@ impl ChainManager { beacon, epoch_constants, ) - .map_ok(|_diff, act, _ctx| { - // Send AddCandidates message to self - // This will run all the validations again - - let block_hash = block.hash(); - // FIXME(#1773): Currently last_block_proposed is not used, but removing it is a breaking change - act.chain_state.node_stats.last_block_proposed = block_hash; - act.chain_state.node_stats.block_proposed_count += 1; - log::info!( + .map_ok(|_diff, act, _ctx| { + // Send AddCandidates message to self + // This will run all the validations again + + let block_hash = block.hash(); + // FIXME(#1773): Currently last_block_proposed is not used, but removing it is a breaking change + act.chain_state.node_stats.last_block_proposed = block_hash; + act.chain_state.node_stats.block_proposed_count += 1; + log::info!( "Proposed block candidate {}", Yellow.bold().paint(block_hash.to_string()) ); - act.process_candidate(block); - }) - .map_err(|e, _, _| log::error!("Error trying to mine a block: {}", e)) + act.process_candidate(block); + }) + .map_err(|e, _, _| log::error!("Error trying to mine a block: {}", e)) }) .map(|_res: Result<(), ()>, _act, _ctx| ()) .wait(ctx); + + Ok(()) } /// Try to mine a data_request diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index db09d8b59..e7383204e 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -129,7 +129,7 @@ pub enum ChainManagerError { #[fail(display = "A block does not exist")] BlockDoesNotExist, /// Optional fields of ChainManager are not properly initialized yet - #[fail(display = "ChainManager is not ready yet")] + #[fail(display = "ChainManager is not ready yet. This may self-fix in a little while")] ChainNotReady, /// The node attempted to do an action that is only allowed while `ChainManager` /// is in `Synced` state. @@ -158,6 +158,15 @@ pub enum ChainManagerError { /// Tells what the current epoch was current_superblock_index: u32, }, + /// Tried to mine block candidates but mining is disabled through configuration. + #[fail(display = "Mining is disabled through configuration")] + MiningIsDisabled, + /// A staking-related error happened. + #[fail(display = "A staking-related error happened: {:?}", _0)] + Staking(StakesError), + /// The node is not eligible to perform a certain action. + #[fail(display = "The node is not eligible to perform this action")] + NotEligible, } /// Synchronization target determined by the beacons received from outbound peers diff --git a/node/src/storage_mngr/node_migrations.rs b/node/src/storage_mngr/node_migrations.rs index 2315a98e3..add12e72a 100644 --- a/node/src/storage_mngr/node_migrations.rs +++ b/node/src/storage_mngr/node_migrations.rs @@ -1,9 +1,12 @@ -use super::*; use witnet_data_structures::{ - chain::{tapi::TapiEngine, ChainState}, + chain::{ChainState, Epoch, PublicKeyHash, tapi::TapiEngine}, + staking::stakes::Stakes, utxo_pool::UtxoWriteBatch, + wit::Wit, }; +use super::*; + macro_rules! as_failure { ($e:expr) => { failure::Error::from_boxed_compat(Box::new($e)) @@ -55,6 +58,17 @@ fn migrate_chain_state_v2_to_v3(chain_state_bytes: &mut [u8]) { chain_state_bytes[0..4].copy_from_slice(&db_version_bytes); } +fn migrate_chain_state_v3_to_v4(old_chain_state_bytes: &[u8]) -> Vec { + let db_version: u32 = 4; + let db_version_bytes = db_version.to_le_bytes(); + + // Extra fields in ChainState v4: + let stakes = Stakes::::default(); + let stakes_bytes = bincode::serialize(&stakes).unwrap(); + + [&db_version_bytes, &old_chain_state_bytes[4..], &stakes_bytes].concat() +} + fn migrate_chain_state(mut bytes: Vec) -> Result { loop { match check_chain_state_version(&bytes) { @@ -73,6 +87,11 @@ fn migrate_chain_state(mut bytes: Vec) -> Result log::debug!("Successfully migrated ChainState v2 to v3"); } Ok(3) => { + // Migrate from v3 to v4 + bytes = migrate_chain_state_v3_to_v4(&bytes); + log::debug!("Successfully migrated ChainState v3 to v4"); + } + Ok(4) => { // Latest version // Skip the first 4 bytes because they are used to encode db_version return match deserialize(&bytes[4..]) { @@ -193,10 +212,12 @@ where #[cfg(test)] mod tests { - use super::*; use serde::{Deserialize, Serialize}; + use witnet_data_structures::chain::ChainInfo; + use super::*; + #[test] fn bincode_version() { #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] From f743a98daf75e15a7c5531a2079a0e7786afa23b Mon Sep 17 00:00:00 2001 From: drcpu Date: Wed, 1 May 2024 23:25:03 +0200 Subject: [PATCH 34/83] fix(validations): fix compilation issues due to missing traits after rebase --- data_structures/src/staking/stakes.rs | 11 +-- net/src/server/ws/mod.rs | 1 + node/src/actors/chain_manager/handlers.rs | 2 +- node/src/actors/chain_manager/mining.rs | 55 +++++++----- node/src/storage_mngr/node_migrations.rs | 9 +- validations/src/eligibility/current.rs | 86 ++++++++++++++----- .../src/tests/compare_block_candidates.rs | 2 +- 7 files changed, 114 insertions(+), 52 deletions(-) diff --git a/data_structures/src/staking/stakes.rs b/data_structures/src/staking/stakes.rs index faa7e3abc..50d1a294d 100644 --- a/data_structures/src/staking/stakes.rs +++ b/data_structures/src/staking/stakes.rs @@ -308,7 +308,7 @@ where pub fn query_stakes( &mut self, query: TIQSK, - ) -> StakingResult + ) -> StakesResult where TIQSK: TryInto>, { @@ -322,7 +322,7 @@ where /// Query stakes by stake key. #[inline(always)] - fn query_by_key(&self, key: StakeKey
) -> StakingResult { + fn query_by_key(&self, key: StakeKey
) -> StakesResult { Ok(self .by_key .get(&key) @@ -334,10 +334,7 @@ where /// Query stakes by validator address. #[inline(always)] - fn query_by_validator( - &self, - validator: Address, - ) -> StakingResult { + fn query_by_validator(&self, validator: Address) -> StakesResult { Ok(self .by_validator .get(&validator) @@ -352,7 +349,7 @@ where fn query_by_withdrawer( &self, withdrawer: Address, - ) -> StakingResult { + ) -> StakesResult { Ok(self .by_withdrawer .get(&withdrawer) diff --git a/net/src/server/ws/mod.rs b/net/src/server/ws/mod.rs index f0facf1c4..b114b07b3 100644 --- a/net/src/server/ws/mod.rs +++ b/net/src/server/ws/mod.rs @@ -12,6 +12,7 @@ pub use error::Error; type PubSubHandler = pubsub::PubSubHandler>; /// TODO: doc +#[allow(dead_code)] pub struct Server(server::Server); impl Server { diff --git a/node/src/actors/chain_manager/handlers.rs b/node/src/actors/chain_manager/handlers.rs index 2c9e23c95..e30cbbeaa 100644 --- a/node/src/actors/chain_manager/handlers.rs +++ b/node/src/actors/chain_manager/handlers.rs @@ -1747,7 +1747,7 @@ impl Handler for ChainManager { } // Any other errors are logged as warning (considering that this is a best-effort // method) - e => { + e => { log::warn!("{}", e); } } diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 127fc3800..92417355a 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -55,14 +55,16 @@ use witnet_rad::{ }; use witnet_util::timestamp::get_timestamp; use witnet_validations::{ - eligibility::{current::Eligibility, legacy::*}, + eligibility::{ + current::{Eligibility, Eligible}, + legacy::*, + }, validations::{ block_reward, calculate_liars_and_errors_count_from_tally, dr_transaction_fee, merkle_tree_root, st_transaction_fee, tally_bytes_on_encode_error, update_utxo_diff, vt_transaction_fee, }, }; -use witnet_validations::eligibility::current::Eligible; use crate::{ actors::{ @@ -82,13 +84,21 @@ impl ChainManager { // We only want to mine in Synced state if self.sm_state != StateMachine::Synced { - return Err(ChainManagerError::NotSynced { current_state: self.sm_state }); + return Err(ChainManagerError::NotSynced { + current_state: self.sm_state, + }); } let current_epoch = self.current_epoch.ok_or(ChainManagerError::ChainNotReady)?; - let own_pkh = self.own_pkh.ok_or(ChainManagerError::ChainNotReady)?; - let epoch_constants = self.epoch_constants.ok_or(ChainManagerError::ChainNotReady)?; - let chain_info = self.chain_state.chain_info.clone().ok_or(ChainManagerError::ChainNotReady)?; + let own_pkh = self.own_pkh.ok_or(ChainManagerError::ChainNotReady)?; + let epoch_constants = self + .epoch_constants + .ok_or(ChainManagerError::ChainNotReady)?; + let chain_info = self + .chain_state + .chain_info + .clone() + .ok_or(ChainManagerError::ChainNotReady)?; let max_vt_weight = chain_info.consensus_constants.max_vt_weight; let max_dr_weight = chain_info.consensus_constants.max_dr_weight; @@ -104,7 +114,11 @@ impl ChainManager { if get_protocol_version(self.current_epoch) == ProtocolVersion::V2_0 { let key = StakeKey::from((own_pkh, own_pkh)); - let eligibility = self.chain_state.stakes.mining_eligibility(key, current_epoch).map_err(ChainManagerError::Staking)?; + let eligibility = self + .chain_state + .stakes + .mining_eligibility(key, current_epoch) + .map_err(ChainManagerError::Staking)?; match eligibility { Eligible::Yes => { @@ -124,7 +138,8 @@ impl ChainManager { Hash::max() } else { let rep_engine = self.chain_state.reputation_engine.as_ref().unwrap().clone(); - let total_identities = u32::try_from(rep_engine.ars().active_identities_number()).unwrap(); + let total_identities = + u32::try_from(rep_engine.ars().active_identities_number()).unwrap(); let epochs_with_minimum_difficulty = chain_info .consensus_constants .epochs_with_minimum_difficulty; @@ -233,22 +248,22 @@ impl ChainManager { beacon, epoch_constants, ) - .map_ok(|_diff, act, _ctx| { - // Send AddCandidates message to self - // This will run all the validations again - - let block_hash = block.hash(); - // FIXME(#1773): Currently last_block_proposed is not used, but removing it is a breaking change - act.chain_state.node_stats.last_block_proposed = block_hash; - act.chain_state.node_stats.block_proposed_count += 1; - log::info!( + .map_ok(|_diff, act, _ctx| { + // Send AddCandidates message to self + // This will run all the validations again + + let block_hash = block.hash(); + // FIXME(#1773): Currently last_block_proposed is not used, but removing it is a breaking change + act.chain_state.node_stats.last_block_proposed = block_hash; + act.chain_state.node_stats.block_proposed_count += 1; + log::info!( "Proposed block candidate {}", Yellow.bold().paint(block_hash.to_string()) ); - act.process_candidate(block); - }) - .map_err(|e, _, _| log::error!("Error trying to mine a block: {}", e)) + act.process_candidate(block); + }) + .map_err(|e, _, _| log::error!("Error trying to mine a block: {}", e)) }) .map(|_res: Result<(), ()>, _act, _ctx| ()) .wait(ctx); diff --git a/node/src/storage_mngr/node_migrations.rs b/node/src/storage_mngr/node_migrations.rs index add12e72a..aae34f27c 100644 --- a/node/src/storage_mngr/node_migrations.rs +++ b/node/src/storage_mngr/node_migrations.rs @@ -1,5 +1,5 @@ use witnet_data_structures::{ - chain::{ChainState, Epoch, PublicKeyHash, tapi::TapiEngine}, + chain::{tapi::TapiEngine, ChainState, Epoch, PublicKeyHash}, staking::stakes::Stakes, utxo_pool::UtxoWriteBatch, wit::Wit, @@ -66,7 +66,12 @@ fn migrate_chain_state_v3_to_v4(old_chain_state_bytes: &[u8]) -> Vec { let stakes = Stakes::::default(); let stakes_bytes = bincode::serialize(&stakes).unwrap(); - [&db_version_bytes, &old_chain_state_bytes[4..], &stakes_bytes].concat() + [ + &db_version_bytes, + &old_chain_state_bytes[4..], + &stakes_bytes, + ] + .concat() } fn migrate_chain_state(mut bytes: Vec) -> Result { diff --git a/validations/src/eligibility/current.rs b/validations/src/eligibility/current.rs index 9c7e5c2c4..b4c48f247 100644 --- a/validations/src/eligibility/current.rs +++ b/validations/src/eligibility/current.rs @@ -1,4 +1,7 @@ -use std::ops::{Add, Div, Mul, Sub}; +use std::{ + fmt::{Debug, Display}, + ops::{Add, Div, Mul, Sub}, +}; use witnet_data_structures::staking::prelude::*; const MINING_REPLICATION_FACTOR: usize = 4; @@ -31,7 +34,12 @@ impl From for Eligible { } /// Trait providing eligibility calculation for multiple protocol capabilities. -pub trait Eligibility { +pub trait Eligibility +where + Address: Debug + Display + Sync + Send + 'static, + Coins: Debug + Display + Sync + Send + 'static, + Epoch: Debug + Display + Sync + Send + 'static, +{ /// Tells whether a VRF proof meets the requirements to become eligible for mining. Unless an error occurs, returns /// an `Eligibility` structure signaling eligibility or lack thereof (in which case you also get an /// `IneligibilityReason`. @@ -62,25 +70,36 @@ pub trait Eligibility { witnesses: u8, round: u8, ) -> StakesResult - where - ISK: Into>; + where + ISK: Into>; /// Tells whether a VRF proof meets the requirements to become eligible for witnessing. Because this function /// returns a simple `bool`, it is best-effort: both lack of eligibility and any error cases are mapped to `false`. - fn witnessing_eligibility_bool(&self, key: ISK, epoch: Epoch, witnesses: u8, round: u8) -> bool - where - ISK: Into>, + fn witnessing_eligibility_bool( + &self, + key: ISK, + epoch: Epoch, + witnesses: u8, + round: u8, + ) -> bool + where + ISK: Into>, { - matches!(self.witnessing_eligibility(key, epoch, witnesses, round), Ok(Eligible::Yes)) + matches!( + self.witnessing_eligibility(key, epoch, witnesses, round), + Ok(Eligible::Yes) + ) } } impl Eligibility for Stakes where - Address: Default, + Address: Clone + Debug + Default + Display + Ord + Sync + Send + 'static, Coins: Copy + + Debug + Default + + Display + Ord + From + Into @@ -88,10 +107,28 @@ where + Add + Sub + Mul - + Mul, - Address: Clone + Ord + 'static, - Epoch: Copy + Default + num_traits::Saturating + Sub + From, - Power: Copy + Default + Ord + Add + Sub + Mul + Div + From, + + Mul + + Sync + + Send + + 'static, + Epoch: Copy + + Debug + + Default + + Display + + num_traits::Saturating + + Sub + + From + + Sync + + Send + + 'static, + Power: Copy + + Default + + Ord + + Add + + Sub + + Mul + + Div + + From, u64: From + From, { fn mining_eligibility( @@ -140,7 +177,16 @@ where Ok(Eligible::Yes) } - fn witnessing_eligibility(&self, key: ISK, epoch: Epoch, witnesses: u8, round: u8) -> StakesResult where ISK: Into> { + fn witnessing_eligibility( + &self, + key: ISK, + epoch: Epoch, + witnesses: u8, + round: u8, + ) -> StakesResult + where + ISK: Into>, + { let power = match self.query_power(key, Capability::Witnessing, epoch) { Ok(p) => p, Err(e) => { @@ -174,14 +220,15 @@ where let stakers = self.stakes_count(); let quantile = stakers / rf; // TODO: verify if defaulting to 0 makes sense - let (_, threshold_power) = rank.nth(quantile).unwrap_or_default() ; + let (_, threshold_power) = rank.nth(quantile).unwrap_or_default(); let dividend = Power::from(WITNESSING_MAX_ROUNDS as u64) * power; - let divisor = max_power * Power::from((rf - WITNESSING_MAX_ROUNDS) as u64) - Power::from(rf as u64) * threshold_power; + let divisor = max_power * Power::from((rf - WITNESSING_MAX_ROUNDS) as u64) + - Power::from(rf as u64) * threshold_power; let threshold = dividend / divisor; println!("{}", u64::from(power)); println!("{}", u64::from(threshold)); if power <= threshold { - return Ok(IneligibilityReason::InsufficientPower.into()) + return Ok(IneligibilityReason::InsufficientPower.into()); } Ok(Eligible::Yes) @@ -223,10 +270,7 @@ mod tests { ); assert_eq!(stakes.mining_eligibility_bool(isk, 0), false); - assert_eq!( - stakes.mining_eligibility(isk, 100), - Ok(Eligible::Yes) - ); + assert_eq!(stakes.mining_eligibility(isk, 100), Ok(Eligible::Yes)); assert_eq!(stakes.mining_eligibility_bool(isk, 100), true); } diff --git a/validations/src/tests/compare_block_candidates.rs b/validations/src/tests/compare_block_candidates.rs index ce95f6ade..fc61c36cc 100644 --- a/validations/src/tests/compare_block_candidates.rs +++ b/validations/src/tests/compare_block_candidates.rs @@ -2,7 +2,7 @@ use witnet_data_structures::chain::{tapi::current_active_wips, Hash, Reputation} use std::cmp::Ordering; -use crate::{validations::*, eligibility::legacy::*}; +use crate::{eligibility::legacy::*, validations::*}; #[test] fn test_compare_candidate_same_section() { From b622aa6b9af62e0edbb22776c35fe200597234f3 Mon Sep 17 00:00:00 2001 From: drcpu Date: Wed, 1 May 2024 23:37:22 +0200 Subject: [PATCH 35/83] fix(schemas): remove optional label from ValueTransferOutput in StakeTransactionBody --- schemas/witnet/witnet.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/witnet/witnet.proto b/schemas/witnet/witnet.proto index 2d6d83f3a..ea424cd31 100644 --- a/schemas/witnet/witnet.proto +++ b/schemas/witnet/witnet.proto @@ -301,7 +301,7 @@ message StakeOutput { message StakeTransactionBody { repeated Input inputs = 1; StakeOutput output = 2; - optional ValueTransferOutput change = 3; + ValueTransferOutput change = 3; } message StakeTransaction { From 7a3d43fd679c870b2a47d706515cc95211981b81 Mon Sep 17 00:00:00 2001 From: drcpu Date: Wed, 1 May 2024 23:53:19 +0200 Subject: [PATCH 36/83] fix(mining): abort trying to mine a block when the node is not eligible --- node/src/actors/chain_manager/mining.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 92417355a..8ad3a7836 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -125,7 +125,8 @@ impl ChainManager { log::info!("Hurray! Found eligibility for proposing a block candidate!"); } Eligible::No(_) => { - log::debug!("No eligibility for proposing a block candidate.") + log::debug!("No eligibility for proposing a block candidate."); + return Ok(()); } } } From 3db22d1f2c5ab3f3f46b7feb3520329faca4387e Mon Sep 17 00:00:00 2001 From: drcpu Date: Thu, 2 May 2024 00:35:43 +0200 Subject: [PATCH 37/83] feat(validations): update block comparison and validation with wit/2 requirements --- data_structures/src/error.rs | 6 ++ node/src/actors/chain_manager/mod.rs | 18 ++++- validations/src/validations.rs | 111 ++++++++++++++++++--------- 3 files changed, 98 insertions(+), 37 deletions(-) diff --git a/data_structures/src/error.rs b/data_structures/src/error.rs index 24dda7e78..50891d06c 100644 --- a/data_structures/src/error.rs +++ b/data_structures/src/error.rs @@ -471,6 +471,12 @@ pub enum BlockError { count, block_hash )] MissingExpectedTallies { count: usize, block_hash: Hash }, + /// Missing expected tallies + #[fail( + display = "Validator {} is not eligible to propose a block", + validator, + )] + ValidatorNotEligible { validator: PublicKeyHash }, } #[derive(Debug, Fail)] diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index e7383204e..cbec0812e 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -69,7 +69,8 @@ use witnet_data_structures::{ SuperBlock, SuperBlockVote, TransactionsPool, }, data_request::DataRequestPool, - get_environment, + get_environment, get_protocol_version, + proto::versioning::ProtocolVersion, radon_report::{RadonReport, ReportContext}, staking::prelude::*, superblock::{ARSIdentities, AddSuperBlockVote, SuperBlockConsensus}, @@ -80,6 +81,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; @@ -697,6 +699,7 @@ impl ChainManager { let mut transaction_visitor = PriorityVisitor::default(); + let protocol_version = get_protocol_version(self.current_epoch); let utxo_diff = process_validations( &block, self.current_epoch.unwrap_or_default(), @@ -712,6 +715,8 @@ impl ChainManager { resynchronizing, &active_wips, Some(&mut transaction_visitor), + protocol_version, + &self.chain_state.stakes, )?; // Extract the collected priorities from the internal state of the visitor @@ -801,6 +806,7 @@ impl ChainManager { return; } }; + let protocol_version = get_protocol_version(Some(block.block_header.beacon.checkpoint)); if let Some(best_candidate) = &self.best_candidate { let best_hash = best_candidate.block.hash(); @@ -824,6 +830,7 @@ impl ChainManager { best_candidate.vrf_proof, best_candidate_is_active, &target_vrf_slots, + protocol_version, ) != Ordering::Greater { log::debug!("Ignoring new block candidate ({}) because a better one ({}) has been already validated", hash_block, best_hash); @@ -853,6 +860,8 @@ impl ChainManager { false, &active_wips, Some(&mut transaction_visitor), + protocol_version, + &self.chain_state.stakes, ) { Ok(utxo_diff) => { let priorities = transaction_visitor.take_state(); @@ -1992,6 +2001,7 @@ impl ChainManager { active_wips: self.chain_state.tapi_engine.wip_activation.clone(), block_epoch: block.block_header.beacon.checkpoint, }; + let protocol_version = get_protocol_version(Some(block.block_header.beacon.checkpoint)); let res = validate_block( &block, current_epoch, @@ -2001,6 +2011,8 @@ impl ChainManager { self.chain_state.reputation_engine.as_ref().unwrap(), &consensus_constants, &active_wips, + protocol_version, + &self.chain_state.stakes, ); let fut = async { @@ -2799,6 +2811,8 @@ pub fn process_validations( resynchronizing: bool, active_wips: &ActiveWips, transaction_visitor: Option<&mut dyn Visitor>, + protocol_version: ProtocolVersion, + stakes: &Stakes, ) -> Result { if !resynchronizing { let mut signatures_to_verify = vec![]; @@ -2811,6 +2825,8 @@ pub fn process_validations( rep_eng, consensus_constants, active_wips, + protocol_version, + stakes, )?; verify_signatures(signatures_to_verify, vrf_ctx)?; } diff --git a/validations/src/validations.rs b/validations/src/validations.rs index 8ce16df2e..bda2f7826 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -34,6 +34,10 @@ use witnet_data_structures::{ get_protocol_version, proto::versioning::{ProtocolVersion, VersionedHashable}, radon_report::{RadonReport, ReportContext}, + staking::{ + prelude::StakeKey, + stakes::Stakes, + }, transaction::{ CommitTransaction, DRTransaction, MintTransaction, RevealTransaction, StakeTransaction, TallyTransaction, Transaction, UnstakeTransaction, VTTransaction, @@ -42,7 +46,7 @@ use witnet_data_structures::{ types::visitor::Visitor, utxo_pool::{Diff, UnspentOutputsPool, UtxoDiff}, vrf::{BlockEligibilityClaim, DataRequestEligibilityClaim, VrfCtx}, - wit::NANOWITS_PER_WIT, + wit::{NANOWITS_PER_WIT, Wit}, }; use witnet_rad::{ conditions::{ @@ -55,7 +59,13 @@ use witnet_rad::{ types::{serial_iter_decode, RadonTypes}, }; -use crate::eligibility::legacy::*; +use crate::eligibility::{ + current::{ + Eligible, Eligibility, + IneligibilityReason::{InsufficientPower, NotStaking}, + }, + legacy::*, +}; // TODO: move to a configuration const MAX_STAKE_BLOCK_WEIGHT: u32 = 10_000_000; @@ -2033,6 +2043,8 @@ pub fn validate_block( rep_eng: &ReputationEngine, consensus_constants: &ConsensusConstants, active_wips: &ActiveWips, + protocol_version: ProtocolVersion, + stakes: &Stakes, ) -> Result<(), failure::Error> { let block_epoch = block.block_header.beacon.checkpoint; let hash_prev_block = block.block_header.beacon.hash_prev_block; @@ -2060,15 +2072,28 @@ pub fn validate_block( // with the genesis_block_hash validate_genesis_block(block, consensus_constants.genesis_hash).map_err(Into::into) } else { - let total_identities = u32::try_from(rep_eng.ars().active_identities_number())?; - let (target_hash, _) = calculate_randpoe_threshold( - total_identities, - consensus_constants.mining_backup_factor, - block_epoch, - consensus_constants.minimum_difficulty, - consensus_constants.epochs_with_minimum_difficulty, - active_wips, - ); + let target_hash = if protocol_version == ProtocolVersion::V2_0 { + let validator = block.block_sig.public_key.pkh(); + let validator_key = StakeKey::from((validator, validator)); + let eligibility = stakes.mining_eligibility(validator_key, block_epoch); + if eligibility == Ok(Eligible::No(InsufficientPower)) || eligibility == Ok(Eligible::No(NotStaking)) { + return Err(BlockError::ValidatorNotEligible{ validator }.into()); + } + + Hash::max() + } else { + let total_identities = u32::try_from(rep_eng.ars().active_identities_number())?; + let (target_hash, _) = calculate_randpoe_threshold( + total_identities, + consensus_constants.mining_backup_factor, + block_epoch, + consensus_constants.minimum_difficulty, + consensus_constants.epochs_with_minimum_difficulty, + active_wips, + ); + + target_hash + }; add_block_vrf_signature_to_verify( signatures_to_verify, @@ -2289,33 +2314,47 @@ pub fn compare_block_candidates( b2_vrf_hash: Hash, b2_is_active: bool, s: &VrfSlots, + version: ProtocolVersion, ) -> Ordering { - let section1 = s.slot(&b1_vrf_hash); - let section2 = s.slot(&b2_vrf_hash); - // Bigger section implies worse block candidate - section1 - .cmp(§ion2) - .reverse() - // Blocks created with nodes with reputation are better candidates than the others - .then({ - match (b1_rep.0 > 0, b2_rep.0 > 0) { - (true, false) => Ordering::Greater, - (false, true) => Ordering::Less, - _ => Ordering::Equal, - } - }) - // Blocks created with active nodes are better candidates than the others - .then({ - match (b1_is_active, b2_is_active) { - (true, false) => Ordering::Greater, - (false, true) => Ordering::Less, - _ => Ordering::Equal, - } - }) + let ordering = if version == ProtocolVersion::V2_0 { // Bigger vrf hash implies worse block candidate - .then(b1_vrf_hash.cmp(&b2_vrf_hash).reverse()) - // Bigger block implies worse block candidate - .then(b1_hash.cmp(&b2_hash).reverse()) + b1_vrf_hash + .cmp(&b2_vrf_hash) + .reverse() + // Bigger block implies worse block candidate + .then( + b1_hash.cmp(&b2_hash).reverse() + ) + } else { + let section1 = s.slot(&b1_vrf_hash); + let section2 = s.slot(&b2_vrf_hash); + // Bigger section implies worse block candidate + section1 + .cmp(§ion2) + .reverse() + // Blocks created with nodes with reputation are better candidates than the others + .then({ + match (b1_rep.0 > 0, b2_rep.0 > 0) { + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + _ => Ordering::Equal, + } + }) + // Blocks created with active nodes are better candidates than the others + .then({ + match (b1_is_active, b2_is_active) { + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + _ => Ordering::Equal, + } + }) + // Bigger vrf hash implies worse block candidate + .then(b1_vrf_hash.cmp(&b2_vrf_hash).reverse()) + // Bigger block implies worse block candidate + .then(b1_hash.cmp(&b2_hash).reverse()) + }; + + ordering } /// Blocking process to verify signatures From f0d8697b6b5e51ce95bc2616bc8465dc22dedba6 Mon Sep 17 00:00:00 2001 From: drcpu Date: Thu, 2 May 2024 14:45:00 +0200 Subject: [PATCH 38/83] fix(validations): move mint transaction validation after staking transaction validation such that the total block fee is calculated correctly --- validations/src/validations.rs | 38 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/validations/src/validations.rs b/validations/src/validations.rs index bda2f7826..ea5963832 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -1885,25 +1885,6 @@ pub fn validate_block_transactions( } let dr_hash_merkle_root = dr_mt.root(); - if !is_genesis { - // Validate mint - validate_mint_transaction( - &block.txns.mint, - total_fee, - block_beacon.checkpoint, - consensus_constants.initial_block_reward, - consensus_constants.halving_period, - )?; - - // Insert mint in utxo - update_utxo_diff( - &mut utxo_diff, - vec![], - block.txns.mint.outputs.iter(), - block.txns.mint.hash(), - ); - } - 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 @@ -2008,6 +1989,25 @@ pub fn validate_block_transactions( (hash::EMPTY_SHA256, hash::EMPTY_SHA256) }; + if !is_genesis { + // Validate mint + validate_mint_transaction( + &block.txns.mint, + total_fee, + block_beacon.checkpoint, + consensus_constants.initial_block_reward, + consensus_constants.halving_period, + )?; + + // Insert mint in utxo + update_utxo_diff( + &mut utxo_diff, + vec![], + block.txns.mint.outputs.iter(), + block.txns.mint.hash(), + ); + } + // Validate Merkle Root let merkle_roots = BlockMerkleRoots { mint_hash: block.txns.mint.hash(), From 701ca014b82c9e22538194bf32a6604027edf53e Mon Sep 17 00:00:00 2001 From: drcpu Date: Thu, 2 May 2024 14:45:44 +0200 Subject: [PATCH 39/83] feat(mining): remove staking transactions from the pool once they are included in a consolidated block --- node/src/actors/chain_manager/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index cbec0812e..44c5e5174 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -2979,6 +2979,10 @@ fn update_pools( transactions_pool.remove_one_reveal(&re_tx.body.dr_pointer, &re_tx.body.pkh, &re_tx.hash()); } + for st_tx in &block.txns.stake_txns { + transactions_pool.st_remove(st_tx); + } + // Update own_utxos utxo_diff.visit( own_utxos, From b2eb3ae5836d5239d61c14e4d9284937091e193f Mon Sep 17 00:00:00 2001 From: drcpu Date: Thu, 2 May 2024 20:14:51 +0200 Subject: [PATCH 40/83] feat(mining): reset collateral age of the validator which mined the latest block --- node/src/actors/chain_manager/mod.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index 44c5e5174..b40182791 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -924,6 +924,13 @@ impl ChainManager { } }; + let current_epoch = if let Some(epoch) = self.current_epoch { + epoch + } else { + log::error!("Current epoch not loaded in ChainManager"); + return; + }; + match self.chain_state { ChainState { chain_info: Some(ref mut chain_info), @@ -993,6 +1000,10 @@ impl ChainManager { let miner_pkh = block.block_header.proof.proof.pkh(); + // Reset the coin age of the miner for all staked coins + let key = StakeKey::from((miner_pkh, miner_pkh)); + let _ = stakes.reset_age(key, Capability::Mining, current_epoch); + // Do not update reputation or stakes when consolidating genesis block if block_hash != chain_info.consensus_constants.genesis_hash { update_reputation( From ad2955a7c422878f9b1d2454ff335d8bff79c6bc Mon Sep 17 00:00:00 2001 From: drcpu Date: Fri, 3 May 2024 23:21:30 +0200 Subject: [PATCH 41/83] chore(format): edit code style in validations to make Cargo formatter happy again --- data_structures/src/error.rs | 5 +---- node/src/actors/chain_manager/mod.rs | 3 ++- validations/src/validations.rs | 19 ++++++++----------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/data_structures/src/error.rs b/data_structures/src/error.rs index 50891d06c..8b4c994d5 100644 --- a/data_structures/src/error.rs +++ b/data_structures/src/error.rs @@ -472,10 +472,7 @@ pub enum BlockError { )] MissingExpectedTallies { count: usize, block_hash: Hash }, /// Missing expected tallies - #[fail( - display = "Validator {} is not eligible to propose a block", - validator, - )] + #[fail(display = "Validator {} is not eligible to propose a block", validator)] ValidatorNotEligible { validator: PublicKeyHash }, } diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index b40182791..6c09ae537 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -806,7 +806,8 @@ impl ChainManager { return; } }; - let protocol_version = get_protocol_version(Some(block.block_header.beacon.checkpoint)); + let protocol_version = + get_protocol_version(Some(block.block_header.beacon.checkpoint)); if let Some(best_candidate) = &self.best_candidate { let best_hash = best_candidate.block.hash(); diff --git a/validations/src/validations.rs b/validations/src/validations.rs index ea5963832..5e034bade 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -34,10 +34,7 @@ use witnet_data_structures::{ get_protocol_version, proto::versioning::{ProtocolVersion, VersionedHashable}, radon_report::{RadonReport, ReportContext}, - staking::{ - prelude::StakeKey, - stakes::Stakes, - }, + staking::{prelude::StakeKey, stakes::Stakes}, transaction::{ CommitTransaction, DRTransaction, MintTransaction, RevealTransaction, StakeTransaction, TallyTransaction, Transaction, UnstakeTransaction, VTTransaction, @@ -46,7 +43,7 @@ use witnet_data_structures::{ types::visitor::Visitor, utxo_pool::{Diff, UnspentOutputsPool, UtxoDiff}, vrf::{BlockEligibilityClaim, DataRequestEligibilityClaim, VrfCtx}, - wit::{NANOWITS_PER_WIT, Wit}, + wit::{Wit, NANOWITS_PER_WIT}, }; use witnet_rad::{ conditions::{ @@ -61,7 +58,7 @@ use witnet_rad::{ use crate::eligibility::{ current::{ - Eligible, Eligibility, + Eligibility, Eligible, IneligibilityReason::{InsufficientPower, NotStaking}, }, legacy::*, @@ -2076,8 +2073,10 @@ pub fn validate_block( let validator = block.block_sig.public_key.pkh(); let validator_key = StakeKey::from((validator, validator)); let eligibility = stakes.mining_eligibility(validator_key, block_epoch); - if eligibility == Ok(Eligible::No(InsufficientPower)) || eligibility == Ok(Eligible::No(NotStaking)) { - return Err(BlockError::ValidatorNotEligible{ validator }.into()); + if eligibility == Ok(Eligible::No(InsufficientPower)) + || eligibility == Ok(Eligible::No(NotStaking)) + { + return Err(BlockError::ValidatorNotEligible { validator }.into()); } Hash::max() @@ -2322,9 +2321,7 @@ pub fn compare_block_candidates( .cmp(&b2_vrf_hash) .reverse() // Bigger block implies worse block candidate - .then( - b1_hash.cmp(&b2_hash).reverse() - ) + .then(b1_hash.cmp(&b2_hash).reverse()) } else { let section1 = s.slot(&b1_vrf_hash); let section2 = s.slot(&b2_vrf_hash); From f8e71974d8a26371c64771ea1c603a5eaf462b5a Mon Sep 17 00:00:00 2001 From: drcpu Date: Thu, 2 May 2024 20:19:09 +0200 Subject: [PATCH 42/83] fix(cli): when require_confirmation is false, the staking transaction should still be built --- src/cli/node/json_rpc_client.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/cli/node/json_rpc_client.rs b/src/cli/node/json_rpc_client.rs index cd345e139..de7d04754 100644 --- a/src/cli/node/json_rpc_client.rs +++ b/src/cli/node/json_rpc_client.rs @@ -964,14 +964,14 @@ pub fn send_st( build_stake_params.fee = prompt_user_for_priority_selection(estimates)?; } - let confirmation = if requires_confirmation.unwrap_or(true) { - let params = BuildStakeParams { - dry_run: true, - ..build_stake_params.clone() - }; - let (dry, _): (BuildStakeResponse, _) = - issue_method("stake", Some(params), &mut stream, id.next())?; + let params = BuildStakeParams { + dry_run: true, + ..build_stake_params.clone() + }; + let (dry, _): (BuildStakeResponse, _) = + issue_method("stake", Some(params), &mut stream, id.next())?; + let confirmation = if requires_confirmation.unwrap_or(true) { // 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)? { @@ -980,7 +980,7 @@ pub fn send_st( None } } else { - None + Some(dry) }; if let Some(dry) = confirmation { From 14752cedb01671d9b534884a0296d57630f52d35 Mon Sep 17 00:00:00 2001 From: drcpu Date: Mon, 6 May 2024 08:05:19 +0200 Subject: [PATCH 43/83] (fix): switch query-stakes arguments --- src/cli/node/with_node.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/node/with_node.rs b/src/cli/node/with_node.rs index 0a45cbd3a..fbd9c097a 100644 --- a/src/cli/node/with_node.rs +++ b/src/cli/node/with_node.rs @@ -292,9 +292,9 @@ pub fn exec_cmd( } Command::QueryStakes { node, - withdrawer, validator, - } => rpc::query_stakes(node.unwrap_or(default_jsonrpc), withdrawer, validator), + withdrawer, + } => rpc::query_stakes(node.unwrap_or(default_jsonrpc), validator, withdrawer), } } From 28c567c971c23531ba4b877d2c605b07de27d90a Mon Sep 17 00:00:00 2001 From: drcpu Date: Mon, 6 May 2024 18:20:00 +0200 Subject: [PATCH 44/83] fix(mining): fix consolidating the genesis block (current epoch is still None) --- node/src/actors/chain_manager/mod.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index 6c09ae537..1d43f84d9 100644 --- a/node/src/actors/chain_manager/mod.rs +++ b/node/src/actors/chain_manager/mod.rs @@ -928,8 +928,18 @@ impl ChainManager { let current_epoch = if let Some(epoch) = self.current_epoch { epoch } else { - log::error!("Current epoch not loaded in ChainManager"); - return; + // If there is no epoch set, it's because the chain is yet to be bootstrapped, or because of a data race + match self.chain_state.chain_info.as_ref() { + // If the chain is yet to be bootstrapped (the block we are processing is the genesis block), set the epoch to zero + Some(chain_info) if chain_info.consensus_constants.genesis_hash == block.hash() => { + 0 + } + // In case of data race, shortcut the function + _ => { + log::error!("Current epoch not loaded in ChainManager"); + return; + } + } }; match self.chain_state { From 02dccce500befbdbcdbe1459956a21883dad0820 Mon Sep 17 00:00:00 2001 From: drcpu Date: Tue, 7 May 2024 21:07:16 +0200 Subject: [PATCH 45/83] feat(cli): make all stake arguments required and explicitly check validator correctness --- node/src/actors/json_rpc/api.rs | 64 +++++++-------------------------- node/src/actors/messages.rs | 4 +-- src/cli/node/json_rpc_client.rs | 15 ++++++-- src/cli/node/with_node.rs | 15 +++++--- 4 files changed, 37 insertions(+), 61 deletions(-) diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index 76c4a1d6c..f8aee9a51 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -1948,62 +1948,24 @@ 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 { - 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 - ); - - address - }; + let withdrawer = PublicKeyHash::from_bech32(get_environment(), ¶ms.withdrawer) + .map_err(internal_error)?; + log::debug!( + "[STAKE] A withdrawer address was provided: {}", + params.withdrawer + ); // This is the actual message that gets signed as part of the authorization let msg = withdrawer.as_secp256k1_msg(); - // If no authorization message is provided, generate a new one using the withdrawer address - let authorization = if let Some(signature) = params.authorization { - 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 {}", - 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 { - let signature = signature_mngr::sign_data(msg) - .map(|res| res.map_err(internal_error)) - .await - .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 - }; + let authorization = KeyedSignature::from_recoverable_hex(¶ms.authorization, &msg) + .map_err(internal_error)?; + let validator = PublicKeyHash::from_public_key(&authorization.public_key); + log::debug!( + "[STAKE] A stake authorization was provided, and it was signed by validator {}", + validator + ); let key = StakeKey { validator, diff --git a/node/src/actors/messages.rs b/node/src/actors/messages.rs index fe605488d..f1c3ab199 100644 --- a/node/src/actors/messages.rs +++ b/node/src/actors/messages.rs @@ -248,13 +248,13 @@ impl Message for BuildStake { pub struct BuildStakeParams { /// Authorization signature and public key #[serde(default)] - pub authorization: Option>, + pub authorization: String, /// List of `ValueTransferOutput`s #[serde(default)] pub value: u64, /// Withdrawer #[serde(default)] - pub withdrawer: Option>, + pub withdrawer: String, /// Fee #[serde(default)] pub fee: Fee, diff --git a/src/cli/node/json_rpc_client.rs b/src/cli/node/json_rpc_client.rs index de7d04754..97692bdf3 100644 --- a/src/cli/node/json_rpc_client.rs +++ b/src/cli/node/json_rpc_client.rs @@ -46,7 +46,7 @@ use witnet_node::actors::{ }, messages::{ AuthorizeStake, BuildDrt, BuildStakeParams, BuildStakeResponse, BuildVtt, GetBalanceTarget, - GetReputationResult, MagicEither, SignalingInfo, StakeAuthorization, + GetReputationResult, SignalingInfo, StakeAuthorization, }, }; use witnet_rad::types::RadonTypes; @@ -867,8 +867,9 @@ pub fn send_dr( pub fn send_st( addr: SocketAddr, value: u64, - authorization: Option>, - withdrawer: Option>, + authorization: String, + validator: String, + withdrawer: String, fee: Option, sorted_bigger: Option, requires_confirmation: Option, @@ -971,6 +972,14 @@ pub fn send_st( let (dry, _): (BuildStakeResponse, _) = issue_method("stake", Some(params), &mut stream, id.next())?; + if validator != dry.validator.to_string() { + bail!( + "The specified validator ({}) does not match the validator recovered from the authorization string ({}), please double check all arguments.", + validator, + dry.validator.to_string(), + ); + } + let confirmation = if requires_confirmation.unwrap_or(true) { // Exactly what it says: shows all the facts about the staking transaction, and expects confirmation through // user input diff --git a/src/cli/node/with_node.rs b/src/cli/node/with_node.rs index fbd9c097a..6ab1f40ed 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, MagicEither}; +use witnet_node::actors::messages::GetBalanceTarget; use super::json_rpc_client as rpc; @@ -273,6 +273,7 @@ pub fn exec_cmd( node, value, authorization, + validator, withdrawer, fee, require_confirmation, @@ -280,8 +281,9 @@ pub fn exec_cmd( } => rpc::send_st( node.unwrap_or(default_jsonrpc), value, - authorization.map(MagicEither::Left), - withdrawer.map(MagicEither::Left), + authorization, + validator, + withdrawer, fee.map(Fee::absolute_from_nanowits), None, require_confirmation, @@ -766,10 +768,13 @@ pub enum Command { value: u64, /// Stake authorization code (the withdrawer address, signed by the validator node) #[structopt(long = "authorization")] - authorization: Option, + authorization: String, + /// Validator + #[structopt(long = "validator")] + validator: String, /// Withdrawer #[structopt(long = "withdrawer")] - withdrawer: Option, + withdrawer: String, /// Fee #[structopt(long = "fee")] fee: Option, From 793ff18620e7f75d8c0980c8141fba93c76a4e59 Mon Sep 17 00:00:00 2001 From: drcpu Date: Tue, 14 May 2024 23:16:45 +0200 Subject: [PATCH 46/83] Applied suggestions --- node/src/actors/json_rpc/api.rs | 13 +++++++++---- node/src/actors/messages.rs | 10 ++++++++-- src/cli/node/json_rpc_client.rs | 14 ++++++++------ src/cli/node/with_node.rs | 8 ++++---- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index f8aee9a51..3eb157310 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -1949,17 +1949,22 @@ pub async fn stake(params: Result) -> JsonRpcResult { // Short-circuit if parameters are wrong let params = params?; - let withdrawer = PublicKeyHash::from_bech32(get_environment(), ¶ms.withdrawer) + let withdrawer = params + .withdrawer + .clone() + .try_do_magic(|hex_str| PublicKeyHash::from_bech32(get_environment(), &hex_str)) .map_err(internal_error)?; log::debug!( - "[STAKE] A withdrawer address was provided: {}", - params.withdrawer + "[STAKE] Creating stake transaction with withdrawer address: {}", + withdrawer ); // This is the actual message that gets signed as part of the authorization let msg = withdrawer.as_secp256k1_msg(); - let authorization = KeyedSignature::from_recoverable_hex(¶ms.authorization, &msg) + let authorization = params + .authorization + .try_do_magic(|hex_str| KeyedSignature::from_recoverable_hex(&hex_str, &msg)) .map_err(internal_error)?; let validator = PublicKeyHash::from_public_key(&authorization.public_key); log::debug!( diff --git a/node/src/actors/messages.rs b/node/src/actors/messages.rs index f1c3ab199..3b31b0ee7 100644 --- a/node/src/actors/messages.rs +++ b/node/src/actors/messages.rs @@ -248,13 +248,13 @@ impl Message for BuildStake { pub struct BuildStakeParams { /// Authorization signature and public key #[serde(default)] - pub authorization: String, + pub authorization: MagicEither, /// List of `ValueTransferOutput`s #[serde(default)] pub value: u64, /// Withdrawer #[serde(default)] - pub withdrawer: String, + pub withdrawer: MagicEither, /// Fee #[serde(default)] pub fee: Fee, @@ -1445,3 +1445,9 @@ impl MagicEither { } } } + +impl Default for MagicEither { + fn default() -> Self { + MagicEither::Right(R::default()) + } +} diff --git a/src/cli/node/json_rpc_client.rs b/src/cli/node/json_rpc_client.rs index 97692bdf3..4c806fdb9 100644 --- a/src/cli/node/json_rpc_client.rs +++ b/src/cli/node/json_rpc_client.rs @@ -46,7 +46,7 @@ use witnet_node::actors::{ }, messages::{ AuthorizeStake, BuildDrt, BuildStakeParams, BuildStakeResponse, BuildVtt, GetBalanceTarget, - GetReputationResult, SignalingInfo, StakeAuthorization, + GetReputationResult, MagicEither, SignalingInfo, StakeAuthorization, }, }; use witnet_rad::types::RadonTypes; @@ -867,9 +867,9 @@ pub fn send_dr( pub fn send_st( addr: SocketAddr, value: u64, - authorization: String, - validator: String, - withdrawer: String, + authorization: MagicEither, + validator: MagicEither, + withdrawer: MagicEither, fee: Option, sorted_bigger: Option, requires_confirmation: Option, @@ -972,10 +972,12 @@ pub fn send_st( let (dry, _): (BuildStakeResponse, _) = issue_method("stake", Some(params), &mut stream, id.next())?; - if validator != dry.validator.to_string() { + let validator_address = validator + .try_do_magic(|hex_str| PublicKeyHash::from_bech32(get_environment(), &hex_str))?; + if validator_address != dry.validator { bail!( "The specified validator ({}) does not match the validator recovered from the authorization string ({}), please double check all arguments.", - validator, + validator_address, dry.validator.to_string(), ); } diff --git a/src/cli/node/with_node.rs b/src/cli/node/with_node.rs index 6ab1f40ed..315c80aef 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; @@ -281,9 +281,9 @@ pub fn exec_cmd( } => rpc::send_st( node.unwrap_or(default_jsonrpc), value, - authorization, - validator, - withdrawer, + MagicEither::Left(authorization), + MagicEither::Left(validator), + MagicEither::Left(withdrawer), fee.map(Fee::absolute_from_nanowits), None, require_confirmation, From cb05de5a6c221f3f7796852859ab7dd940a25812 Mon Sep 17 00:00:00 2001 From: drcpu Date: Thu, 16 May 2024 21:46:38 +0200 Subject: [PATCH 47/83] fix(mining): do not include multiple staking transactions for a validator in a single block --- node/src/actors/chain_manager/mining.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 8ad3a7836..e98083cd7 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -983,7 +983,17 @@ pub fn build_block( } } + let mut included_validators = HashSet::::new(); for st_tx in transactions_pool.st_iter() { + let validator_pkh = st_tx.body.output.authorization.public_key.pkh(); + if included_validators.contains(&validator_pkh) { + log::debug!( + "Cannot include more than one stake transaction for {} in a single block", + validator_pkh + ); + continue; + } + let transaction_weight = st_tx.weight(); let transaction_fee = match st_transaction_fee(st_tx, &utxo_diff, epoch, epoch_constants) { Ok(x) => x, @@ -1014,6 +1024,8 @@ pub fn build_block( if st_weight > max_st_weight.saturating_sub(min_st_weight) { break; } + + included_validators.insert(validator_pkh); } // Include Mint Transaction by miner From 1df5651667ac66ca45ce65519ad9232f5ad5506f Mon Sep 17 00:00:00 2001 From: aeweda Date: Mon, 18 Mar 2024 05:07:06 +0400 Subject: [PATCH 48/83] chore: fix netcat & upgrade ubuntu --- docker/witnet-rust/Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/witnet-rust/Dockerfile b/docker/witnet-rust/Dockerfile index 50e274e0e..69bcf44c3 100644 --- a/docker/witnet-rust/Dockerfile +++ b/docker/witnet-rust/Dockerfile @@ -1,14 +1,14 @@ -FROM --platform=$TARGETPLATFORM ubuntu:jammy +FROM ubuntu:noble # Install basic environment dependencies -RUN apt-get update && apt-get install -y --no-install-recommends \ +RUN apt update && apt install -y --no-install-recommends \ ca-certificates \ curl \ - netcat \ + netcat-traditional \ jq # Clean up apt packages so the docker image is as compact as possible -RUN apt-get clean && apt-get autoremove +RUN apt clean && apt autoremove # Set needed environment variables ENV RUST_BACKTRACE=1 From 0e1be39b3b0956656a8f364f0bba1fbeab368b50 Mon Sep 17 00:00:00 2001 From: aeweda Date: Mon, 18 Mar 2024 05:07:54 +0400 Subject: [PATCH 49/83] fix: return platform specification --- docker/witnet-rust/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/witnet-rust/Dockerfile b/docker/witnet-rust/Dockerfile index 69bcf44c3..dd7566fd9 100644 --- a/docker/witnet-rust/Dockerfile +++ b/docker/witnet-rust/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:noble +FROM --platform=$TARGETPLATFORM ubuntu:noble # Install basic environment dependencies RUN apt update && apt install -y --no-install-recommends \ From 8ec3a3406d841551758773a7241818d6fa23b16b Mon Sep 17 00:00:00 2001 From: tommytrg Date: Tue, 30 Jan 2024 20:16:24 +0100 Subject: [PATCH 50/83] test(superblock): update two_thirds_consensus --- data_structures/src/superblock.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/data_structures/src/superblock.rs b/data_structures/src/superblock.rs index 8557596ed..f75407151 100644 --- a/data_structures/src/superblock.rs +++ b/data_structures/src/superblock.rs @@ -2611,16 +2611,17 @@ mod tests { #[test] fn test_two_thirds_consensus() { - assert!(!two_thirds_consensus(2, 3)); + assert!(!two_thirds_consensus(1, 3)); + assert!(two_thirds_consensus(2, 3)); assert!(two_thirds_consensus(3, 3)); - assert!(!two_thirds_consensus(2, 4)); + assert!(!two_thirds_consensus(1, 4)); assert!(two_thirds_consensus(3, 4)); - assert!(!two_thirds_consensus(21, 32)); - assert!(two_thirds_consensus(22, 32)); - assert!(!two_thirds_consensus(22, 33)); - assert!(two_thirds_consensus(23, 33)); - assert!(!two_thirds_consensus(22, 34)); - assert!(two_thirds_consensus(23, 34)); + assert!(!two_thirds_consensus(20, 32)); + assert!(two_thirds_consensus(21, 32)); + assert!(!two_thirds_consensus(21, 33)); + assert!(two_thirds_consensus(22, 33)); + assert!(!two_thirds_consensus(21, 34)); + assert!(two_thirds_consensus(22, 34)); } #[test] @@ -2702,7 +2703,7 @@ mod tests { // 4 valid votes -> SameAsLocal let mut v3 = SuperBlockVote::new_unsigned(sb2.hash(), 2); - v3.secp256k1_signature.public_key = p3; + v3.secp256k1_signature.public_key = p3.clone(); assert_eq!(sbs.add_vote(&v3, 2), AddSuperBlockVote::ValidWithSameHash); let mut v4 = SuperBlockVote::new_unsigned(sb2.hash(), 2); v4.secp256k1_signature.public_key = p4; @@ -2710,13 +2711,16 @@ mod tests { assert_eq!(sbs.has_consensus(), SuperBlockConsensus::SameAsLocal); - // 2 valid votes, 2 double votes and 1 missing vote -> NoConsensus + // 1 valid votes, 3 double votes and 1 missing vote -> NoConsensus let mut v1_b = SuperBlockVote::new_unsigned(Hash::SHA256([2; 32]), 2); v1_b.secp256k1_signature.public_key = p1; assert_eq!(sbs.add_vote(&v1_b, 2), AddSuperBlockVote::DoubleVote); let mut v2_b = SuperBlockVote::new_unsigned(Hash::SHA256([2; 32]), 2); v2_b.secp256k1_signature.public_key = p2; assert_eq!(sbs.add_vote(&v2_b, 2), AddSuperBlockVote::DoubleVote); + let mut v3_b = SuperBlockVote::new_unsigned(Hash::SHA256([2; 32]), 2); + v3_b.secp256k1_signature.public_key = p3; + assert_eq!(sbs.add_vote(&v3_b, 2), AddSuperBlockVote::DoubleVote); assert_eq!(sbs.has_consensus(), SuperBlockConsensus::NoConsensus); } From a9b5fe19decb0899b31b344a62a867e40fd7a385 Mon Sep 17 00:00:00 2001 From: tommytrg Date: Mon, 25 Mar 2024 19:14:05 +0100 Subject: [PATCH 51/83] chore(cargo): adapt codebase to latest Rust (1.77) --- bridges/centralized-ethereum/src/config.rs | 2 +- config/src/loaders/toml.rs | 2 +- net/src/server/ws/mod.rs | 1 + rad/src/operators/array.rs | 2 +- rad/src/operators/map.rs | 2 +- src/cli/node/json_rpc_client.rs | 6 +++--- wallet/src/actors/worker/methods.rs | 10 +++++----- 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/bridges/centralized-ethereum/src/config.rs b/bridges/centralized-ethereum/src/config.rs index 8a4affce5..55a25fbd9 100644 --- a/bridges/centralized-ethereum/src/config.rs +++ b/bridges/centralized-ethereum/src/config.rs @@ -120,7 +120,7 @@ pub fn from_env() -> Result { thread_local! { /// Thread-local flag to indicate the `nested_toml_if_using_envy` function that we are indeed /// using envy. - static USING_ENVY: Cell = Cell::new(false); + static USING_ENVY: Cell = const { Cell::new(false) }; } /// If using the `envy` crate to deserialize this value, try to deserialize it as a TOML string. diff --git a/config/src/loaders/toml.rs b/config/src/loaders/toml.rs index e44206152..1b6b23e24 100644 --- a/config/src/loaders/toml.rs +++ b/config/src/loaders/toml.rs @@ -48,7 +48,7 @@ pub fn from_str(contents: &str) -> Result { } #[cfg(test)] -thread_local!(static FILE_CONTENTS: Cell<&'static str> = Cell::new("")); +thread_local!(static FILE_CONTENTS: Cell<&'static str> = const { Cell::new("") }); #[cfg(test)] fn read_file_contents(_filename: &Path, contents: &mut String) -> io::Result { diff --git a/net/src/server/ws/mod.rs b/net/src/server/ws/mod.rs index b114b07b3..a0e0cbe00 100644 --- a/net/src/server/ws/mod.rs +++ b/net/src/server/ws/mod.rs @@ -11,6 +11,7 @@ pub use error::Error; type PubSubHandler = pubsub::PubSubHandler>; +// We need to pass the server as an argument to avoid dropping it before the start had finished /// TODO: doc #[allow(dead_code)] pub struct Server(server::Server); diff --git a/rad/src/operators/array.rs b/rad/src/operators/array.rs index a4136d9f8..80fbc22ab 100644 --- a/rad/src/operators/array.rs +++ b/rad/src/operators/array.rs @@ -64,7 +64,7 @@ fn inner_get(input: &RadonArray, args: &[Value]) -> Result input .value() .get(index) - .map(Clone::clone) + .cloned() .ok_or_else(|| not_found(index)) } diff --git a/rad/src/operators/map.rs b/rad/src/operators/map.rs index f0ccdc105..d02d1dc8c 100644 --- a/rad/src/operators/map.rs +++ b/rad/src/operators/map.rs @@ -22,7 +22,7 @@ fn inner_get(input: &RadonMap, args: &[Value]) -> Result { input .value() .get(&key) - .map(Clone::clone) + .cloned() .ok_or_else(|| not_found(key)) } diff --git a/src/cli/node/json_rpc_client.rs b/src/cli/node/json_rpc_client.rs index 4c806fdb9..f82db9ea3 100644 --- a/src/cli/node/json_rpc_client.rs +++ b/src/cli/node/json_rpc_client.rs @@ -1953,10 +1953,10 @@ struct JsonRpcError { /// Id. Can be null, a number, or a string #[derive(Debug, Deserialize)] #[serde(untagged)] -enum Id<'a> { +enum Id { Null, - Number(u64), - String(&'a str), + Number(), + String(), } /// A failed request returns an error with code and message diff --git a/wallet/src/actors/worker/methods.rs b/wallet/src/actors/worker/methods.rs index 4904d775b..23efd591a 100644 --- a/wallet/src/actors/worker/methods.rs +++ b/wallet/src/actors/worker/methods.rs @@ -625,7 +625,7 @@ impl Worker { .body .outputs .get(output.output_index as usize) - .map(ValueTransferOutput::clone) + .cloned() .ok_or_else(|| { Error::OutputIndexNotFound(output.output_index, format!("{:?}", txn)) }), @@ -633,21 +633,21 @@ impl Worker { .body .outputs .get(output.output_index as usize) - .map(ValueTransferOutput::clone) + .cloned() .ok_or_else(|| { Error::OutputIndexNotFound(output.output_index, format!("{:?}", txn)) }), Transaction::Tally(tally) => tally .outputs .get(output.output_index as usize) - .map(ValueTransferOutput::clone) + .cloned() .ok_or_else(|| { Error::OutputIndexNotFound(output.output_index, format!("{:?}", txn)) }), Transaction::Mint(mint) => mint .outputs .get(output.output_index as usize) - .map(ValueTransferOutput::clone) + .cloned() .ok_or_else(|| { Error::OutputIndexNotFound(output.output_index, format!("{:?}", txn)) }), @@ -655,7 +655,7 @@ impl Worker { .body .outputs .get(output.output_index as usize) - .map(ValueTransferOutput::clone) + .cloned() .ok_or_else(|| { Error::OutputIndexNotFound(output.output_index, format!("{:?}", txn)) }), From de5775e59ce02cc38686cd08c6e50f784f460d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ad=C3=A1n=20SDPC?= Date: Wed, 29 May 2024 17:25:49 +0200 Subject: [PATCH 52/83] chore(data_structures): rename aux.rs file for Windows compatibility --- data_structures/src/staking/{aux.rs => helpers.rs} | 0 data_structures/src/staking/mod.rs | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename data_structures/src/staking/{aux.rs => helpers.rs} (100%) diff --git a/data_structures/src/staking/aux.rs b/data_structures/src/staking/helpers.rs similarity index 100% rename from data_structures/src/staking/aux.rs rename to data_structures/src/staking/helpers.rs diff --git a/data_structures/src/staking/mod.rs b/data_structures/src/staking/mod.rs index 1a5b21418..d678c71e3 100644 --- a/data_structures/src/staking/mod.rs +++ b/data_structures/src/staking/mod.rs @@ -1,7 +1,7 @@ #![deny(missing_docs)] /// Auxiliary convenience types and data structures. -pub mod aux; +pub mod helpers; /// Constants related to the staking functionality. pub mod constants; /// Errors related to the staking functionality. @@ -16,7 +16,7 @@ pub mod stakes; pub mod prelude { pub use crate::capabilities::*; - pub use super::aux::*; + pub use super::helpers::*; pub use super::constants::*; pub use super::errors::*; pub use super::stake::*; From 70761dfd5e37375f2f310cdd3a11fcd78ba149d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Tue, 10 Oct 2023 12:48:47 +0200 Subject: [PATCH 53/83] feat: implement HttpHead as new RADType --- data_structures/src/chain/mod.rs | 10 ++++-- data_structures/src/proto/mod.rs | 2 ++ data_structures/src/serialization_helpers.rs | 4 +-- rad/src/lib.rs | 36 +++++++++++++++++--- schemas/witnet/witnet.proto | 3 +- 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index 1ed721de9..ef8ddd7a9 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -1889,11 +1889,14 @@ pub enum RADType { /// HTTP POST request #[serde(rename = "HTTP-POST")] HttpPost, + /// HTTP HEAD request + #[serde(rename = "HTTP-HEAD")] + HttpHead, } impl RADType { pub fn is_http(&self) -> bool { - matches!(self, RADType::HttpGet | RADType::HttpPost) + matches!(self, RADType::HttpGet | RADType::HttpPost | RADType::HttpHead) } } @@ -1948,7 +1951,7 @@ pub struct RADRetrieve { pub script: Vec, /// Body of a HTTP-POST request pub body: Vec, - /// Extra headers of a HTTP-GET or HTTP-POST request + /// Extra headers of a HTTP-GET, HTTP-POST or HTTP-HEAD request pub headers: Vec<(String, String)>, } @@ -2056,7 +2059,8 @@ impl RADRetrieve { &[Field::Kind, Field::Url, Field::Script], &[Field::Body, Field::Headers], ) - } + }, + RADType::HttpHead => check(&[Field::Kind, Field::Url, Field::Script], &[Field::Headers]) } } diff --git a/data_structures/src/proto/mod.rs b/data_structures/src/proto/mod.rs index 6fc6ba0e5..2e7ba0e2c 100644 --- a/data_structures/src/proto/mod.rs +++ b/data_structures/src/proto/mod.rs @@ -52,6 +52,7 @@ impl ProtobufConvert for chain::RADType { chain::RADType::HttpGet => witnet::DataRequestOutput_RADRequest_RADType::HttpGet, chain::RADType::Rng => witnet::DataRequestOutput_RADRequest_RADType::Rng, chain::RADType::HttpPost => witnet::DataRequestOutput_RADRequest_RADType::HttpPost, + chain::RADType::HttpHead => witnet::DataRequestOutput_RADRequest_RADType::HttpHead, } } @@ -61,6 +62,7 @@ impl ProtobufConvert for chain::RADType { witnet::DataRequestOutput_RADRequest_RADType::HttpGet => chain::RADType::HttpGet, witnet::DataRequestOutput_RADRequest_RADType::Rng => chain::RADType::Rng, witnet::DataRequestOutput_RADRequest_RADType::HttpPost => chain::RADType::HttpPost, + witnet::DataRequestOutput_RADRequest_RADType::HttpHead => chain::RADType::HttpHead, }) } } diff --git a/data_structures/src/serialization_helpers.rs b/data_structures/src/serialization_helpers.rs index 7f7d7d4e6..a8d59ee69 100644 --- a/data_structures/src/serialization_helpers.rs +++ b/data_structures/src/serialization_helpers.rs @@ -360,7 +360,7 @@ struct RADRetrieveSerializationHelperJson { /// Body of a HTTP-POST request #[serde(default, skip_serializing_if = "Vec::is_empty")] pub body: Vec, - /// Extra headers of a HTTP-GET or HTTP-POST request + /// Extra headers of a HTTP-GET, HTTP-HEAD or HTTP-POST request #[serde(default, skip_serializing_if = "Vec::is_empty")] pub headers: Vec<(String, String)>, } @@ -377,7 +377,7 @@ struct RADRetrieveSerializationHelperBincode { pub script: Vec, /// Body of a HTTP-POST request pub body: Vec, - /// Extra headers of a HTTP-GET or HTTP-POST request + /// Extra headers of a HTTP-GET, HTTP-HEAD or HTTP-POST request pub headers: Vec<(String, String)>, } diff --git a/rad/src/lib.rs b/rad/src/lib.rs index 64c826780..056323721 100644 --- a/rad/src/lib.rs +++ b/rad/src/lib.rs @@ -25,10 +25,11 @@ use crate::{ create_radon_script_from_filters_and_reducer, execute_radon_script, unpack_radon_script, RadonScriptExecutionSettings, }, - types::{array::RadonArray, bytes::RadonBytes, string::RadonString, RadonTypes}, + types::{array::RadonArray, bytes::RadonBytes, string::RadonString, map::RadonMap, RadonTypes}, user_agents::UserAgent, }; use core::convert::From; +use std::collections::BTreeMap; use witnet_net::client::http::{WitnetHttpBody, WitnetHttpRequest}; pub mod conditions; @@ -173,6 +174,25 @@ fn string_response_with_data_report( execute_radon_script(input, &radon_script, context, settings) } +/// Handle HTTP-HEAD response with data, and return a `RadonReport`. +fn headers_response_with_data_report( + retrieve: &RADRetrieve, + response: &str, + context: &mut ReportContext, + settings: RadonScriptExecutionSettings, +) -> Result> { + let headers: BTreeMap = response.split("\r\n").map(|line| { + let parts: Vec<&str> = line.split(":").map(|part| part.trim()).collect(); + // todo: check there are two parts, and two parts only + // todo: make sure that values from repeated keys get appended within a RadonArray + (String::from(parts[0]), RadonTypes::from(RadonString::from(parts[1]))) + }).collect(); + let input = RadonTypes::from(RadonMap::from(headers)); + let radon_script = unpack_radon_script(&retrieve.script)?; + + execute_radon_script(input, &radon_script, context, settings) +} + /// Handle Rng response with data report fn rng_response_with_data_report( response: &str, @@ -196,7 +216,10 @@ pub fn run_retrieval_with_data_report( RADType::Rng => rng_response_with_data_report(response, context), RADType::HttpPost => { string_response_with_data_report(retrieve, response, context, settings) - } + }, + RADType::HttpHead => { + headers_response_with_data_report(retrieve, response, context, settings) + }, _ => Err(RadError::UnknownRetrieval), } } @@ -214,7 +237,7 @@ pub fn run_retrieval_with_data( .map(RadonReport::into_inner) } -/// Handle generic HTTP (GET/POST) response +/// Handle generic HTTP (GET/POST/HEAD) response async fn http_response( retrieve: &RADRetrieve, context: &mut ReportContext, @@ -258,7 +281,11 @@ async fn http_response( builder.method("POST").uri(&retrieve.url), WitnetHttpBody::from(retrieve.body.clone()), ) - } + }, + RADType::HttpHead => ( + builder.method("HEAD").uri(&retrieve.url), + WitnetHttpBody::empty(), + ), _ => panic!( "Called http_response with invalid retrieval kind {:?}", retrieve.kind @@ -357,6 +384,7 @@ pub async fn run_retrieval_report( RADType::HttpGet => http_response(retrieve, context, settings, client).await, RADType::Rng => rng_response(context, settings).await, RADType::HttpPost => http_response(retrieve, context, settings, client).await, + RADType::HttpHead => http_response(retrieve, context, settings, client).await, _ => Err(RadError::UnknownRetrieval), } } diff --git a/schemas/witnet/witnet.proto b/schemas/witnet/witnet.proto index ea424cd31..9a56022a6 100644 --- a/schemas/witnet/witnet.proto +++ b/schemas/witnet/witnet.proto @@ -179,6 +179,7 @@ message DataRequestOutput { HttpGet = 1; Rng = 2; HttpPost = 3; + HttpHead = 4; } message RADFilter { uint32 op = 1; @@ -191,7 +192,7 @@ message DataRequestOutput { bytes script = 3; // Body of HTTP-POST request bytes body = 4; - // Extra headers for HTTP-GET and HTTP-POST requests + // Extra headers for HTTP-GET, HTTP-HEAD and HTTP-POST requests repeated StringPair headers = 5; } message RADAggregate { From 8c75da624e5e030847aac2f608aaa00ee16501e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Fri, 1 Mar 2024 16:44:37 +0100 Subject: [PATCH 54/83] chore(c-bridge): refactor config --- bridges/centralized-ethereum/.env.example | 45 +++++----- bridges/centralized-ethereum/README.md | 8 +- .../src/actors/dr_sender/tests.rs | 4 +- bridges/centralized-ethereum/src/config.rs | 84 ++++++++++--------- bridges/centralized-ethereum/src/lib.rs | 20 ++--- bridges/centralized-ethereum/src/main.rs | 83 +++++++++--------- witnet_centralized_ethereum_bridge.toml | 28 +++---- witnet_ethereum_bridge.toml | 12 +-- 8 files changed, 146 insertions(+), 138 deletions(-) diff --git a/bridges/centralized-ethereum/.env.example b/bridges/centralized-ethereum/.env.example index 9e58d54e4..fc601256d 100644 --- a/bridges/centralized-ethereum/.env.example +++ b/bridges/centralized-ethereum/.env.example @@ -1,22 +1,27 @@ # Sample env file -# Note that the bridge does not read .env files, you need to import these variables into the environment using some external tool -# adjustable params -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_ACCOUNT: "0x8c49CAfC4542D9EA9107D4E48412ACEd2A68aA77" -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_GAS_LIMITS: "report_result = 3_000_000" -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_NUM_CONFIRMATIONS: 8 -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_JSONRPC_ADDR: "10.5.1.4:21338" -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WRB_CONTRACT_ADDR: "0x58D8ECe142c60f5707594a7C1D90e46eAE5AF431" -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_DR_FEE_NANOWITS: 10000 -# not-to-be-changed params -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_CLIENT_URL: "http://gateway:8535" -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_MAX_DR_VALUE_NANOWITS: 100000000000 # 1 WIT -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_MAX_RESULT_SIZE: 100 # 100 bytes -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_REQUEST_EXAMPLE_CONTRACT_ADDR: "0xEaA9e7Ea612b169f5b41cfF86dA6322f57264a19" -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_STORAGE: "db_path = \"data/polygon_goerli\"" +# Note that the bridge does not read .env files, you need to import these variables +# into the environment using some external tool + +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_FROM: "0x8c49CAfC4542D9EA9107D4E48412ACEd2A68aA77" +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_FROM_BALANCE_LIMIT: 100000000000000000 # 0.1 ETH +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_GAS_LIMITS: "report_result = 3_000_000" # (optional) +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_JSONRPC_URL: "http://gateway:8535" +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_MAX_BATCH_SIZE: 64 +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_NANOWIT_WEI_PRICE: 10000 # Price of nanoWIT in WEI (optional) +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_NEW_DRS_POLLING_RATE_MS: 30000 # 30 secs, not less than average EVM's block period +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_TXS_CONFIRMATIONS: 2 +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_TXS_TIMEOUT_MS: 900000 # 15 min +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_WITNET_ORACLE: "0x58D8ECe142c60f5707594a7C1D90e46eAE5AF431" + +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_DR_MIN_COLLATERAL_NANOWITS: 20000000000 # 20 WIT +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_DR_MAX_FEE_NANOWITS: 100000 # 0.1 milliWIT +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_DR_MAX_RESULT_SIZE: 64 # 64 bytes +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_DR_MAX_VALUE_NANOWITS: 100000000000 # 1 WIT +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_DR_TXS_POLLING_RATE_MS: 45000 # 45 secs +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_DR_TXS_TIMEOUT_MS: 600000 # 10 min + +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_JSONRPC_SOCKET: "10.5.1.4:21338" WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WITNET_TESTNET: "false" -# timing params -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_DR_TX_UNRESOLVED_TIMEOUT_MS: 600000 # 10 min -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_CONFIRMATION_TIMEOUT_MS: 900000 # 15 min -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_NEW_DR_POLLING_RATE_MS: 45000 # 45 secs -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WIT_TALLY_POLLING_RATE_MS: 45000 # 45 secs -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_WIT_DR_SENDER_POLLING_RATE_MS: 45000 # 45 secs + +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_STORAGE: "db_path = \"data/storage\"" +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_STORAGE_SKIP_FIRST: 0 diff --git a/bridges/centralized-ethereum/README.md b/bridges/centralized-ethereum/README.md index c2a88f7fa..ef03eb43d 100644 --- a/bridges/centralized-ethereum/README.md +++ b/bridges/centralized-ethereum/README.md @@ -103,11 +103,11 @@ We need to modify the configuration file that the bridge will use to establish c There are some key fields that need to be edited here to make the bridge connect properly to your Ethereum/Witnet nodes. These are: -- *witnet_jsonrpc_addr*: make sure this address is identical to the jsonRPC address of your Witnet node. -- *eth_client_url*: make sure this field points to address where your ethereum client is running. -- *wrb_contract_addr*: this field should contain the address of the WitnetRequestsBoard contract you wish your node to connect to. +- *witnet_jsonrpc_socket*: make sure this address is identical to the jsonRPC address of your Witnet node. +- *eth_jsonrpc_url*: make sure this field points to address where your ethereum client is running. +- *eth_witnet_oracle*: this field should contain the address of the WitnetRequestsBoard contract you wish your node to connect to. - *block_relay_contract_add*: this field should contain the address of the BlockRelay contract you wish your node to connect to. -- *eth_account*: this is the account you are using in your ethereum client. +- *eth_from*: this is the account you are using in your ethereum client. - **NOTE**: In the case of using ganache-cli, this needs to be the first account, since currently the only account that can relay Witnet blocks to Ethereum is the one that deployed the BlockRelay contract, which by default is the first account. diff --git a/bridges/centralized-ethereum/src/actors/dr_sender/tests.rs b/bridges/centralized-ethereum/src/actors/dr_sender/tests.rs index b492ea2b0..2992bc083 100644 --- a/bridges/centralized-ethereum/src/actors/dr_sender/tests.rs +++ b/bridges/centralized-ethereum/src/actors/dr_sender/tests.rs @@ -113,7 +113,7 @@ fn deserialize_and_validate_dr_bytes_wip_0022() { }; let dro_bytes = dro.to_pb_bytes().unwrap(); - let max_dr_value_nanowits = 100_000_000_000; - let err = deserialize_and_validate_dr_bytes(&dro_bytes, max_dr_value_nanowits).unwrap_err(); + let witnet_dr_max_value_nanowits = 100_000_000_000; + let err = deserialize_and_validate_dr_bytes(&dro_bytes, witnet_dr_max_value_nanowits).unwrap_err(); assert_eq!(err.encode_cbor(), vec![216, 39, 129, 24, 224]); } diff --git a/bridges/centralized-ethereum/src/config.rs b/bridges/centralized-ethereum/src/config.rs index 55a25fbd9..6e8dfd2de 100644 --- a/bridges/centralized-ethereum/src/config.rs +++ b/bridges/centralized-ethereum/src/config.rs @@ -13,57 +13,61 @@ use witnet_data_structures::chain::Environment; #[derive(Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Config { - /// Address of the witnet node JSON-RPC server - pub witnet_jsonrpc_addr: SocketAddr, + + /// Ethereum account used to report data request results + pub eth_from: H160, + /// Ethereum account balance under which alerts will be logged + pub eth_from_balance_threshold: u64, + /// Gas limits for some methods. If missing, let the client estimate + #[serde(deserialize_with = "nested_toml_if_using_envy")] + pub eth_gas_limits: Gas, /// Url of the ethereum client - pub eth_client_url: String, - /// Address of the WitnetRequestsBoard deployed contract - pub wrb_contract_addr: H160, - /// Address of a Request example deployed contract - pub request_example_contract_addr: H160, - /// Ethereum account used to create the transactions - pub eth_account: H160, - /// Period to check for new requests in the WRB - pub eth_new_dr_polling_rate_ms: u64, - /// Period to check for completed requests in Witnet - pub wit_tally_polling_rate_ms: u64, - /// Period to post new requests to Witnet - pub wit_dr_sender_polling_rate_ms: u64, - /// If the data request has been sent to witnet but it is not included in a block, retry after this many milliseconds - pub dr_tx_unresolved_timeout_ms: Option, - /// Max value that will be accepted by the bridge node in a data request - pub max_dr_value_nanowits: u64, + pub eth_jsonrpc_url: String, + /// Max number of queries to be batched together + #[serde(default = "default_max_batch_size")] + pub eth_max_batch_size: u16, + /// Price of $nanoWit in Wei, used to improve estimation of report profits + pub eth_nanowit_wei_price: Option, + /// Polling period for checking new queries in the WitnetOracle contract + pub eth_new_drs_polling_rate_ms: u64, + /// Number of block confirmations needed to assume finality when sending transactions to ethereum + #[serde(default = "one")] + pub eth_txs_confirmations: usize, + /// Max time to wait for an ethereum transaction to be confirmed before returning an error + pub eth_txs_timeout_ms: u64, + /// Address of the WitnetRequestsBoard contract + pub eth_witnet_oracle: H160, + + /// Minimum collateral required on data requests read from the WitnetOracle contract + pub witnet_dr_min_collateral_nanowits: u64, + /// Maximium data request transaction fee assumed by the bridge + pub witnet_dr_max_fee_nanowits: u64, + /// Maximum data request result size (in bytes) will accept to report + pub witnet_dr_max_result_size: usize, + /// Maximum data request value that the bridge will accept to relay + pub witnet_dr_max_value_nanowits: u64, + /// Polling period for checking resolution of data requests in the Witnet blockchain + pub witnet_dr_txs_polling_rate_ms: u64, + /// Max time to wait for data request resolutions, in milliseconds + pub witnet_dr_txs_timeout_ms: u64, + /// Address of the witnet node JSON-RPC server + pub witnet_jsonrpc_socket: SocketAddr, /// Running in the witnet testnet? pub witnet_testnet: bool, - /// Gas limits for some methods. If missing, let the client estimate - #[serde(deserialize_with = "nested_toml_if_using_envy")] - pub gas_limits: Gas, + /// Storage #[serde(deserialize_with = "nested_toml_if_using_envy")] pub storage: Storage, /// Skip first requests up to index n when updating database - pub skip_first: Option, - /// Maximum data request result size (in bytes) - pub max_result_size: usize, - /// Max time to wait for an ethereum transaction to be confirmed before returning an error - pub eth_confirmation_timeout_ms: u64, - /// Number of block confirmations needed to assume finality when sending transactions to ethereum - #[serde(default = "one")] - pub num_confirmations: usize, - /// Miner fee for the witnet data request transactions, in nanowits - pub dr_fee_nanowits: u64, - /// Max ratio between the gas price recommended by the provider and the gas price of the requests in the WRB - /// That is, the bridge will refrain from paying more than these times the gas price originally set forth by the requesters. - #[serde(default = "one_f64")] - pub report_result_max_network_gas_price_ratio: f64, + pub storage_skip_first: Option, } fn one() -> usize { 1 } -fn one_f64() -> f64 { - 1.0 +fn default_max_batch_size() -> u16 { + 256 } /// Gas limits for some methods. If missing, let the client estimate @@ -167,7 +171,7 @@ mod tests { struct SmallConfig { /// Gas limits for some methods. If missing, let the client estimate #[serde(deserialize_with = "nested_toml_if_using_envy")] - pub gas_limits: Gas, + pub eth_gas_limits: Gas, /// Storage #[serde(deserialize_with = "nested_toml_if_using_envy")] pub storage: Storage, @@ -186,7 +190,7 @@ mod tests { ]; let expected = SmallConfig { - gas_limits: Gas { + eth_gas_limits: Gas { post_data_request: Some(10_000), report_result: Some(20_000), }, diff --git a/bridges/centralized-ethereum/src/lib.rs b/bridges/centralized-ethereum/src/lib.rs index 5ae56d675..90fcb5992 100644 --- a/bridges/centralized-ethereum/src/lib.rs +++ b/bridges/centralized-ethereum/src/lib.rs @@ -24,15 +24,15 @@ pub mod config; /// Creates a Witnet Request Board contract from Config information pub fn create_wrb_contract( - eth_client_url: &str, - wrb_contract_addr: H160, + eth_jsonrpc_url: &str, + eth_witnet_oracle: H160, ) -> (Web3, Contract) { - let web3_http = web3::transports::Http::new(eth_client_url) + let web3_http = web3::transports::Http::new(eth_jsonrpc_url) .map_err(|e| format!("Failed to connect to Ethereum client.\nError: {:?}", e)) .unwrap(); let web3 = web3::Web3::new(web3_http); // Why read files at runtime when you can read files at compile time - let wrb_contract_abi_json: &[u8] = include_bytes!("../wrb_abi.json"); + let wrb_contract_abi_json: &[u8] = include_bytes!("../../wrb_abi.json"); let mut wrb_contract_abi = web3::ethabi::Contract::load(wrb_contract_abi_json) .map_err(|e| format!("Unable to load WRB contract from ABI: {:?}", e)) .unwrap(); @@ -41,7 +41,7 @@ pub fn create_wrb_contract( // https://github.com/witnet/witnet-rust/issues/2046 hack_fix_functions_with_multiple_definitions(&mut wrb_contract_abi); - let wrb_contract = Contract::new(web3.eth(), wrb_contract_addr, wrb_contract_abi); + let wrb_contract = Contract::new(web3.eth(), eth_witnet_oracle, wrb_contract_abi); (web3, wrb_contract) } @@ -108,8 +108,8 @@ pub async fn check_witnet_node_running(witnet_addr: &str) -> Result<(), String> } /// Check if the ethereum node is running -pub async fn check_ethereum_node_running(eth_client_url: &str) -> Result<(), String> { - let web3_http = web3::transports::Http::new(eth_client_url) +pub async fn check_ethereum_node_running(eth_jsonrpc_url: &str) -> Result<(), String> { + let web3_http = web3::transports::Http::new(eth_jsonrpc_url) .map_err(|e| format!("Failed to connect to Ethereum client.\nError: {:?}", e)) .unwrap(); let web3 = web3::Web3::new(web3_http); @@ -118,7 +118,7 @@ pub async fn check_ethereum_node_running(eth_client_url: &str) -> Result<(), Str let res = web3.eth().syncing().await; match res { Ok(syncing) => { - log::debug!("Ethereum node is running at {}", eth_client_url); + log::debug!("Ethereum node is running at {}", eth_jsonrpc_url); match syncing { web3::types::SyncState::NotSyncing => {} web3::types::SyncState::Syncing(sync_info) => { @@ -135,7 +135,7 @@ pub async fn check_ethereum_node_running(eth_client_url: &str) -> Result<(), Str { // Ignore this error because it can be caused by a non-standard ethereum provider // https://github.com/witnet/witnet-rust/issues/2141 - log::debug!("Ethereum node is running at {}", eth_client_url); + log::debug!("Ethereum node is running at {}", eth_jsonrpc_url); log::warn!("Ethereum provider returned `true` on eth_syncing method"); Ok(()) @@ -179,7 +179,7 @@ mod tests { fn test_hack_fix_functions_with_multiple_definitions() { // The hack_fix_functions_with_multiple_definitions function already does some checks // internally, so here we call it to ensure the ABI is correct. - let wrb_contract_abi_json: &[u8] = include_bytes!("../wrb_abi.json"); + let wrb_contract_abi_json: &[u8] = include_bytes!("../../wrb_abi.json"); let mut wrb_contract_abi = web3::ethabi::Contract::load(wrb_contract_abi_json) .map_err(|e| format!("Unable to load WRB contract from ABI: {:?}", e)) .unwrap(); diff --git a/bridges/centralized-ethereum/src/main.rs b/bridges/centralized-ethereum/src/main.rs index a44b1c70c..b0f29d6bd 100644 --- a/bridges/centralized-ethereum/src/main.rs +++ b/bridges/centralized-ethereum/src/main.rs @@ -114,48 +114,47 @@ fn run(callback: fn()) -> Result<(), String> { } else { let witnet_client_url = config.witnet_jsonrpc_addr.to_string(); - // Check if Ethereum and Witnet nodes are running before starting actors - check_ethereum_node_running(&config.eth_client_url) - .await - .expect("ethereum node not running"); - check_witnet_node_running(&witnet_client_url) - .await - .expect("witnet node not running"); - - // Web3 contract using HTTP transport with an Ethereum client - let (web3, wrb_contract) = - create_wrb_contract(&config.eth_client_url, config.wrb_contract_addr); - let wrb_contract = Arc::new(wrb_contract); - - // Start DrDatabase actor - let dr_database_addr = DrDatabase::default().start(); - SystemRegistry::set(dr_database_addr); - - // Start Json-RPC actor connected to Witnet node - let node_client = JsonRpcClient::start(&witnet_client_url) - .expect("Json-RPC Client actor failed to started"); - - // Start WitPoller actor - let wit_poller_addr = WitPoller::from_config(&config, node_client.clone()).start(); - SystemRegistry::set(wit_poller_addr); - - // Start DrSender actor - let dr_sender_addr = DrSender::from_config(&config, node_client).start(); - SystemRegistry::set(dr_sender_addr); - - // Start EthPoller actor - let eth_poller_addr = EthPoller::from_config(&config, wrb_contract.clone()).start(); - SystemRegistry::set(eth_poller_addr); - - // Start DrReporter actor - let dr_reporter_addr = DrReporter::from_config(&config, wrb_contract, web3).start(); - SystemRegistry::set(dr_reporter_addr); - - // Initialize Storage Manager - let mut node_config = NodeConfig::default(); - node_config.storage.db_path = config.storage.db_path.clone(); - storage_mngr::start_from_config(node_config); - } + // Check if Ethereum and Witnet nodes are running before starting actors + check_ethereum_node_running(&config.eth_jsonrpc_url) + .await + .expect("ethereum node not running"); + check_witnet_node_running(&config.witnet_jsonrpc_socket.to_string()) + .await + .expect("witnet node not running"); + + // Start DrDatabase actor + let dr_database_addr = DrDatabase::default().start(); + SystemRegistry::set(dr_database_addr); + + // Web3 contract using HTTP transport with an Ethereum client + let (web3, wrb_contract) = + create_wrb_contract(&config.eth_jsonrpc_url, config.eth_witnet_oracle); + let wrb_contract = Arc::new(wrb_contract); + + // Start EthPoller actor + let eth_poller_addr = EthPoller::from_config(&config, web3.clone(), wrb_contract.clone()).start(); + SystemRegistry::set(eth_poller_addr); + + // Start DrReporter actor + let dr_reporter_addr = DrReporter::from_config(&config, web3, wrb_contract).start(); + SystemRegistry::set(dr_reporter_addr); + + // Start Json-RPC actor connected to Witnet node + let node_client = JsonRpcClient::start(&config.witnet_jsonrpc_socket.to_string()) + .expect("Json-RPC Client actor failed to started"); + + // Start WitPoller actor + let wit_poller_addr = WitPoller::from_config(&config, node_client.clone()).start(); + SystemRegistry::set(wit_poller_addr); + + // Start DrSender actor + let dr_sender_addr = DrSender::from_config(&config, node_client).start(); + SystemRegistry::set(dr_sender_addr); + + // Initialize Storage Manager + let mut node_config = NodeConfig::default(); + node_config.storage.db_path = config.storage.db_path.clone(); + storage_mngr::start_from_config(node_config); }); // Run system diff --git a/witnet_centralized_ethereum_bridge.toml b/witnet_centralized_ethereum_bridge.toml index f07e7b504..3d35da96a 100644 --- a/witnet_centralized_ethereum_bridge.toml +++ b/witnet_centralized_ethereum_bridge.toml @@ -1,49 +1,49 @@ # Address of the witnet node JSON-RPC server -witnet_jsonrpc_addr = "127.0.0.1:21338" +witnet_jsonrpc_socket = "127.0.0.1:21338" # Url of the ethereum client -eth_client_url = "http://127.0.0.1:8544" +eth_jsonrpc_url = "http://127.0.0.1:8544" # Address of the WitnetRequestsBoard deployed contract -wrb_contract_addr = "0x6cE42a35C61ccfb42907EEE57eDF14Bb69C7fEF4" +eth_witnet_oracle = "0x6cE42a35C61ccfb42907EEE57eDF14Bb69C7fEF4" # Address of a Request Example deployed contract request_example_contract_addr = "0xEaA9e7Ea612b169f5b41cfF86dA6322f57264a19" # Ethereum account used to create the transactions -eth_account = "0x8d86Bc475bEDCB08179c5e6a4d494EbD3b44Ea8B" +eth_from = "0x8d86Bc475bEDCB08179c5e6a4d494EbD3b44Ea8B" # Period to check for new requests in the WRB -eth_new_dr_polling_rate_ms = 45_000 +eth_new_drs_polling_rate_ms = 45_000 # Period to check for completed requests in Witnet -wit_tally_polling_rate_ms = 45_000 +witnet_tallies_polling_rate_ms = 45_000 # Period to post new requests to Witnet -wit_dr_sender_polling_rate_ms = 45_000 +witnet_polling_rate_ms = 45_000 # If the data request has been sent to witnet but it is not included in a block, retry after this many milliseconds -dr_tx_unresolved_timeout_ms = 600_000 # 10 minutes +witnet_dr_tx_retry_timeout_ms = 600_000 # 10 minutes # Maximum data request result size (in bytes) # TODO: Choose a proper value -max_result_size = 100 +witnet_dr_max_result_size = 100 # Max time to wait for an ethereum transaction to be confirmed before returning an error -eth_confirmation_timeout_ms = 900_000 # 15 minutes +eth_txs_timeout_ms = 900_000 # 15 minutes # Max value that will be accepted by the bridge node in a data request # This is the maximum amount that the relayer is willing to lose per one data request -max_dr_value_nanowits = 100_000_000_000 +witnet_dr_max_value_nanowits = 100_000_000_000 # Running in the witnet testnet? witnet_testnet = false # Number of block confirmations needed to assume finality when sending transactions to ethereum -num_confirmations = 1 +eth_txs_confirmations = 1 # Miner fee for the witnet data request transactions, in nanowits -dr_fee_nanowits = 10_000 +witnet_dr_max_fee_nanowits = 10_000 # Max ratio between the gas price recommended by the provider and the gas price of the requests in the WRB # That is, the bridge will refrain from paying more than these times the gas price originally set forth by the requesters. @@ -51,7 +51,7 @@ report_result_max_network_gas_price_ratio = 1.0 # Gas limits for some methods. # To let the client estimate, comment out the fields -[gas_limits] +[eth_gas_limits] post_data_request = 10000000 report_result = 2000000 diff --git a/witnet_ethereum_bridge.toml b/witnet_ethereum_bridge.toml index 84d296e59..92487143f 100644 --- a/witnet_ethereum_bridge.toml +++ b/witnet_ethereum_bridge.toml @@ -1,13 +1,13 @@ # Address of the witnet node JSON-RPC server -witnet_jsonrpc_addr = "127.0.0.1:21336" +witnet_jsonrpc_socket = "127.0.0.1:21336" # Url of the ethereum client -eth_client_url = "http://127.0.0.1:8888" +eth_jsonrpc_url = "http://127.0.0.1:8888" # Address of the WitnetRequestsBoard deployed contract -wrb_contract_addr = "0x354B08f9fD4b171e774898261908DfeA113b0e14" +eth_witnet_oracle = "0x354B08f9fD4b171e774898261908DfeA113b0e14" # Address of the BlockRelay deployed contract block_relay_contract_addr = "0xEaA9e7Ea612b169f5b41cfF86dA6322f57264a19" # Ethereum account used to create the transactions -eth_account = "0x333b18f64949C59Cd0b84b51C7580f260c563c31" +eth_from = "0x333b18f64949C59Cd0b84b51C7580f260c563c31" # Enable block relay from witnet to ethereum, relay only new blocks # (blocks that were recently consolidated) enable_block_relay_new_blocks = true @@ -38,7 +38,7 @@ claim_dr_rate_ms = 30_000 # Period to check for state updates in existing requests in the WRB eth_existing_dr_polling_rate_ms = 10_000 # Period to check for new requests in the WRB -eth_new_dr_polling_rate_ms = 1_000 +eth_new_drs_polling_rate_ms = 1_000 # Running in the witnet testnet? witnet_testnet = false @@ -47,7 +47,7 @@ read_dr_hash_interval_ms = 10_000 # Gas limits for some methods. # To let the client estimate, comment out the fields -[gas_limits] +[eth_gas_limits] claim_data_requests = 5000000 post_data_request = 10000000 post_new_block = 2000000 From 3c8c36184fc156c7cef6cfd8b9949249d4c5345b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Fri, 1 Mar 2024 16:46:40 +0100 Subject: [PATCH 55/83] chore(c-bridge): deprecate cli's post-dr --- bridges/centralized-ethereum/src/main.rs | 35 ------------------------ 1 file changed, 35 deletions(-) diff --git a/bridges/centralized-ethereum/src/main.rs b/bridges/centralized-ethereum/src/main.rs index b0f29d6bd..132a21c20 100644 --- a/bridges/centralized-ethereum/src/main.rs +++ b/bridges/centralized-ethereum/src/main.rs @@ -4,7 +4,6 @@ use actix::{Actor, System, SystemRegistry}; use std::{path::PathBuf, process::exit, sync::Arc}; use structopt::StructOpt; -use web3::{contract, types::U256}; use witnet_centralized_ethereum_bridge::{ actors::{ dr_database::DrDatabase, dr_reporter::DrReporter, dr_sender::DrSender, @@ -22,9 +21,6 @@ struct App { /// Path of the config file #[structopt(short = "c", long)] config: Option, - /// Post data request and exit - #[structopt(long = "post-dr")] - post_dr: bool, /// Read config from environment #[structopt(long = "env", conflicts_with = "config")] env: bool, @@ -44,30 +40,6 @@ fn init_logger() { .init(); } -async fn post_example_dr(config: Arc) { - log::info!("Posting an example of Data Request"); - let (_web3, wrb_contract) = - create_wrb_contract(&config.eth_client_url, config.wrb_contract_addr); - - log::info!("calling postDataRequest"); - - let res = wrb_contract - .call_with_confirmations( - "postDataRequest", - (config.request_example_contract_addr,), - config.eth_account, - contract::Options::with(|opt| { - opt.value = Some(U256::from_dec_str("2500000000000000").unwrap()); - // The cost of posting a data request is mainly the storage, so - // big data requests may need bigger amounts of gas - opt.gas = config.gas_limits.post_data_request.map(Into::into); - }), - 1, - ) - .await; - log::info!("The receipt is {:?}", res); -} - fn main() { init_logger(); @@ -101,19 +73,12 @@ fn run(callback: fn()) -> Result<(), String> { // Init system let system = System::new(); - let condition = app.post_dr; // Init actors system.block_on(async { // Call cb function (register interrupt handlers) callback(); - if condition { - post_example_dr(config).await; - log::info!("post post_example DR"); - } else { - let witnet_client_url = config.witnet_jsonrpc_addr.to_string(); - // Check if Ethereum and Witnet nodes are running before starting actors check_ethereum_node_running(&config.eth_jsonrpc_url) .await From e3e68522e70939a4962c34fb4ab09505bbf73e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Fri, 1 Mar 2024 16:55:43 +0100 Subject: [PATCH 56/83] chore(c-bridge): upgrade wrb_abi --- bridges/centralized-ethereum/wrb_abi.json | 2057 --------------------- bridges/wrb_abi.json | 288 +++ 2 files changed, 288 insertions(+), 2057 deletions(-) delete mode 100644 bridges/centralized-ethereum/wrb_abi.json create mode 100644 bridges/wrb_abi.json diff --git a/bridges/centralized-ethereum/wrb_abi.json b/bridges/centralized-ethereum/wrb_abi.json deleted file mode 100644 index 02e766e97..000000000 --- a/bridges/centralized-ethereum/wrb_abi.json +++ /dev/null @@ -1,2057 +0,0 @@ -[ - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "queryId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "string", - "name": "reason", - "type": "string" - } - ], - "name": "BatchReportError", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "queryId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "from", - "type": "address" - } - ], - "name": "DeletedQuery", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "queryId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "from", - "type": "address" - } - ], - "name": "PostedRequest", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "queryId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "from", - "type": "address" - } - ], - "name": "PostedResult", - "type": "event" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asBool", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asBytes", - "outputs": [ - { - "internalType": "bytes", - "name": "", - "type": "bytes" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asBytes32", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asErrorCode", - "outputs": [ - { - "internalType": "enum Witnet.ErrorCodes", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asErrorMessage", - "outputs": [ - { - "internalType": "enum Witnet.ErrorCodes", - "name": "", - "type": "uint8" - }, - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asFixed16", - "outputs": [ - { - "internalType": "int32", - "name": "", - "type": "int32" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asFixed16Array", - "outputs": [ - { - "internalType": "int32[]", - "name": "", - "type": "int32[]" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asInt128", - "outputs": [ - { - "internalType": "int128", - "name": "", - "type": "int128" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asInt128Array", - "outputs": [ - { - "internalType": "int128[]", - "name": "", - "type": "int128[]" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asRawError", - "outputs": [ - { - "internalType": "uint64[]", - "name": "", - "type": "uint64[]" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asString", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asStringArray", - "outputs": [ - { - "internalType": "string[]", - "name": "", - "type": "string[]" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asUint64", - "outputs": [ - { - "internalType": "uint64", - "name": "", - "type": "uint64" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "asUint64Array", - "outputs": [ - { - "internalType": "uint64[]", - "name": "", - "type": "uint64[]" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "deleteQuery", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "reporter", - "type": "address" - }, - { - "internalType": "uint256", - "name": "timestamp", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "drTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "cborBytes", - "type": "bytes" - } - ], - "internalType": "struct Witnet.Response", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_gasPrice", - "type": "uint256" - } - ], - "name": "estimateReward", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getNextQueryId", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "getQueryData", - "outputs": [ - { - "components": [ - { - "components": [ - { - "internalType": "contract IWitnetRequest", - "name": "addr", - "type": "address" - }, - { - "internalType": "address", - "name": "requester", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "hash", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "gasprice", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "reward", - "type": "uint256" - } - ], - "internalType": "struct Witnet.Request", - "name": "request", - "type": "tuple" - }, - { - "components": [ - { - "internalType": "address", - "name": "reporter", - "type": "address" - }, - { - "internalType": "uint256", - "name": "timestamp", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "drTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "cborBytes", - "type": "bytes" - } - ], - "internalType": "struct Witnet.Response", - "name": "response", - "type": "tuple" - }, - { - "internalType": "address", - "name": "from", - "type": "address" - } - ], - "internalType": "struct Witnet.Query", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "getQueryStatus", - "outputs": [ - { - "internalType": "enum Witnet.QueryStatus", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "isError", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "_result", - "type": "tuple" - } - ], - "name": "isOk", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "contract IWitnetRequest", - "name": "_addr", - "type": "address" - } - ], - "name": "postRequest", - "outputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readRequest", - "outputs": [ - { - "components": [ - { - "internalType": "contract IWitnetRequest", - "name": "addr", - "type": "address" - }, - { - "internalType": "address", - "name": "requester", - "type": "address" - }, - { - "internalType": "bytes32", - "name": "hash", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "gasprice", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "reward", - "type": "uint256" - } - ], - "internalType": "struct Witnet.Request", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readRequestBytecode", - "outputs": [ - { - "internalType": "bytes", - "name": "", - "type": "bytes" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readRequestGasPrice", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readRequestReward", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readResponse", - "outputs": [ - { - "components": [ - { - "internalType": "address", - "name": "reporter", - "type": "address" - }, - { - "internalType": "uint256", - "name": "timestamp", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "drTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "cborBytes", - "type": "bytes" - } - ], - "internalType": "struct Witnet.Response", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readResponseDrTxHash", - "outputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readResponseReporter", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readResponseResult", - "outputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "readResponseTimestamp", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "_drTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "_result", - "type": "bytes" - } - ], - "name": "reportResult", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_timestamp", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "_drTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "_result", - "type": "bytes" - } - ], - "name": "reportResult", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "internalType": "uint256", - "name": "queryId", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "timestamp", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "drTxHash", - "type": "bytes32" - }, - { - "internalType": "bytes", - "name": "cborBytes", - "type": "bytes" - } - ], - "internalType": "struct IWitnetRequestBoardReporter.BatchResult[]", - "name": "_batchResults", - "type": "tuple[]" - }, - { - "internalType": "bool", - "name": "_verbose", - "type": "bool" - } - ], - "name": "reportResultBatch", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes", - "name": "_cborBytes", - "type": "bytes" - } - ], - "name": "resultFromCborBytes", - "outputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "_cborValue", - "type": "tuple" - } - ], - "name": "resultFromCborValue", - "outputs": [ - { - "components": [ - { - "internalType": "bool", - "name": "success", - "type": "bool" - }, - { - "components": [ - { - "components": [ - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - }, - { - "internalType": "uint32", - "name": "cursor", - "type": "uint32" - } - ], - "internalType": "struct Witnet.Buffer", - "name": "buffer", - "type": "tuple" - }, - { - "internalType": "uint8", - "name": "initialByte", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "majorType", - "type": "uint8" - }, - { - "internalType": "uint8", - "name": "additionalInformation", - "type": "uint8" - }, - { - "internalType": "uint64", - "name": "len", - "type": "uint64" - }, - { - "internalType": "uint64", - "name": "tag", - "type": "uint64" - } - ], - "internalType": "struct Witnet.CBOR", - "name": "value", - "type": "tuple" - } - ], - "internalType": "struct Witnet.Result", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "pure", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_queryId", - "type": "uint256" - } - ], - "name": "upgradeReward", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "stateMutability": "payable", - "type": "receive" - } -] diff --git a/bridges/wrb_abi.json b/bridges/wrb_abi.json new file mode 100644 index 000000000..598af5866 --- /dev/null +++ b/bridges/wrb_abi.json @@ -0,0 +1,288 @@ +[ + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "queryId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "string", + "name": "reason", + "type": "string" + } + ], + "name": "BatchReportError", + "type": "event" + }, + { + "inputs": [], + "name": "channel", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "class", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "witnetQueryIds", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "reportTxMsgData", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "reportTxGasPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "nanoWitPrice", + "type": "uint256" + } + ], + "name": "estimateReportEarnings", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "queryIds", + "type": "uint256[]" + } + ], + "name": "extractWitnetDataRequests", + "outputs": [ + { + "internalType": "bytes[]", + "name": "drBytecodes", + "type": "bytes[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "contract WitnetRequestFactory", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getNextQueryId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "queryIds", + "type": "uint256[]" + } + ], + "name": "getQueryStatusBatch", + "outputs": [ + { + "internalType": "enum WitnetV2.QueryStatus[]", + "name": "", + "type": "uint8[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "registry", + "outputs": [ + { + "internalType": "contract WitnetRequestBytecodes", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "witnetQueryId", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "witnetQueryResultTallyHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "witnetQueryResultCborBytes", + "type": "bytes" + } + ], + "name": "reportResult", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "witnetQueryId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "witnetQueryResultTimestamp", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "witnetQueryResultTallyHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "witnetQueryResultCborBytes", + "type": "bytes" + } + ], + "name": "reportResult", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "queryId", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "queryResultTimestamp", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "queryResultTallyHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "queryResultCborBytes", + "type": "bytes" + } + ], + "internalType": "struct IWitnetOracleReporter.BatchResult[]", + "name": "_batchResults", + "type": "tuple[]" + } + ], + "name": "reportResultBatch", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "specs", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "queryId", + "type": "uint256" + } + ], + "name": "upgradeQueryEvmReward", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] \ No newline at end of file From 87ce5cac5e75098fd26d0c46e56d7380b7ea89f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Fri, 1 Mar 2024 16:57:17 +0100 Subject: [PATCH 57/83] feat(c-bridge): dr_database: adapt to wsb 2.0 --- .../src/actors/dr_database.rs | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/dr_database.rs b/bridges/centralized-ethereum/src/actors/dr_database.rs index 7d0f258c4..77c9f397e 100644 --- a/bridges/centralized-ethereum/src/actors/dr_database.rs +++ b/bridges/centralized-ethereum/src/actors/dr_database.rs @@ -53,17 +53,20 @@ pub struct DrInfoBridge { } /// Data request state -#[derive(Clone, Default, Serialize, Deserialize)] +#[derive(Clone, Copy, Default, Serialize, Deserialize)] pub enum DrState { - /// New: the data request has just been posted to the smart contract. + /// New: a new query was detected on the Witnet Oracle contract, + /// but has not yet been attended. #[default] New, - /// Pending: the data request has been created and broadcast to witnet, but it has not been - /// included in a witnet block yet. + /// Pending: a data request transaction was broadcasted to the Witnet blockchain, + /// but has not yet been resolved. Pending, - /// Finished: data request has been resolved in witnet and the result is in the smart - /// contract. + /// Finished: the data request result was reported back to the Witnet Oracle contract. Finished, + /// Dismissed: the data request result cannot be reported back to the Witnet Oracle contract, + /// or was already reported by another bridge instance. + Dismissed, } impl fmt::Display for DrState { @@ -72,24 +75,24 @@ impl fmt::Display for DrState { DrState::New => "New", DrState::Pending => "Pending", DrState::Finished => "Finished", + DrState::Dismissed => "Dismissed", }; f.write_str(s) } } -/// Data request states in Witnet Request Board contract +/// Possible query states in the Witnet Oracle contract #[derive(Serialize, Deserialize, Clone)] pub enum WitnetQueryStatus { - /// Unknown: the data request does not exist. + /// Unknown: the query does not exist, or got eventually deleted. Unknown, - /// Posted: the data request has just been posted to the smart contract. + /// Posted: the query exists, but has not yet been reported. Posted, - /// Reported: the data request has been resolved in witnet and the result is in the smart - /// contract. + /// Reported: some query result got stored into the WitnetOracle, although not yet finalized. Reported, - /// Deleted: the data request has been resolved in witnet but the result was deleted. - Deleted, + /// Finalized: the query was reported, and considered to be final. + Finalized, } impl WitnetQueryStatus { @@ -98,7 +101,7 @@ impl WitnetQueryStatus { match i { 1 => WitnetQueryStatus::Posted, 2 => WitnetQueryStatus::Reported, - 3 => WitnetQueryStatus::Deleted, + 3 => WitnetQueryStatus::Finalized, _ => WitnetQueryStatus::Unknown, } } @@ -164,13 +167,15 @@ impl Message for GetLastDrId { type Result = Result; } -/// Set data request id as "finished" -pub struct SetFinished { - /// Data Request Id +/// Set state of given data request id +pub struct SetDrState { + /// Data Request id pub dr_id: DrId, + /// Data Request new state + pub dr_state: DrState, } -impl Message for SetFinished { +impl Message for SetDrState { type Result = Result<(), ()>; } @@ -239,31 +244,31 @@ impl Handler for DrDatabase { } } -impl Handler for DrDatabase { +impl Handler for DrDatabase { type Result = Result<(), ()>; - fn handle(&mut self, msg: SetFinished, ctx: &mut Self::Context) -> Self::Result { - let SetFinished { dr_id } = msg; + fn handle(&mut self, msg: SetDrState, ctx: &mut Self::Context) -> Self::Result { + let SetDrState { dr_id, dr_state } = msg; match self.dr.entry(dr_id) { Entry::Occupied(entry) => { entry.into_mut().dr_state = DrState::Finished; log::debug!( "Data request #{} updated to state {}", dr_id, - DrState::Finished + dr_state, ); } Entry::Vacant(entry) => { entry.insert(DrInfoBridge { dr_bytes: vec![], - dr_state: DrState::Finished, + dr_state: dr_state, dr_tx_hash: None, dr_tx_creation_timestamp: None, }); log::debug!( "Data request #{} inserted with state {}", dr_id, - DrState::Finished + dr_state, ); } } From eca22979b4023a602fc1ac6d9434584dafcd4732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Fri, 1 Mar 2024 16:59:55 +0100 Subject: [PATCH 58/83] feat(c-bridge): dr_reporter: new feats - adapt to wsb v2.0 - report dr_tally_tx_hash instead of dr_tx_hash - low evm funds alert - detect and dismiss already reported queries --- .../src/actors/dr_reporter.rs | 716 +++++++++--------- 1 file changed, 363 insertions(+), 353 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/dr_reporter.rs b/bridges/centralized-ethereum/src/actors/dr_reporter.rs index d0e5fc322..31c387f76 100644 --- a/bridges/centralized-ethereum/src/actors/dr_reporter.rs +++ b/bridges/centralized-ethereum/src/actors/dr_reporter.rs @@ -1,43 +1,47 @@ use crate::{ - actors::dr_database::{DrDatabase, DrId, SetFinished, WitnetQueryStatus}, + actors::dr_database::{DrDatabase, DrId, DrState, SetDrState}, config::Config, handle_receipt, }; use actix::prelude::*; use std::{collections::HashSet, sync::Arc, time::Duration}; use web3::{ - contract::{self, Contract}, + contract::{self, Contract, tokens::Tokenize}, ethabi::{ethereum_types::H256, Token}, transports::Http, types::{H160, U256}, Web3, }; +use web3_unit_converter::Unit; use witnet_data_structures::{chain::Hash, radon_error::RadonErrors}; use witnet_node::utils::stop_system_if_panicking; /// DrReporter actor sends the the Witnet Request tally results to Ethereum #[derive(Default)] pub struct DrReporter { - /// WRB contract - pub wrb_contract: Option>>, /// Web3 pub web3: Option>, - /// eth_account - pub eth_account: H160, + /// WRB contract + pub wrb_contract: Option>>, + /// EVM account used to report data request results + pub eth_from: H160, + /// EVM account minimum balance under which alerts will be logged + pub eth_from_balance_threshold: u64, + /// Flag indicating whether low funds alert was already logged + pub eth_from_balance_alert: bool, /// report_result_limit - pub report_result_limit: Option, + pub eth_max_gas: Option, + /// Price of $nanoWit in Wei, used to improve estimation of report profits + pub eth_nanowit_wei_price: Option, + /// Max time to wait for an ethereum transaction to be confirmed before returning an error + pub eth_txs_timeout_ms: u64, + /// Number of block confirmations needed to assume finality when sending transactions to ethereum + pub eth_txs_confirmations: usize, /// maximum result size (in bytes) - pub max_result_size: usize, + pub witnet_dr_max_result_size: usize, /// Pending reportResult transactions. The actor should not attempt to report these requests /// until the timeout has elapsed - pub pending_report_result: HashSet, - /// Max time to wait for an ethereum transaction to be confirmed before returning an error - pub eth_confirmation_timeout_ms: u64, - /// Number of block confirmations needed to assume finality when sending transactions to ethereum - pub num_confirmations: usize, - /// Max ratio between the gas price recommended by the provider and the gas price of the requests in the WRB - /// That is, the bridge will refrain from paying more than these times the gas price originally set forth by the requesters. - pub report_result_max_network_gas_price_ratio: f64, + pub pending_dr_reports: HashSet, } impl Drop for DrReporter { @@ -68,20 +72,23 @@ impl DrReporter { /// Initialize `DrReporter` taking the configuration from a `Config` structure pub fn from_config( config: &Config, - wrb_contract: Arc>, web3: Web3, + wrb_contract: Arc> ) -> Self { Self { - wrb_contract: Some(wrb_contract), web3: Some(web3), - eth_account: config.eth_account, - report_result_limit: config.gas_limits.report_result, - max_result_size: config.max_result_size, - pending_report_result: Default::default(), - eth_confirmation_timeout_ms: config.eth_confirmation_timeout_ms, - num_confirmations: config.num_confirmations, - report_result_max_network_gas_price_ratio: config - .report_result_max_network_gas_price_ratio, + wrb_contract: Some(wrb_contract), + eth_from: config.eth_from, + eth_from_balance_threshold: config.eth_from_balance_threshold, + eth_from_balance_alert: false, + eth_max_gas: config.eth_gas_limits.report_result, + eth_nanowit_wei_price: config.eth_nanowit_wei_price, + eth_txs_timeout_ms: config.eth_txs_timeout_ms, + eth_txs_confirmations: config.eth_txs_confirmations, + witnet_dr_max_result_size: config.witnet_dr_max_result_size, + pending_dr_reports: Default::default(), + + } } } @@ -94,13 +101,15 @@ pub struct DrReporterMsg { /// Report the result of this data request id to ethereum pub struct Report { - /// Data request id in ethereum + /// Data Request's unique query id as known by the WitnetOracle contract pub dr_id: DrId, - /// Timestamp of the solving commit txs in Witnet. If zero is provided, EVM-timestamp will be used instead - pub timestamp: u64, - /// Hash of the data request in witnet + /// Timestamp at which reported result was actually generated + pub dr_timestamp: u64, + /// Hash of the Data Request Transaction in the Witnet blockchain pub dr_tx_hash: Hash, - /// Data request result from witnet, in bytes + /// Hash of the Data Request Tally Transaction in the Witnet blockchain + pub dr_tally_tx_hash: Hash, + /// CBOR-encoded result to Data Request, as resolved by the Witnet blockchain pub result: Vec, } @@ -114,10 +123,10 @@ impl Handler for DrReporter { fn handle(&mut self, mut msg: DrReporterMsg, ctx: &mut Self::Context) -> Self::Result { // Remove all reports that have already been reported, but whose reporting transaction is still pending msg.reports.retain(|report| { - if self.pending_report_result.contains(&report.dr_id) { + if self.pending_dr_reports.contains(&report.dr_id) { // Timeout is not over yet, no action is needed log::debug!( - "Request [{}] is already being resolved, ignoring DrReporterMsg", + "[{}]: currently being reported...", report.dr_id ); @@ -133,15 +142,18 @@ impl Handler for DrReporter { } let dr_ids: Vec<_> = msg.reports.iter().map(|report| report.dr_id).collect(); - let dr_ids2 = dr_ids.clone(); + let incoming_dr_ids = dr_ids.clone(); let wrb_contract = self.wrb_contract.clone().unwrap(); - let eth_account = self.eth_account; - let report_result_limit = self.report_result_limit; - let num_confirmations = self.num_confirmations; - let eth_confirmation_timeout = Duration::from_millis(self.eth_confirmation_timeout_ms); + let eth_from = self.eth_from; + let eth_from_balance_threshold = self.eth_from_balance_threshold; + let mut eth_from_balance_alert = self.eth_from_balance_alert; + let eth_max_gas = self.eth_max_gas; + let eth_txs_confirmations = self.eth_txs_confirmations; + let eth_tx_timeout = Duration::from_millis(self.eth_txs_timeout_ms); + let eth_nanowit_wei_price = U256::from(self.eth_nanowit_wei_price.unwrap_or_default()); for report in &mut msg.reports { - if report.result.len() > self.max_result_size { + if report.result.len() > self.witnet_dr_max_result_size { let radon_error = RadonErrors::BridgeOversizedResult as u8; report.result = vec![0xD8, 0x27, 0x81, 0x18, radon_error] } @@ -149,435 +161,426 @@ impl Handler for DrReporter { // New request or timeout elapsed, save dr_id for report in &msg.reports { - self.pending_report_result.insert(report.dr_id); + self.pending_dr_reports.insert(report.dr_id); } let eth = self.web3.as_ref().unwrap().eth(); - let report_result_max_network_gas_price_ratio = - self.report_result_max_network_gas_price_ratio; - let fut = async move { - // Check if the request has already been resolved by some old pending transaction - // that got confirmed after the eth_confirmation_timeout has elapsed - let mut reports = vec![]; - for report in msg.reports.drain(..) { - if let Some(set_finished_msg) = - read_resolved_request_from_contract(report.dr_id, &wrb_contract).await - { - // The request is already resolved, mark as finished in the database - let dr_database_addr = DrDatabase::from_registry(); - dr_database_addr.send(set_finished_msg).await.ok(); - } else { - // Not resolved yet, insert back into the list - reports.push(report); + + // Trace low funds alerts if required. + let eth_from_balance = match eth.balance(eth_from, None).await { + Ok(x) => { + if x < U256::from(eth_from_balance_threshold) { + eth_from_balance_alert = true; + log::warn!( + "EVM address {} running low of funds: {} ETH", + eth_from, + Unit::Wei(&x.to_string()).to_eth_str().unwrap_or_default() + ); + } else if eth_from_balance_alert { + log::info!("EVM address {} recovered funds.", eth_from); + eth_from_balance_alert = false; + } + + x } - } - msg.reports = reports; + Err(e) => { + log::error!("Error geting balance from address {}: {:?}", eth_from, e); + + return eth_from_balance_alert; + } + }; if msg.reports.is_empty() { // Nothing to report - return; + return eth_from_balance_alert; } - // TODO: max_gas_price is the same for all batches, it could be calculated per-batch - // We don't want to proceed with reporting if there's no way to fetch the report gas - // price from the WRB. - let mut report_gas_price = match get_max_gas_price(&msg, &wrb_contract).await { - Some(x) => x, - None => { - log::error!("Error reading report gas price"); - - return; - } - }; // We don't want to proceed with reporting if there's no way to fetch the gas price // from the provider or gateway. - let network_gas_price = match eth.gas_price().await { + let eth_gas_price = match eth.gas_price().await { Ok(x) => x, Err(e) => { log::error!("Error estimating network gas price: {}", e); - - return; + + return eth_from_balance_alert; } }; - let max_report_gas_price = u256_saturating_mul_f64( - report_gas_price, - report_result_max_network_gas_price_ratio, - ); - if report_gas_price <= max_report_gas_price { - // If not higher than the allowed ratio, set gas price - if network_gas_price > report_gas_price { - log::debug!("Network gas price is higher than requests' gas price. Setting report gas price to {}", network_gas_price); - } - report_gas_price = network_gas_price; - } else { - // Higher network gas price: show warning but try anyway, the reportResult transaction may fail - let ratio = u256_div_as_f64(network_gas_price, report_gas_price); - log::warn!("Network gas price is {}x higher than request's gas price. Capping report gas price to {}", ratio, max_report_gas_price); - report_gas_price = max_report_gas_price; - } - let batch_results: Vec<_> = msg + let batched_report: Vec<_> = msg .reports .iter() .map(|report| { - let dr_hash = H256::from_slice(report.dr_tx_hash.as_ref()); - + let dr_hash = H256::from_slice(report.dr_tally_tx_hash.as_ref()); // the trait `web3::contract::tokens::Tokenize` is not implemented for // `(std::vec::Vec<(web3::types::U256, web3::types::U256, web3::types::H256, std::vec::Vec)>, bool) // Need to manually convert to tuple Token::Tuple(vec![ Token::Uint(report.dr_id), - Token::Uint(report.timestamp.into()), + Token::Uint(report.dr_timestamp.into()), Token::FixedBytes(dr_hash.to_fixed_bytes().to_vec()), Token::Bytes(report.result.clone()), ]) }) .collect(); - let verbose = true; - let batches = split_by_gas_limit( - batch_results, + + let batched_reports = split_by_gas_limit( + batched_report, &wrb_contract, - eth_account, - report_result_limit, - verbose, - report_gas_price, + eth_from, + eth_gas_price, + eth_nanowit_wei_price, + eth_max_gas ) .await; log::debug!( - "Requests [{:?}] will be reported in {} transactions", + "[{:?}] will be reported in {} transactions", dr_ids, - batches.len() + batched_reports.len() ); - for (batch_results, estimated_gas_limit) in batches { - if batch_results.len() > 1 { - log::debug!("Executing reportResultBatch {:?}", batch_results); - } else { - log::debug!("Executing reportResult {:?}", batch_results); - } - let params_str; - let only_1_batch = batch_results.len() == 1; - let receipt = if only_1_batch { - let (dr_id, ts, dr_tx_hash, report_result) = - unwrap_batch(batch_results[0].clone()); - params_str = format!( - "reportResult{:?}", - (&dr_id, &ts, &dr_tx_hash, &report_result) - ); - - let receipt_fut = wrb_contract.call_with_confirmations( - "reportResult", - (dr_id, ts, dr_tx_hash, report_result), - eth_account, - contract::Options::with(|opt| { - opt.gas = Some(estimated_gas_limit); - opt.gas_price = Some(report_gas_price); - }), - num_confirmations, - ); - tokio::time::timeout(eth_confirmation_timeout, receipt_fut).await - } else { - params_str = format!("reportResultBatch{:?}", (&batch_results, verbose)); - - let receipt_fut = wrb_contract.call_with_confirmations( - "reportResultBatch", - (batch_results, verbose), - eth_account, - contract::Options::with(|opt| { - opt.gas = Some(estimated_gas_limit); - opt.gas_price = Some(report_gas_price); - }), - num_confirmations, - ); - tokio::time::timeout(eth_confirmation_timeout, receipt_fut).await - }; + for (batched_report, eth_gas_limit) in batched_reports { + log::debug!("Executing reportResultBatch {:?}", batched_report); + + let receipt_fut = wrb_contract.call_with_confirmations( + "reportResultBatch", + batched_report.clone(), + eth_from, + contract::Options::with(|opt| { + opt.gas = Some(eth_gas_limit); + opt.gas_price = Some(eth_gas_price); + }), + eth_txs_confirmations, + ); + let receipt = tokio::time::timeout(eth_tx_timeout, receipt_fut).await; match receipt { Ok(Ok(receipt)) => { - log::debug!("Request [{:?}], reportResult: {:?}", dr_ids, receipt); + log::debug!("[{:?}]: tx receipt: {:?}", dr_ids, receipt); match handle_receipt(&receipt).await { Ok(()) => { - log::debug!("{}: success", params_str); - // Set successful reports as Finished in the database using - // SetFinished message + let mut dismissed_dr_reports: HashSet = Default::default(); for log in receipt.logs { - if let Some(finished_dr_id) = - parse_posted_result_event(wrb_contract.abi(), log) + if let Some((dismissed_dr_id, reason)) = + parse_batch_report_error_log(wrb_contract.abi(), log) { - // We assume that the PostedResult event implies that the - // data request state in the contract is "Reported" or - // "Deleted" - let dr_database_addr = DrDatabase::from_registry(); + if dismissed_dr_reports.insert(dismissed_dr_id) { + log::warn!("[{}]: dismissed => {}", + dismissed_dr_id, + reason + ); + } + } + } + let dr_database_addr = DrDatabase::from_registry(); + for report in &msg.reports { + if dismissed_dr_reports.contains(&report.dr_id) { + // Dismiss data requests that could not (or need not) get reported dr_database_addr - .send(SetFinished { - dr_id: finished_dr_id, + .send(SetDrState { + dr_id: report.dr_id, + dr_state: DrState::Dismissed, + }) + .await + .ok(); + } else { + // Finalize data requests that got successfully reported + log::info!("[{}]: success => drTallyTxHash: {}", + report.dr_id, + report.dr_tally_tx_hash + ); + dr_database_addr + .send(SetDrState { + dr_id: report.dr_id, + dr_state: DrState::Finished, }) .await .ok(); } - } + }; } Err(()) => { - log::error!("{}: transaction reverted (?)", params_str); + log::error!("[{:?}]: evm tx failed: {}", dr_ids, receipt.transaction_hash); } } } Ok(Err(e)) => { // Error in call_with_confirmations - log::error!("{}: {:?}", params_str, e); + log::error!("{}: {:?}", format!("reportResultBatch{:?}", &batched_report), e); } - Err(_e) => { + Err(elapsed) => { // Timeout is over - log::warn!("{}: timeout is over", params_str); + log::warn!("[{:?}]: evm tx timeout after {}", dr_ids, elapsed); } } } + + if let Ok(x) = eth.balance(eth_from, None).await { + if x < eth_from_balance { + log::warn!("EVM address {} loss: -{} ETH", + eth_from, + Unit::Wei(&(eth_from_balance - x).to_string()).to_eth_str().unwrap_or_default() + ); + } else { + log::debug!("EVM address {} revenue: +{} ETH", + eth_from, + Unit::Wei(&(x - eth_from_balance).to_string()).to_eth_str().unwrap_or_default() + ); + eth_from_balance_alert = false; + } + } + + eth_from_balance_alert }; - ctx.spawn(fut.into_actor(self).map(move |(), act, _ctx| { + ctx.spawn(fut.into_actor(self).map(move |eth_from_balance_alert, act, _ctx: &mut Context| { // Reset timeouts - for dr_id in dr_ids2 { - act.pending_report_result.remove(&dr_id); + for dr_id in incoming_dr_ids { + act.pending_dr_reports.remove(&dr_id); } + act.eth_from_balance_alert = eth_from_balance_alert })); } } -/// Check if the request is already resolved in the WRB contract -async fn read_resolved_request_from_contract( - dr_id: U256, - wrb_contract: &Contract, -) -> Option { - let query_status: Result = wrb_contract - .query( - "getQueryStatus", - (dr_id,), - None, - contract::Options::default(), - None, - ) - .await; - - match query_status { - Ok(status) => match WitnetQueryStatus::from_code(status) { - WitnetQueryStatus::Unknown => log::debug!("[{}] does not exist, skipping", dr_id), - WitnetQueryStatus::Posted => { - log::debug!("[{}] has not got a result yet, skipping", dr_id) - } - WitnetQueryStatus::Reported => { - log::debug!("[{}] already reported", dr_id); - return Some(SetFinished { dr_id }); - } - WitnetQueryStatus::Deleted => { - log::debug!("[{}] already reported and deleted", dr_id); - return Some(SetFinished { dr_id }); - } - }, - Err(err) => { - log::error!( - "Fail to read getQueryStatus from contract: {:?}", - err.to_string(), - ); - } - } - - None -} - -async fn get_max_gas_price(msg: &DrReporterMsg, wrb_contract: &Contract) -> Option { - // The gas price of the report transaction should equal the maximum gas price paid - // by any of the requests being solved here - let mut max_gas_price: Option = None; - for report in &msg.reports { - // Read gas price - let dr_gas_price: Result = wrb_contract - .query( - "readRequestGasPrice", - report.dr_id, - None, - contract::Options::default(), - None, - ) - .await; - match dr_gas_price { - Ok(dr_gas_price) => { - max_gas_price = match max_gas_price { - None => Some(dr_gas_price), - Some(prev) => Some(std::cmp::max(prev, dr_gas_price)), - } - } - Err(e) => { - log::error!("[{}] ReadGasPrice {:?}", report.dr_id, e); - continue; - } +/// Get the queryId of a PostedResult event, or return None if this is a different kind of event +fn parse_batch_report_error_log( + wrb_contract_abi: &web3::ethabi::Contract, + log: web3::types::Log, +) -> Option<(DrId, String)> { + let batch_report_error = wrb_contract_abi.events_by_name("BatchReportError").unwrap(); + // There should be exactly one PostedResult event declartion within the ABI + assert_eq!(batch_report_error.len(), 1); + let batch_report_error = &batch_report_error[0]; + // Parse log, ignoring it if the topic does not match "BatchReportError" + let batch_report_error_log = batch_report_error + .parse_log(web3::ethabi::RawLog { + topics: log.topics, + data: log.data.0, + }) + .ok()?; + let batch_report_error_log_params = batch_report_error_log.params; + let query_id = &batch_report_error_log_params[0]; + assert_eq!(query_id.name, "queryId"); + let reason = &batch_report_error_log_params[1]; + assert_eq!(reason.name, "reason"); + match (&query_id.value, &reason.value) { + (Token::Uint(query_id), Token::String(reason)) => Some((*query_id, reason.to_string())), + _ => { + panic!("Invalid BatchReportError params: {:?}", batch_report_error_log_params); } } - - max_gas_price } -/// Split batch_param (argument of reportResultBatch) into multiple smaller batch_param in order to -/// fit into the gas limit. +/// Split a batched report (argument of reportResultBatch) into multiple smaller +/// batched reports in order to fit into some gas limit. /// -/// Returns a list of `(batch_param, estimated_gas)` that should be used to create -/// "reportResultBatch" transactions. +/// Returns a list of `(batched_report, estimated_gas)` that should be used to +/// create multiple "reportResultBatch" transactions. async fn split_by_gas_limit( - batch_param: Vec, + batched_report: Vec, wrb_contract: &Contract, - eth_account: H160, - report_result_limit: Option, - verbose: bool, - max_gas_price: U256, + eth_from: H160, + eth_gas_price: U256, + eth_nanowit_wei_price: U256, + eth_max_gas: Option, ) -> Vec<(Vec, U256)> { let mut v = vec![]; - let mut stack = vec![batch_param]; + let mut stack = vec![batched_report]; + + while let Some(batch_params) = stack.pop() { - while let Some(batch_param) = stack.pop() { - let params = (batch_param.clone(), verbose); + let eth_report_result_batch_params = batch_params.clone(); + + // -------------------------------------------------------------------------- + // First: try to estimate gas required for reporting this batch of tuples ... + let estimated_gas = wrb_contract .estimate_gas( "reportResultBatch", - params, - eth_account, + eth_report_result_batch_params.clone(), + eth_from, contract::Options::with(|opt| { - opt.gas = report_result_limit.map(Into::into); - opt.gas_price = Some(max_gas_price); + opt.gas = eth_max_gas.map(Into::into); + opt.gas_price = Some(eth_gas_price); }), ) .await; + + if let Err(e) = estimated_gas { + if batch_params.len() <= 1 { + // Skip this single-query batch if still not possible to estimate gas + log::error!("Cannot estimate gas limit: {:?}", e); + log::warn!("Skipping report: {:?}", batch_params); + } else { + // Split batch in half if gas estimation is not possible + let (batch_tuples_1, batch_tuples_2) = batch_params.split_at(batch_params.len() / 2); + stack.push(batch_tuples_1.to_vec()); + stack.push(batch_tuples_2.to_vec()); + } + + continue; + } + + let estimated_gas = estimated_gas.unwrap(); log::debug!( - "reportResultBatch {} estimated gas: {:?}", - batch_param.len(), + "reportResultBatch (x{} drs) estimated gas: {:?}", + batch_params.len(), estimated_gas ); - match estimated_gas { - Ok(estimated_gas) => { - v.push((batch_param, estimated_gas)); + // ------------------------------------------------ + // Second: try to estimate actual profit, if any... + + let query_ids: Vec = batch_params.iter().map(|report_params| { + if let Token::Tuple(report_params) = report_params { + assert_eq!(report_params.len(), 4); + + report_params[0].clone() + } else { + panic!("Cannot extract query id from batch tuple"); + } + }).collect(); + + // the size of the report result tx data may affect the actual profit + // on some layer-2 EVM chains: + let eth_report_result_batch_msg_data = wrb_contract + .abi() + .function("reportResultBatch") + .and_then(|f| { + f.encode_input(ð_report_result_batch_params.into_tokens()) + }); + + let estimated_profit: Result = wrb_contract + .query( + "estimateReportEarnings", + Token::Tuple(vec![ + Token::Tuple(query_ids), + Token::Bytes(eth_report_result_batch_msg_data.unwrap_or_default()), + Token::Uint(eth_gas_price), + Token::Uint(eth_nanowit_wei_price), + ]), + eth_from, + contract::Options::with(|opt| { + opt.gas = eth_max_gas.map(Into::into); + opt.gas_price = Some(eth_gas_price); + }), + None + ) + .await; + + match estimated_profit { + Ok(estimated_profit) if estimated_profit > U256::from(0) => { + log::debug!( + "reportResultBatch (x{} drs) estimated profit: {:?} ETH", + batch_params.len(), + Unit::Wei(&estimated_profit.to_string()).to_eth_str().unwrap_or_default(), + ); + v.push((batch_params, estimated_gas)); + continue; + } + Ok(_) => { + if batch_params.len() <= 1 { + log::warn!("Skipping unprofitable report: {:?}", batch_params); + } } Err(e) => { - if batch_param.len() <= 1 { - log::error!("reportResultBatch estimate gas: {:?}", e); - log::warn!("skipped dr: {:?}", batch_param); - } else { - // Split batch_param in half - let (batch_param1, batch_param2) = batch_param.split_at(batch_param.len() / 2); - stack.push(batch_param1.to_vec()); - stack.push(batch_param2.to_vec()); + if batch_params.len() <= 1 { + log::error!("Cannot estimate report profit: {:?}", e); } } + }; + + if batch_params.len() > 1 { + // Split batch in half if no profit, or no profit estimation was possible + let (sub_batch_1, sub_batch_2) = batch_params.split_at(batch_params.len() / 2); + stack.push(sub_batch_1.to_vec()); + stack.push(sub_batch_2.to_vec()); } } v } -fn unwrap_batch(t: Token) -> (Token, Token, Token, Token) { - if let Token::Tuple(token_vec) = t { - assert_eq!(token_vec.len(), 4); - ( - token_vec[0].clone(), - token_vec[1].clone(), - token_vec[2].clone(), - token_vec[3].clone(), - ) - } else { - panic!("Token:Tuple not found in unwrap_batch function"); - } -} - -/// Get the queryId of a PostedResult event, or return None if this is a different kind of event -fn parse_posted_result_event( - wrb_contract_abi: &web3::ethabi::Contract, - log: web3::types::Log, -) -> Option { - let posted_result_event = wrb_contract_abi.events_by_name("PostedResult").unwrap(); - // There should be exactly one PostedResult event - assert_eq!(posted_result_event.len(), 1); - let posted_result_event = &posted_result_event[0]; - // Parse log, ignoring it if the topic does not match "PostedResult" - let posted_result_log = posted_result_event - .parse_log(web3::ethabi::RawLog { - topics: log.topics, - data: log.data.0, - }) - .ok()?; - let posted_result_log_params = posted_result_log.params; - let query_id = &posted_result_log_params[0]; - assert_eq!(query_id.name, "queryId"); +#[cfg(test)] +mod tests { + use super::*; + use crate::hack_fix_functions_with_multiple_definitions; + use web3::contract::tokens::Tokenize; - match &query_id.value { - Token::Uint(value) => Some(*value), - x => panic!("Invalid queryId type: {:?} (expected Uint)", x), + /// Returns `a / b`, as f64 + fn u256_div_as_f64(a: U256, b: U256) -> f64 { + u256_to_f64(a) / u256_to_f64(b) } -} - -/// Returns `a / b`, as f64 -fn u256_div_as_f64(a: U256, b: U256) -> f64 { - u256_to_f64(a) / u256_to_f64(b) -} - -/// Converts `U256` into `f64` in a lossy way -fn u256_to_f64(a: U256) -> f64 { - a.to_string().parse().unwrap() -} -/// Returns `a * b` as U256, saturating on overflow -fn u256_saturating_mul_f64(a: U256, b: f64) -> U256 { - assert!( - b >= 0.0, - "u256_mul_f64 only supports positive floating point values, got {}", - b - ); - - // Prevent doing any further calculations if we're multiplying zero by something else. - if a == U256::zero() || b == 0.0 { - return U256::zero(); + /// Converts `U256` into `f64` in a lossy way + fn u256_to_f64(a: U256) -> f64 { + a.to_string().parse().unwrap() } - // Binary search a value x such that x / a == b - let mut lo = U256::from(0); - let mut hi = U256::MAX; - // mid = (lo + hi) / 2, but avoid overflows - let mut mid = lo / 2 + hi / 2; - - loop { - let ratio = u256_div_as_f64(mid, a); + /// Returns `a * b` as U256, saturating on overflow + fn u256_saturating_mul_f64(a: U256, b: f64) -> U256 { + assert!( + b >= 0.0, + "u256_mul_f64 only supports positive floating point values, got {}", + b + ); - if ratio == b { - break mid; - } - if ratio > b { - hi = mid; - } - if ratio < b { - lo = mid; + // Prevent doing any further calculations if we're multiplying zero by something else. + if a == U256::zero() || b == 0.0 { + return U256::zero(); } - let new_mid = lo / 2 + hi / 2; - if new_mid == mid { + // Binary search a value x such that x / a == b + let mut lo = U256::from(0); + let mut hi = U256::MAX; + // mid = (lo + hi) / 2, but avoid overflows + let mut mid = lo / 2 + hi / 2; + + loop { + let ratio = u256_div_as_f64(mid, a); + + if ratio == b { + break mid; + } if ratio > b { - break lo; + hi = mid; } if ratio < b { - break hi; + lo = mid; } + + let new_mid = lo / 2 + hi / 2; + if new_mid == mid { + if ratio > b { + break lo; + } + if ratio < b { + break hi; + } + } + mid = new_mid; } - mid = new_mid; } -} -#[cfg(test)] -mod tests { - use super::*; - use crate::hack_fix_functions_with_multiple_definitions; - use web3::contract::tokens::Tokenize; + fn unwrap_batch(t: Token) -> (Token, Token, Token, Token) { + if let Token::Tuple(token_vec) = t { + assert_eq!(token_vec.len(), 4); + ( + token_vec[0].clone(), + token_vec[1].clone(), + token_vec[2].clone(), + token_vec[3].clone(), + ) + } else { + panic!("Token:Tuple not found in unwrap_batch function"); + } + } #[test] fn report_result_type_check() { - let wrb_contract_abi_json: &[u8] = include_bytes!("../../wrb_abi.json"); + let wrb_contract_abi_json: &[u8] = include_bytes!("../../../wrb_abi.json"); let mut wrb_contract_abi = web3::ethabi::Contract::load(wrb_contract_abi_json) .map_err(|e| format!("Unable to load WRB contract from ABI: {:?}", e)) .unwrap(); @@ -586,11 +589,15 @@ mod tests { let msg = DrReporterMsg { reports: vec![Report { dr_id: DrId::from(4358u32), - timestamp: 0, + dr_timestamp: 0, dr_tx_hash: Hash::SHA256([ 106, 107, 78, 5, 218, 5, 159, 172, 215, 12, 141, 98, 19, 163, 167, 65, 62, 79, 3, 170, 169, 162, 186, 24, 59, 135, 45, 146, 133, 85, 250, 155, ]), + dr_tally_tx_hash: Hash::SHA256([ + 106, 107, 78, 5, 218, 5, 159, 172, 215, 12, 141, 98, 19, 163, 167, 65, 62, 79, + 3, 170, 169, 162, 186, 24, 59, 135, 45, 146, 133, 85, 250, 155, + ]), result: vec![26, 160, 41, 182, 230], }], }; @@ -606,7 +613,7 @@ mod tests { // Need to manually call `.into_tokens()`: Token::Tuple(vec![ Token::Uint(report.dr_id), - Token::Uint(report.timestamp.into()), + Token::Uint(report.dr_timestamp.into()), Token::FixedBytes(dr_hash.to_fixed_bytes().to_vec()), Token::Bytes(report.result.clone()), ]) @@ -629,7 +636,7 @@ mod tests { #[test] fn parse_logs_report_result_batch() { - let wrb_contract_abi_json: &[u8] = include_bytes!("../../wrb_abi.json"); + let wrb_contract_abi_json: &[u8] = include_bytes!("../../../wrb_abi.json"); let mut wrb_contract_abi = web3::ethabi::Contract::load(wrb_contract_abi_json) .map_err(|e| format!("Unable to load WRB contract from ABI: {:?}", e)) .unwrap(); @@ -660,8 +667,11 @@ mod tests { }; assert_eq!( - parse_posted_result_event(&wrb_contract_abi, log_posted_result), - Some(U256::from(63605)), + parse_batch_report_error_log(&wrb_contract_abi, log_posted_result), + Some(( + U256::from(63605), + String::from("WitnetOracle: query not in Posted status"), + )) ); } From e627e3cbd21bc1e790516cacc56a6319f47ed9a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Fri, 1 Mar 2024 17:03:20 +0100 Subject: [PATCH 59/83] feat(c-bridge): dr_sender: new feats - adapt to wsb v2.0 - reject queries with collateral too low (<20 wit) - reject queries with total wit value too high - set dr tx fee to the minimum of query sla and config - polling rate set to half of eth_pollers' - log trace reporting witnet node's pkh --- .../src/actors/dr_sender.rs | 133 ++++++++++++------ 1 file changed, 88 insertions(+), 45 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/dr_sender.rs b/bridges/centralized-ethereum/src/actors/dr_sender.rs index e0e390dd2..3e69e8923 100644 --- a/bridges/centralized-ethereum/src/actors/dr_sender.rs +++ b/bridges/centralized-ethereum/src/actors/dr_sender.rs @@ -28,9 +28,11 @@ mod tests; #[derive(Default)] pub struct DrSender { witnet_client: Option>, - wit_dr_sender_polling_rate_ms: u64, - max_dr_value_nanowits: u64, - dr_fee_nanowits: u64, + witnet_dr_min_collateral_nanowits: u64, + witnet_dr_max_value_nanowits: u64, + witnet_dr_max_fee_nanowits: u64, + witnet_node_pkh: Option, + polling_rate_ms: u64, } impl Drop for DrSender { @@ -51,7 +53,7 @@ impl Actor for DrSender { self.check_new_drs( ctx, - Duration::from_millis(self.wit_dr_sender_polling_rate_ms), + Duration::from_millis(self.polling_rate_ms), ); } } @@ -66,42 +68,79 @@ impl DrSender { /// Initialize the `DrSender` taking the configuration from a `Config` structure /// and a Json-RPC client connected to a Witnet node pub fn from_config(config: &Config, node_client: Addr) -> Self { - let max_dr_value_nanowits = config.max_dr_value_nanowits; - let wit_dr_sender_polling_rate_ms = config.wit_dr_sender_polling_rate_ms; - let dr_fee_nanowits = config.dr_fee_nanowits; - Self { + polling_rate_ms: config.eth_new_drs_polling_rate_ms / 2 + 1000, witnet_client: Some(node_client), - wit_dr_sender_polling_rate_ms, - max_dr_value_nanowits, - dr_fee_nanowits, + witnet_dr_min_collateral_nanowits: config.witnet_dr_min_collateral_nanowits, + witnet_dr_max_value_nanowits: config.witnet_dr_max_value_nanowits, + witnet_dr_max_fee_nanowits: config.witnet_dr_max_fee_nanowits, + witnet_node_pkh: None, } } fn check_new_drs(&self, ctx: &mut Context, period: Duration) { let witnet_client = self.witnet_client.clone().unwrap(); - let max_dr_value_nanowits = self.max_dr_value_nanowits; - let dr_fee_nanowits = self.dr_fee_nanowits; + let witnet_dr_min_collateral_nanowits = self.witnet_dr_min_collateral_nanowits; + let witnet_dr_max_value_nanowits = self.witnet_dr_max_value_nanowits; + let witnet_dr_max_fee_nanowits = self.witnet_dr_max_fee_nanowits; + let mut witnet_node_pkh = self.witnet_node_pkh.clone(); let fut = async move { let dr_database_addr = DrDatabase::from_registry(); let dr_reporter_addr = DrReporter::from_registry(); + if witnet_node_pkh.is_none() { + // get witnet node's pkh if not yet known + let req = jsonrpc::Request::method("getPkh") + .timeout(Duration::from_millis(5000)); + let res = witnet_client.send(req).await; + witnet_node_pkh = match res { + Ok(Ok(res)) => match serde_json::from_value::(res) { + Ok(pkh) => Some(pkh), + Err(_) => None + } + Ok(Err(_)) => { + log::warn!("Cannot deserialize witnet node's pkh, will retry later"); + + None + } + Err(_) => { + log::warn!("Cannot get witnet node's pkh, will retry later"); + + None + } + }; + } else { + // TODO: alert if number of big enough utxos is less number of drs to broadcast + // let req = jsonrpc::Request::method("getUtxoInfo") + // .timeout(Duration::from_millis(5_000)) + // .params(witnet_node_pkh.unwrap()) + // .expect("getUtxoInfo params failed serialization"); + } + + // process latest drs added or set as New in the database let new_drs = dr_database_addr.send(GetAllNewDrs).await.unwrap().unwrap(); let mut dr_reporter_msgs = vec![]; for (dr_id, dr_bytes) in new_drs { - match deserialize_and_validate_dr_bytes(&dr_bytes, max_dr_value_nanowits) { + match deserialize_and_validate_dr_bytes( + &dr_bytes, + witnet_dr_min_collateral_nanowits, + witnet_dr_max_value_nanowits, + ) { Ok(dr_output) => { let req = jsonrpc::Request::method("sendRequest") .timeout(Duration::from_millis(5_000)) - .params(json!({"dro": dr_output, "fee": dr_fee_nanowits})) - .expect("params failed serialization"); + .params(json!({ + "dro": dr_output, + "fee": std::cmp::min(dr_output.witness_reward, witnet_dr_max_fee_nanowits) + })) + .expect("DataRequestOutput params failed serialization"); let res = witnet_client.send(req).await; let res = match res { Ok(res) => res, Err(_) => { - log::error!("Failed to connect to witnet client, will retry later"); + log::error!("Failed to connect to witnet node, will retry later"); break; } }; @@ -126,7 +165,7 @@ impl DrSender { } Err(e) => { // Unexpected error deserializing hash - panic!("[{}] error deserializing dr_tx: {}", dr_id, e); + panic!("[{}]: cannot deserialize dr_tx: {}", dr_id, e); } } } @@ -134,7 +173,7 @@ impl DrSender { // Error sending transaction: node not synced, not enough balance, etc. // Do nothing, will retry later. log::error!( - "[{}] error creating data request transaction: {}", + "[{}]: cannot broadcast dr_tx: {}", dr_id, e ); @@ -145,20 +184,23 @@ impl DrSender { Err(err) => { // Error deserializing or validating data request: mark data request as // error and report error as result to ethereum. - log::error!("[{}] error: {}", dr_id, err); + log::error!("[{}]: unacceptable data request bytecode: {}", + dr_id, + err + ); let result = err.encode_cbor(); - // In this case there is no data request transaction, so the dr_tx_hash - // field can be set to anything. - // Except all zeros, because that hash is invalid. - let dr_tx_hash = - "0000000000000000000000000000000000000000000000000000000000000001" + // In this case there is no data request transaction, so + // we set both the dr_tx_hash and dr_tally_tx_hash to zero values. + let zero_hash = + "0000000000000000000000000000000000000000000000000000000000000000" .parse() .unwrap(); dr_reporter_msgs.push(Report { dr_id, - timestamp: 0, - dr_tx_hash, + dr_timestamp: u64::from_ne_bytes(get_timestamp().to_ne_bytes()), + dr_tx_hash: zero_hash, + dr_tally_tx_hash: zero_hash, result, }); } @@ -171,12 +213,15 @@ impl DrSender { }) .await .unwrap(); + + return witnet_node_pkh; }; - ctx.spawn(fut.into_actor(self).then(move |(), _act, ctx| { + ctx.spawn(fut.into_actor(self).then(move |node_pkh, _act, ctx| { // Wait until the function finished to schedule next call. // This avoids tasks running in parallel. ctx.run_later(period, move |act, ctx| { + act.witnet_node_pkh = node_pkh; // Reschedule check_new_drs act.check_new_drs(ctx, period); }); @@ -190,23 +235,23 @@ impl DrSender { /// an error #[derive(Debug)] enum DrSenderError { - /// The data request bytes are not a valid DataRequestOutput + /// Cannot deserialize data request bytecode as read from the WitnetOracle contract Deserialization { msg: String }, - /// The DataRequestOutput is invalid (wrong number of witnesses, wrong min_consensus_percentage) + /// Invalid data request SLA parameters Validation { msg: String }, - /// The RADRequest is invalid (malformed radon script) + /// Malformed Radon script RadonValidation { msg: String }, - /// The specified collateral amount is invalid + /// Invalid collateral amount InvalidCollateral { msg: String }, /// E.g. the WIP-0022 reward to collateral ratio is not satisfied InvalidReward { msg: String }, - /// Overflow when calculating the data request value + /// Invalid data request total value InvalidValue { msg: String }, /// The cost of the data request is greater than the maximum allowed by the configuration of /// this bridge node ValueGreaterThanAllowed { dr_value_nanowits: u64, - max_dr_value_nanowits: u64, + dr_max_value_nanowits: u64, }, } @@ -233,12 +278,12 @@ impl fmt::Display for DrSenderError { } DrSenderError::ValueGreaterThanAllowed { dr_value_nanowits, - max_dr_value_nanowits, + dr_max_value_nanowits, } => { write!( f, "data request value ({}) higher than maximum allowed ({})", - dr_value_nanowits, max_dr_value_nanowits + dr_value_nanowits, dr_max_value_nanowits ) } } @@ -269,19 +314,17 @@ impl DrSenderError { fn deserialize_and_validate_dr_bytes( dr_bytes: &[u8], - max_dr_value_nanowits: u64, + dr_min_collateral_nanowits: u64, + dr_max_value_nanowits: u64, ) -> Result { match DataRequestOutput::from_pb_bytes(dr_bytes) { Err(e) => Err(DrSenderError::Deserialization { msg: e.to_string() }), Ok(dr_output) => { - // TODO: read from consensus constants - let collateral_minimum = 1_000_000_000; - // TODO: read from consensus_constants let required_reward_collateral_ratio = PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; validate_data_request_output( &dr_output, - collateral_minimum, + dr_output.collateral, // we don't want to ever alter the dro_hash required_reward_collateral_ratio, ¤t_active_wips(), ) @@ -294,11 +337,11 @@ fn deserialize_and_validate_dr_bytes( // Collateral value validation // If collateral is equal to 0 means that is equal to collateral_minimum value - if (dr_output.collateral != 0) && (dr_output.collateral < collateral_minimum) { + if (dr_output.collateral != 0) && (dr_output.collateral < dr_min_collateral_nanowits) { return Err(DrSenderError::InvalidCollateral { msg: format!( "Collateral ({}) must be greater than the minimum ({})", - dr_output.collateral, collateral_minimum + dr_output.collateral, dr_min_collateral_nanowits ), }); } @@ -311,10 +354,10 @@ fn deserialize_and_validate_dr_bytes( let dr_value_nanowits = dr_output .checked_total_value() .map_err(|e| DrSenderError::InvalidValue { msg: e.to_string() })?; - if dr_value_nanowits > max_dr_value_nanowits { + if dr_value_nanowits > dr_max_value_nanowits { return Err(DrSenderError::ValueGreaterThanAllowed { dr_value_nanowits, - max_dr_value_nanowits, + dr_max_value_nanowits, }); } From bad4a42af4ea0233db8ffb97bfcf829488d86ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Fri, 1 Mar 2024 17:06:56 +0100 Subject: [PATCH 60/83] feat(c-bridge): eth_poller: new feats - adapt to wsb v2.0 - limit max batch sizes by config --- .../src/actors/eth_poller.rs | 197 ++++++++---------- 1 file changed, 92 insertions(+), 105 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/eth_poller.rs b/bridges/centralized-ethereum/src/actors/eth_poller.rs index 7619e1d54..e2dc3340b 100644 --- a/bridges/centralized-ethereum/src/actors/eth_poller.rs +++ b/bridges/centralized-ethereum/src/actors/eth_poller.rs @@ -1,6 +1,6 @@ use crate::{ actors::dr_database::{ - DrDatabase, DrInfoBridge, GetLastDrId, SetDrInfoBridge, WitnetQueryStatus, + DrDatabase, DrInfoBridge, DrState, GetLastDrId, SetDrInfoBridge, SetDrState, WitnetQueryStatus }, config::Config, }; @@ -8,9 +8,10 @@ use actix::prelude::*; use std::{convert::TryFrom, sync::Arc, time::Duration}; use web3::{ contract::{self, Contract}, - ethabi::Bytes, + ethabi::{Bytes, Token}, transports::Http, types::U256, + Web3, }; use witnet_node::utils::stop_system_if_panicking; @@ -18,12 +19,16 @@ use witnet_node::utils::stop_system_if_panicking; /// in the DrDatabase #[derive(Default)] pub struct EthPoller { + /// Web3 object + pub web3: Option>, /// WRB contract pub wrb_contract: Option>>, /// Period to check for new requests in the WRB - pub eth_new_dr_polling_rate_ms: u64, + pub polling_rate_ms: u64, /// Skip first requests up to index n when updating database pub skip_first: u64, + /// Max number of queries to be batched together + pub max_batch_size: u16, } impl Drop for EthPoller { @@ -44,7 +49,7 @@ impl Actor for EthPoller { self.check_new_requests_from_ethereum( ctx, - Duration::from_millis(self.eth_new_dr_polling_rate_ms), + Duration::from_millis(self.polling_rate_ms), ); } } @@ -57,22 +62,28 @@ impl SystemService for EthPoller {} impl EthPoller { /// Initialize `PeersManager` taking the configuration from a `Config` structure - pub fn from_config(config: &Config, wrb_contract: Arc>) -> Self { + pub fn from_config(config: &Config, web3: Web3, wrb_contract: Arc>) -> Self { Self { + web3: Some(web3), wrb_contract: Some(wrb_contract), - eth_new_dr_polling_rate_ms: config.eth_new_dr_polling_rate_ms, - skip_first: config.skip_first.unwrap_or(0), + polling_rate_ms: config.eth_new_drs_polling_rate_ms, + skip_first: config.storage_skip_first.unwrap_or(0), + max_batch_size: config.eth_max_batch_size, } } fn check_new_requests_from_ethereum(&self, ctx: &mut Context, period: Duration) { - log::debug!("Checking new DRs from Ethereum contract..."); + log::debug!("Checking posted queries on the WitnetOracle contract..."); let wrb_contract = self.wrb_contract.clone().unwrap(); let skip_first = U256::from(self.skip_first); + let max_batch_size = self.max_batch_size; + // Check requests let fut = async move { - let total_requests_count: Result = wrb_contract + let dr_database_addr = DrDatabase::from_registry(); + + let next_dr_id: Result = wrb_contract .query( "getNextQueryId", (), @@ -90,73 +101,94 @@ impl EthPoller { err }); - let dr_database_addr = DrDatabase::from_registry(); - let db_request_count = dr_database_addr.send(GetLastDrId).await; + let last_dr_id = dr_database_addr.send(GetLastDrId).await; - if let (Ok(total_requests_count), Ok(Ok(mut db_request_count))) = - (total_requests_count, db_request_count) + if let (Ok(mut next_dr_id), Ok(Ok(mut last_dr_id))) = + (next_dr_id, last_dr_id) { - if db_request_count < skip_first { + if last_dr_id < skip_first { log::debug!( - "Skipping first {} requests per skip_first config param", + "Skipping first {} queries as per SKIP_FIRST config param", skip_first ); - db_request_count = skip_first; + last_dr_id = skip_first; } - if db_request_count < total_requests_count { - let init_index = usize::try_from(db_request_count + 1).unwrap(); - let last_index = usize::try_from(total_requests_count).unwrap(); - - for i in init_index..last_index { - log::debug!("[{}] checking dr in wrb", i); - - let query_status: Result = wrb_contract - .query( - "getQueryStatus", - (U256::from(i),), - None, - contract::Options::default(), - None, - ) - .await; - - match query_status { - Ok(status) => match WitnetQueryStatus::from_code(status) { + if last_dr_id < next_dr_id { + if last_dr_id + max_batch_size > next_dr_id { + next_dr_id = last_dr_id + max_batch_size; + } + let init_index = usize::try_from(last_dr_id + 1).unwrap(); + let last_index = usize::try_from(next_dr_id).unwrap(); + let ids = init_index .. last_index; + let ids: Vec = ids.map(|id| { Token::Uint(id.into()) }).collect(); + + let queries_status: Result, web3::contract::Error> = wrb_contract + .query( + "getQueryStatusBatch", + Token::Tuple(ids.clone()), + None, + contract::Options::default(), + None + ) + .await; + + if let Ok(queries_status) = queries_status { + let mut posted_ids: Vec = vec![]; + for (pos, status) in queries_status.iter().enumerate() { + let query_id = ids[pos].to_owned().into_uint().unwrap(); + match WitnetQueryStatus::from_code(*status) { WitnetQueryStatus::Unknown => { - log::debug!("[{}] has not exist, skipping", i) + log::debug!("[{}]: not available.", query_id); } WitnetQueryStatus::Posted => { - log::info!("[{}] new dr in wrb", i); - if let Ok(set_dr_info_bridge) = - process_posted_request(i.into(), &wrb_contract).await - { - dr_database_addr.do_send(set_dr_info_bridge); - } else { - break; - } + log::info!("[{}]: not yet reported.", query_id); + posted_ids.push(Token::Uint(query_id)); } - WitnetQueryStatus::Reported => { - log::debug!("[{}] already reported", i); - if let Ok(set_dr_info_bridge) = - process_posted_request(i.into(), &wrb_contract).await - { - dr_database_addr.do_send(set_dr_info_bridge); - } else { - break; - } + WitnetQueryStatus::Reported + | WitnetQueryStatus::Finalized + => { + log::debug!("[{}]: already reported.", query_id); + dr_database_addr.do_send(SetDrState { + dr_id: query_id, + dr_state: DrState::Finished, + }); } - WitnetQueryStatus::Deleted => { - log::debug!("[{}] has been deleted, skipping", i) + } + } + if !posted_ids.is_empty() { + let dr_bytecodes: Result, web3::contract::Error> = wrb_contract + .query( + "extractWitnetDataRequests", + Token::Tuple(posted_ids.clone()), + None, + contract::Options::default(), + None + ) + .await; + if let Ok(dr_bytecodes) = dr_bytecodes { + for (pos, dr_id) in posted_ids.iter().enumerate() { + dr_database_addr.do_send(SetDrInfoBridge( + dr_id.to_owned().into_uint().unwrap(), + DrInfoBridge { + dr_bytes: dr_bytecodes[pos].to_owned(), + ..Default::default() + } + )); } - }, - Err(err) => { + } else { log::error!( - "Fail to read getQueryStatus from contract: {:?}", - err.to_string(), + "Fail to extract Witnet Data Request bytecodes from queries {:?}", + posted_ids ); - break; } } + } else { + log::error!( + "Fail to get status of queries #{} to #{}: {}", + init_index, + last_index, + queries_status.unwrap_err().to_string() + ); } } } @@ -174,48 +206,3 @@ impl EthPoller { })); } } - -/// Auxiliary function that process the information of a new posted request -async fn process_posted_request( - query_id: U256, - wrb_contract: &Contract, -) -> Result { - let dr_bytes: Result = wrb_contract - .query( - "readRequestBytecode", - (query_id,), - None, - contract::Options::default(), - None, - ) - .await; - - // Re-route some errors as success (explanation below) - match dr_bytes { - Ok(dr_bytes) => Ok(dr_bytes), - Err(err) => { - log::error!("Fail to read dr bytes from contract: {}", err.to_string()); - - // In some versions of the bridge contracts (those based on - // `WitnetRequestBoardTrustableBase`), we may get a revert when trying to fetch the dr - // bytes for a deleted query. - // If that's the case, we can return a success here, with empty bytes, so that the - // request can locally marked as complete, and we can move on. - if err.to_string().contains("WitnetRequestBoardTrustableBase") { - log::error!("Wait! This is an instance of `WitnetRequestBoardTrustableBase`. Let's assume we got a revert because the dr bytes were deleted, and simply move on."); - - Ok(Default::default()) - // Otherwise, handle the error normally - } else { - Err(err) - } - } - // Wrap the dr bytes in a `SetDrInfoBridge` structure - }.map(|dr_bytes| SetDrInfoBridge( - query_id, - DrInfoBridge { - dr_bytes, - ..Default::default() - }, - )) -} From 4386d7f4191386ab361c87df0c99250ca770f493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Fri, 1 Mar 2024 17:10:05 +0100 Subject: [PATCH 61/83] feat(c-bridge): wit_poller: new feats - extract dr_tally_tx_hash from wit_dataRequestReport - calculate dr_timestamp as that of the block where the dr commit txs were included --- .../src/actors/wit_poller.rs | 176 +++++++++--------- 1 file changed, 85 insertions(+), 91 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/wit_poller.rs b/bridges/centralized-ethereum/src/actors/wit_poller.rs index e9851dee3..a971361b2 100644 --- a/bridges/centralized-ethereum/src/actors/wit_poller.rs +++ b/bridges/centralized-ethereum/src/actors/wit_poller.rs @@ -9,7 +9,7 @@ use actix::prelude::*; use serde_json::json; use std::{convert::TryFrom, time::Duration}; use witnet_data_structures::chain::{ - Block, ConsensusConstants, DataRequestInfo, Epoch, EpochConstants, Hash, + Block, ConsensusConstants, DataRequestInfo, Epoch, EpochConstants, Hash, Hashable, }; use witnet_net::client::tcp::{jsonrpc, JsonRpcClient}; use witnet_node::utils::stop_system_if_panicking; @@ -20,8 +20,8 @@ use witnet_util::timestamp::get_timestamp; #[derive(Default)] pub struct WitPoller { witnet_client: Option>, - wit_tally_polling_rate_ms: u64, - dr_tx_unresolved_timeout_ms: Option, + witnet_dr_txs_polling_rate_ms: u64, + witnet_dr_txs_timeout_ms: u64 } impl Drop for WitPoller { @@ -40,7 +40,7 @@ impl Actor for WitPoller { fn started(&mut self, ctx: &mut Self::Context) { log::debug!("WitPoller actor has been started!"); - self.check_tally_pending_drs(ctx, Duration::from_millis(self.wit_tally_polling_rate_ms)) + self.check_tally_pending_drs(ctx, Duration::from_millis(self.witnet_dr_txs_polling_rate_ms)) } } @@ -54,20 +54,17 @@ impl WitPoller { /// Initialize the `WitPoller` taking the configuration from a `Config` structure /// and a Json-RPC client connected to a Witnet node pub fn from_config(config: &Config, node_client: Addr) -> Self { - let wit_tally_polling_rate_ms = config.wit_tally_polling_rate_ms; - let dr_tx_unresolved_timeout_ms = config.dr_tx_unresolved_timeout_ms; - Self { witnet_client: Some(node_client), - wit_tally_polling_rate_ms, - dr_tx_unresolved_timeout_ms, + witnet_dr_txs_polling_rate_ms: config.witnet_dr_txs_polling_rate_ms, + witnet_dr_txs_timeout_ms: config.witnet_dr_txs_timeout_ms } } fn check_tally_pending_drs(&self, ctx: &mut Context, period: Duration) { let witnet_client = self.witnet_client.clone().unwrap(); - let dr_tx_unresolved_timeout_ms = self.dr_tx_unresolved_timeout_ms; - + let timeout_secs = i64::try_from(self.witnet_dr_txs_timeout_ms / 1000).unwrap(); + let fut = async move { let dr_database_addr = DrDatabase::from_registry(); let dr_reporter_addr = DrReporter::from_registry(); @@ -95,82 +92,77 @@ impl WitPoller { } }; - let report = match report { - Ok(report) => report, - - Err(e) => { - log::debug!( - "[{}] dataRequestReport call error: {}", - dr_id, - e.to_string() - ); - - if let Some(dr_timeout_ms) = dr_tx_unresolved_timeout_ms { - // In case of error, if the data request has been unresolved for more than - // X milliseconds, retry by setting it to "New" - if (current_timestamp - dr_tx_creation_timestamp) - > i64::try_from(dr_timeout_ms / 1000).unwrap() - { - log::debug!("[{}] has been unresolved after more than {} ms, setting to New", dr_id, dr_timeout_ms); - dr_database_addr - .send(SetDrInfoBridge( - dr_id, - DrInfoBridge { - dr_bytes, - dr_state: DrState::New, - dr_tx_hash: None, - dr_tx_creation_timestamp: None, - }, - )) - .await - .unwrap(); - } + if let Ok(report) = report { + match serde_json::from_value::>(report) { + Ok(Some(DataRequestInfo { + tally: Some(tally), + block_hash_dr_tx: Some(dr_block_hash), + current_commit_round: dr_commits_round, + .. + })) => { + log::info!( + "[{}]: found tally {} for dr_tx {}", + dr_id, + &tally.hash(), + dr_tx_hash + ); + + let result = tally.tally.clone(); + // Get timestamp of the epoch at which all data request commit txs + // were incuded in the Witnet blockchain: + let dr_timestamp = + match get_dr_timestamp(witnet_client.clone(), dr_block_hash, dr_commits_round).await { + Ok(timestamp) => timestamp, + Err(()) => continue, + }; + + dr_reporter_msgs.push(Report { + dr_id, + dr_timestamp, + dr_tx_hash, + dr_tally_tx_hash: tally.hash(), + result, + }); } - continue; - } - }; - - match serde_json::from_value::>(report) { - Ok(Some(DataRequestInfo { - tally: Some(tally), - block_hash_dr_tx: Some(dr_block_hash), - .. - })) => { - log::info!( - "[{}] Found possible tally to be reported for dr_tx_hash {}", - dr_id, - dr_tx_hash - ); - - let result = tally.tally; - // Get timestamp of first block with commits. The timestamp of the data - // point is the timestamp of that block minus 45 seconds, because the commit - // transactions are created one epoch earlier. - // TODO: first block with commits is hard to obtain, we are simply using the - // block that included the data request. - let timestamp = - match get_block_timestamp(witnet_client.clone(), dr_block_hash).await { - Ok(timestamp) => timestamp, - Err(()) => continue, - }; + Ok(..) => { + // the data request is being resolved, just not yet + } + Err(e) => { + log::error!("[{}]: cannot deserialize dataRequestReport([{}]): {:?}", + dr_id, + dr_tx_hash, + e + ); + } + }; + } else { + log::debug!("[{}]: dataRequestReport([{}]) call error: {}", + dr_id, + dr_tx_hash, + report.unwrap_err().to_string() + ); + } - dr_reporter_msgs.push(Report { + let elapsed_secs = current_timestamp - dr_tx_creation_timestamp; + if elapsed_secs >= timeout_secs { + log::debug!("[{}]: retrying new dr_tx after {} secs", + dr_id, + elapsed_secs + ); + DrDatabase::from_registry() + .send(SetDrInfoBridge( dr_id, - timestamp, - dr_tx_hash, - result, - }); - } - Ok(..) => { - // No problem, this means the data request has not been resolved yet - log::debug!("[{}] Data request not resolved yet", dr_id); - continue; - } - Err(e) => { - log::error!("[{}] dataRequestReport deserialize error: {:?}", dr_id, e); - continue; - } - }; + DrInfoBridge { + dr_bytes, + dr_state: DrState::New, + dr_tx_hash: None, + dr_tx_creation_timestamp: None, + }, + )) + .await + .unwrap(); + } + } dr_reporter_addr @@ -195,12 +187,13 @@ impl WitPoller { } /// Return the timestamp of this block hash -async fn get_block_timestamp( +async fn get_dr_timestamp( witnet_client: Addr, - block_hash: Hash, + drt_block_hash: Hash, + dr_commits_round: u16, ) -> Result { let method = String::from("getBlock"); - let params = json!([block_hash]); + let params = json!([drt_block_hash]); let req = jsonrpc::Request::method(method) .timeout(Duration::from_millis(5_000)) .params(params) @@ -216,7 +209,7 @@ async fn get_block_timestamp( let block = match report { Ok(value) => serde_json::from_value::(value).expect("failed to deserialize block"), Err(e) => { - log::error!("error in getBlock call ({}): {:?}", block_hash, e); + log::error!("error in getBlock call ({}): {:?}", drt_block_hash, e); return Err(()); } }; @@ -232,9 +225,10 @@ async fn get_block_timestamp( checkpoint_zero_timestamp: consensus_constants.checkpoint_zero_timestamp, checkpoints_period: consensus_constants.checkpoints_period, }; - // TODO: try to guess commit block by adding +1 to block_epoch - // When we actually use the hash of the commit block, this +1 must be removed - let timestamp = convert_block_epoch_to_timestamp(epoch_constants, block_epoch + 1); + let timestamp = convert_block_epoch_to_timestamp( + epoch_constants, + block_epoch + u32::from(dr_commits_round + 1) + ); Ok(timestamp) } From 2f9ed7302259e7be2f50affba5a20a30af34dbea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Fri, 1 Mar 2024 17:11:06 +0100 Subject: [PATCH 62/83] chore(c-bridge): bump binary version to 2.0.0 --- Cargo.lock | 177 +++++++++++++++++++++--- bridges/centralized-ethereum/Cargo.toml | 3 +- 2 files changed, 163 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94f549a6d..5af240e5c 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,6 +260,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfdb4953a096c551ce9ace855a604d702e6e62d77fac690575ae347571717f5" +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "bincode" version = "1.3.3" @@ -308,10 +319,22 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" dependencies = [ - "funty", - "radium", + "funty 1.1.0", + "radium 0.6.2", "tap", - "wyz", + "wyz 0.2.0", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty 2.0.0", + "radium 0.7.0", + "tap", + "wyz 0.5.1", ] [[package]] @@ -971,7 +994,7 @@ version = "16.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c98847055d934070b90e806e12d3936b787d0a115068981c1d8dfd5dfef5a5" dependencies = [ - "ethereum-types", + "ethereum-types 0.12.1", "hex", "serde", "serde_json", @@ -993,17 +1016,40 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "ethbloom" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11da94e443c60508eb62cf256243a64da87304c2802ac2528847f79d750007ef" +dependencies = [ + "crunchy", + "fixed-hash", + "tiny-keccak", +] + [[package]] name = "ethereum-types" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05136f7057fe789f06e6d41d07b34e6f70d8c86e5693b60f97aaa6553553bdaf" dependencies = [ - "ethbloom", + "ethbloom 0.11.1", "fixed-hash", "impl-rlp", "impl-serde", - "primitive-types", + "primitive-types 0.10.1", + "uint", +] + +[[package]] +name = "ethereum-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2827b94c556145446fcce834ca86b7abf0c39a805883fe20e72c5bfdb5a0dc6" +dependencies = [ + "ethbloom 0.12.1", + "fixed-hash", + "primitive-types 0.11.1", "uint", ] @@ -1148,6 +1194,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.1.31" @@ -1629,7 +1681,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" dependencies = [ - "parity-scale-codec", + "parity-scale-codec 2.3.1", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec 3.6.9", ] [[package]] @@ -2613,10 +2674,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" dependencies = [ "arrayvec", - "bitvec", + "bitvec 0.20.4", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive 2.3.1", + "serde", +] + +[[package]] +name = "parity-scale-codec" +version = "3.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881331e34fa842a2fb61cc2db9643a8fedc615e47cfcc52597d1af0db9a7e8fe" +dependencies = [ + "arrayvec", + "bitvec 1.0.1", "byte-slice-cast", "impl-trait-for-tuples", - "parity-scale-codec-derive", + "parity-scale-codec-derive 3.6.9", "serde", ] @@ -2632,6 +2707,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "parity-scale-codec-derive" +version = "3.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b" +dependencies = [ + "proc-macro-crate 2.0.2", + "proc-macro2 1.0.78", + "quote 1.0.35", + "syn 1.0.109", +] + [[package]] name = "parity-ws" version = "0.10.1" @@ -2922,12 +3009,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" dependencies = [ "fixed-hash", - "impl-codec", + "impl-codec 0.5.1", "impl-rlp", "impl-serde", "uint", ] +[[package]] +name = "primitive-types" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28720988bff275df1f51b171e1b2a18c30d194c4d2b61defdacecd625a5d94a" +dependencies = [ + "fixed-hash", + "impl-codec 0.6.0", + "uint", +] + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -2944,7 +3042,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime", + "toml_edit 0.20.2", ] [[package]] @@ -3090,6 +3198,12 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.6.5" @@ -4440,9 +4554,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" @@ -4455,6 +4569,17 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -4830,7 +4955,7 @@ dependencies = [ "bytes 1.5.0", "derive_more", "ethabi", - "ethereum-types", + "ethereum-types 0.12.1", "futures 0.3.30", "futures-timer", "headers", @@ -4848,6 +4973,16 @@ dependencies = [ "url", ] +[[package]] +name = "web3-unit-converter" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe2891c5241406679f80048578f2ab1a72a79cd86bb1c8835ac2c6c9931e9ad" +dependencies = [ + "bigdecimal", + "ethereum-types 0.13.1", +] + [[package]] name = "webbrowser" version = "0.8.12" @@ -5211,7 +5346,7 @@ dependencies = [ [[package]] name = "witnet-centralized-ethereum-bridge" -version = "1.7.1" +version = "2.0.0" dependencies = [ "actix", "async-jsonrpc-client", @@ -5227,6 +5362,7 @@ dependencies = [ "tokio 1.36.0", "toml", "web3", + "web3-unit-converter", "witnet_config", "witnet_data_structures", "witnet_net", @@ -5284,7 +5420,7 @@ dependencies = [ "byteorder", "cbor-codec", "chrono", - "ethereum-types", + "ethereum-types 0.12.1", "exonum-build", "failure", "futures 0.3.30", @@ -5560,6 +5696,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "zstd-sys" version = "2.0.9+zstd.1.5.5" diff --git a/bridges/centralized-ethereum/Cargo.toml b/bridges/centralized-ethereum/Cargo.toml index f8851c8e3..f42e852dc 100644 --- a/bridges/centralized-ethereum/Cargo.toml +++ b/bridges/centralized-ethereum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "witnet-centralized-ethereum-bridge" -version = "1.7.1" +version = "2.0.0" authors = ["Witnet Foundation "] edition = "2018" @@ -25,3 +25,4 @@ witnet_net = { path = "../../net" } witnet_node = { path = "../../node" } witnet_util = { path = "../../util" } witnet_validations = { path = "../../validations" } +web3-unit-converter = "0.1.1" From 5611fd3ce43b0acd7a4c718647bb100d3ae09982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Fri, 1 Mar 2024 17:12:54 +0100 Subject: [PATCH 63/83] chore(c-bridge): cargo fmt --all --- .../src/actors/dr_database.rs | 16 +- .../src/actors/dr_reporter.rs | 144 ++++++++++-------- .../src/actors/dr_sender.rs | 37 ++--- .../src/actors/dr_sender/tests.rs | 3 +- .../src/actors/eth_poller.rs | 61 ++++---- .../src/actors/wit_poller.rs | 48 +++--- bridges/centralized-ethereum/src/config.rs | 3 +- bridges/centralized-ethereum/src/main.rs | 7 +- data_structures/src/chain/mod.rs | 11 +- rad/src/lib.rs | 26 ++-- 10 files changed, 188 insertions(+), 168 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/dr_database.rs b/bridges/centralized-ethereum/src/actors/dr_database.rs index 77c9f397e..5f9ec3058 100644 --- a/bridges/centralized-ethereum/src/actors/dr_database.rs +++ b/bridges/centralized-ethereum/src/actors/dr_database.rs @@ -55,11 +55,11 @@ pub struct DrInfoBridge { /// Data request state #[derive(Clone, Copy, Default, Serialize, Deserialize)] pub enum DrState { - /// New: a new query was detected on the Witnet Oracle contract, + /// New: a new query was detected on the Witnet Oracle contract, /// but has not yet been attended. #[default] New, - /// Pending: a data request transaction was broadcasted to the Witnet blockchain, + /// Pending: a data request transaction was broadcasted to the Witnet blockchain, /// but has not yet been resolved. Pending, /// Finished: the data request result was reported back to the Witnet Oracle contract. @@ -252,11 +252,7 @@ impl Handler for DrDatabase { match self.dr.entry(dr_id) { Entry::Occupied(entry) => { entry.into_mut().dr_state = DrState::Finished; - log::debug!( - "Data request #{} updated to state {}", - dr_id, - dr_state, - ); + log::debug!("Data request #{} updated to state {}", dr_id, dr_state,); } Entry::Vacant(entry) => { entry.insert(DrInfoBridge { @@ -265,11 +261,7 @@ impl Handler for DrDatabase { dr_tx_hash: None, dr_tx_creation_timestamp: None, }); - log::debug!( - "Data request #{} inserted with state {}", - dr_id, - dr_state, - ); + log::debug!("Data request #{} inserted with state {}", dr_id, dr_state,); } } diff --git a/bridges/centralized-ethereum/src/actors/dr_reporter.rs b/bridges/centralized-ethereum/src/actors/dr_reporter.rs index 31c387f76..573014978 100644 --- a/bridges/centralized-ethereum/src/actors/dr_reporter.rs +++ b/bridges/centralized-ethereum/src/actors/dr_reporter.rs @@ -6,7 +6,7 @@ use crate::{ use actix::prelude::*; use std::{collections::HashSet, sync::Arc, time::Duration}; use web3::{ - contract::{self, Contract, tokens::Tokenize}, + contract::{self, tokens::Tokenize, Contract}, ethabi::{ethereum_types::H256, Token}, transports::Http, types::{H160, U256}, @@ -73,7 +73,7 @@ impl DrReporter { pub fn from_config( config: &Config, web3: Web3, - wrb_contract: Arc> + wrb_contract: Arc>, ) -> Self { Self { web3: Some(web3), @@ -87,8 +87,6 @@ impl DrReporter { eth_txs_confirmations: config.eth_txs_confirmations, witnet_dr_max_result_size: config.witnet_dr_max_result_size, pending_dr_reports: Default::default(), - - } } } @@ -125,10 +123,7 @@ impl Handler for DrReporter { msg.reports.retain(|report| { if self.pending_dr_reports.contains(&report.dr_id) { // Timeout is not over yet, no action is needed - log::debug!( - "[{}]: currently being reported...", - report.dr_id - ); + log::debug!("[{}]: currently being reported...", report.dr_id); false } else { @@ -166,27 +161,26 @@ impl Handler for DrReporter { let eth = self.web3.as_ref().unwrap().eth(); let fut = async move { - // Trace low funds alerts if required. let eth_from_balance = match eth.balance(eth_from, None).await { Ok(x) => { if x < U256::from(eth_from_balance_threshold) { eth_from_balance_alert = true; log::warn!( - "EVM address {} running low of funds: {} ETH", - eth_from, + "EVM address {} running low of funds: {} ETH", + eth_from, Unit::Wei(&x.to_string()).to_eth_str().unwrap_or_default() ); } else if eth_from_balance_alert { log::info!("EVM address {} recovered funds.", eth_from); eth_from_balance_alert = false; } - + x } Err(e) => { log::error!("Error geting balance from address {}: {:?}", eth_from, e); - + return eth_from_balance_alert; } }; @@ -202,7 +196,7 @@ impl Handler for DrReporter { Ok(x) => x, Err(e) => { log::error!("Error estimating network gas price: {}", e); - + return eth_from_balance_alert; } }; @@ -223,14 +217,14 @@ impl Handler for DrReporter { ]) }) .collect(); - + let batched_reports = split_by_gas_limit( batched_report, &wrb_contract, eth_from, eth_gas_price, eth_nanowit_wei_price, - eth_max_gas + eth_max_gas, ) .await; @@ -266,8 +260,9 @@ impl Handler for DrReporter { parse_batch_report_error_log(wrb_contract.abi(), log) { if dismissed_dr_reports.insert(dismissed_dr_id) { - log::warn!("[{}]: dismissed => {}", - dismissed_dr_id, + log::warn!( + "[{}]: dismissed => {}", + dismissed_dr_id, reason ); } @@ -286,8 +281,9 @@ impl Handler for DrReporter { .ok(); } else { // Finalize data requests that got successfully reported - log::info!("[{}]: success => drTallyTxHash: {}", - report.dr_id, + log::info!( + "[{}]: success => drTallyTxHash: {}", + report.dr_id, report.dr_tally_tx_hash ); dr_database_addr @@ -298,16 +294,24 @@ impl Handler for DrReporter { .await .ok(); } - }; + } } Err(()) => { - log::error!("[{:?}]: evm tx failed: {}", dr_ids, receipt.transaction_hash); + log::error!( + "[{:?}]: evm tx failed: {}", + dr_ids, + receipt.transaction_hash + ); } } } Ok(Err(e)) => { // Error in call_with_confirmations - log::error!("{}: {:?}", format!("reportResultBatch{:?}", &batched_report), e); + log::error!( + "{}: {:?}", + format!("reportResultBatch{:?}", &batched_report), + e + ); } Err(elapsed) => { // Timeout is over @@ -318,14 +322,20 @@ impl Handler for DrReporter { if let Ok(x) = eth.balance(eth_from, None).await { if x < eth_from_balance { - log::warn!("EVM address {} loss: -{} ETH", - eth_from, - Unit::Wei(&(eth_from_balance - x).to_string()).to_eth_str().unwrap_or_default() + log::warn!( + "EVM address {} loss: -{} ETH", + eth_from, + Unit::Wei(&(eth_from_balance - x).to_string()) + .to_eth_str() + .unwrap_or_default() ); } else { - log::debug!("EVM address {} revenue: +{} ETH", + log::debug!( + "EVM address {} revenue: +{} ETH", eth_from, - Unit::Wei(&(x - eth_from_balance).to_string()).to_eth_str().unwrap_or_default() + Unit::Wei(&(x - eth_from_balance).to_string()) + .to_eth_str() + .unwrap_or_default() ); eth_from_balance_alert = false; } @@ -334,13 +344,15 @@ impl Handler for DrReporter { eth_from_balance_alert }; - ctx.spawn(fut.into_actor(self).map(move |eth_from_balance_alert, act, _ctx: &mut Context| { - // Reset timeouts - for dr_id in incoming_dr_ids { - act.pending_dr_reports.remove(&dr_id); - } - act.eth_from_balance_alert = eth_from_balance_alert - })); + ctx.spawn(fut.into_actor(self).map( + move |eth_from_balance_alert, act, _ctx: &mut Context| { + // Reset timeouts + for dr_id in incoming_dr_ids { + act.pending_dr_reports.remove(&dr_id); + } + act.eth_from_balance_alert = eth_from_balance_alert + }, + )); } } @@ -368,15 +380,18 @@ fn parse_batch_report_error_log( match (&query_id.value, &reason.value) { (Token::Uint(query_id), Token::String(reason)) => Some((*query_id, reason.to_string())), _ => { - panic!("Invalid BatchReportError params: {:?}", batch_report_error_log_params); + panic!( + "Invalid BatchReportError params: {:?}", + batch_report_error_log_params + ); } } } -/// Split a batched report (argument of reportResultBatch) into multiple smaller +/// Split a batched report (argument of reportResultBatch) into multiple smaller /// batched reports in order to fit into some gas limit. /// -/// Returns a list of `(batched_report, estimated_gas)` that should be used to +/// Returns a list of `(batched_report, estimated_gas)` that should be used to /// create multiple "reportResultBatch" transactions. async fn split_by_gas_limit( batched_report: Vec, @@ -390,12 +405,11 @@ async fn split_by_gas_limit( let mut stack = vec![batched_report]; while let Some(batch_params) = stack.pop() { - let eth_report_result_batch_params = batch_params.clone(); // -------------------------------------------------------------------------- // First: try to estimate gas required for reporting this batch of tuples ... - + let estimated_gas = wrb_contract .estimate_gas( "reportResultBatch", @@ -407,7 +421,7 @@ async fn split_by_gas_limit( }), ) .await; - + if let Err(e) = estimated_gas { if batch_params.len() <= 1 { // Skip this single-query batch if still not possible to estimate gas @@ -415,14 +429,15 @@ async fn split_by_gas_limit( log::warn!("Skipping report: {:?}", batch_params); } else { // Split batch in half if gas estimation is not possible - let (batch_tuples_1, batch_tuples_2) = batch_params.split_at(batch_params.len() / 2); + let (batch_tuples_1, batch_tuples_2) = + batch_params.split_at(batch_params.len() / 2); stack.push(batch_tuples_1.to_vec()); stack.push(batch_tuples_2.to_vec()); } continue; - } - + } + let estimated_gas = estimated_gas.unwrap(); log::debug!( "reportResultBatch (x{} drs) estimated gas: {:?}", @@ -433,25 +448,26 @@ async fn split_by_gas_limit( // ------------------------------------------------ // Second: try to estimate actual profit, if any... - let query_ids: Vec = batch_params.iter().map(|report_params| { - if let Token::Tuple(report_params) = report_params { - assert_eq!(report_params.len(), 4); - - report_params[0].clone() - } else { - panic!("Cannot extract query id from batch tuple"); - } - }).collect(); + let query_ids: Vec = batch_params + .iter() + .map(|report_params| { + if let Token::Tuple(report_params) = report_params { + assert_eq!(report_params.len(), 4); + + report_params[0].clone() + } else { + panic!("Cannot extract query id from batch tuple"); + } + }) + .collect(); - // the size of the report result tx data may affect the actual profit + // the size of the report result tx data may affect the actual profit // on some layer-2 EVM chains: let eth_report_result_batch_msg_data = wrb_contract .abi() .function("reportResultBatch") - .and_then(|f| { - f.encode_input(ð_report_result_batch_params.into_tokens()) - }); - + .and_then(|f| f.encode_input(ð_report_result_batch_params.into_tokens())); + let estimated_profit: Result = wrb_contract .query( "estimateReportEarnings", @@ -466,16 +482,18 @@ async fn split_by_gas_limit( opt.gas = eth_max_gas.map(Into::into); opt.gas_price = Some(eth_gas_price); }), - None + None, ) .await; - + match estimated_profit { Ok(estimated_profit) if estimated_profit > U256::from(0) => { log::debug!( "reportResultBatch (x{} drs) estimated profit: {:?} ETH", batch_params.len(), - Unit::Wei(&estimated_profit.to_string()).to_eth_str().unwrap_or_default(), + Unit::Wei(&estimated_profit.to_string()) + .to_eth_str() + .unwrap_or_default(), ); v.push((batch_params, estimated_gas)); continue; @@ -597,7 +615,7 @@ mod tests { dr_tally_tx_hash: Hash::SHA256([ 106, 107, 78, 5, 218, 5, 159, 172, 215, 12, 141, 98, 19, 163, 167, 65, 62, 79, 3, 170, 169, 162, 186, 24, 59, 135, 45, 146, 133, 85, 250, 155, - ]), + ]), result: vec![26, 160, 41, 182, 230], }], }; @@ -669,7 +687,7 @@ mod tests { assert_eq!( parse_batch_report_error_log(&wrb_contract_abi, log_posted_result), Some(( - U256::from(63605), + U256::from(63605), String::from("WitnetOracle: query not in Posted status"), )) ); diff --git a/bridges/centralized-ethereum/src/actors/dr_sender.rs b/bridges/centralized-ethereum/src/actors/dr_sender.rs index 3e69e8923..8e22bbb38 100644 --- a/bridges/centralized-ethereum/src/actors/dr_sender.rs +++ b/bridges/centralized-ethereum/src/actors/dr_sender.rs @@ -51,10 +51,7 @@ impl Actor for DrSender { fn started(&mut self, ctx: &mut Self::Context) { log::debug!("DrSender actor has been started!"); - self.check_new_drs( - ctx, - Duration::from_millis(self.polling_rate_ms), - ); + self.check_new_drs(ctx, Duration::from_millis(self.polling_rate_ms)); } } @@ -91,14 +88,13 @@ impl DrSender { if witnet_node_pkh.is_none() { // get witnet node's pkh if not yet known - let req = jsonrpc::Request::method("getPkh") - .timeout(Duration::from_millis(5000)); + let req = jsonrpc::Request::method("getPkh").timeout(Duration::from_millis(5000)); let res = witnet_client.send(req).await; witnet_node_pkh = match res { Ok(Ok(res)) => match serde_json::from_value::(res) { Ok(pkh) => Some(pkh), - Err(_) => None - } + Err(_) => None, + }, Ok(Err(_)) => { log::warn!("Cannot deserialize witnet node's pkh, will retry later"); @@ -106,10 +102,10 @@ impl DrSender { } Err(_) => { log::warn!("Cannot get witnet node's pkh, will retry later"); - + None } - }; + }; } else { // TODO: alert if number of big enough utxos is less number of drs to broadcast // let req = jsonrpc::Request::method("getUtxoInfo") @@ -124,9 +120,9 @@ impl DrSender { for (dr_id, dr_bytes) in new_drs { match deserialize_and_validate_dr_bytes( - &dr_bytes, - witnet_dr_min_collateral_nanowits, - witnet_dr_max_value_nanowits, + &dr_bytes, + witnet_dr_min_collateral_nanowits, + witnet_dr_max_value_nanowits, ) { Ok(dr_output) => { let req = jsonrpc::Request::method("sendRequest") @@ -172,11 +168,7 @@ impl DrSender { Err(e) => { // Error sending transaction: node not synced, not enough balance, etc. // Do nothing, will retry later. - log::error!( - "[{}]: cannot broadcast dr_tx: {}", - dr_id, - e - ); + log::error!("[{}]: cannot broadcast dr_tx: {}", dr_id, e); continue; } } @@ -184,12 +176,9 @@ impl DrSender { Err(err) => { // Error deserializing or validating data request: mark data request as // error and report error as result to ethereum. - log::error!("[{}]: unacceptable data request bytecode: {}", - dr_id, - err - ); + log::error!("[{}]: unacceptable data request bytecode: {}", dr_id, err); let result = err.encode_cbor(); - // In this case there is no data request transaction, so + // In this case there is no data request transaction, so // we set both the dr_tx_hash and dr_tally_tx_hash to zero values. let zero_hash = "0000000000000000000000000000000000000000000000000000000000000000" @@ -324,7 +313,7 @@ fn deserialize_and_validate_dr_bytes( PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; validate_data_request_output( &dr_output, - dr_output.collateral, // we don't want to ever alter the dro_hash + dr_output.collateral, // we don't want to ever alter the dro_hash required_reward_collateral_ratio, ¤t_active_wips(), ) diff --git a/bridges/centralized-ethereum/src/actors/dr_sender/tests.rs b/bridges/centralized-ethereum/src/actors/dr_sender/tests.rs index 2992bc083..58ae02e43 100644 --- a/bridges/centralized-ethereum/src/actors/dr_sender/tests.rs +++ b/bridges/centralized-ethereum/src/actors/dr_sender/tests.rs @@ -114,6 +114,7 @@ fn deserialize_and_validate_dr_bytes_wip_0022() { let dro_bytes = dro.to_pb_bytes().unwrap(); let witnet_dr_max_value_nanowits = 100_000_000_000; - let err = deserialize_and_validate_dr_bytes(&dro_bytes, witnet_dr_max_value_nanowits).unwrap_err(); + let err = + deserialize_and_validate_dr_bytes(&dro_bytes, witnet_dr_max_value_nanowits).unwrap_err(); assert_eq!(err.encode_cbor(), vec![216, 39, 129, 24, 224]); } diff --git a/bridges/centralized-ethereum/src/actors/eth_poller.rs b/bridges/centralized-ethereum/src/actors/eth_poller.rs index e2dc3340b..f33d33927 100644 --- a/bridges/centralized-ethereum/src/actors/eth_poller.rs +++ b/bridges/centralized-ethereum/src/actors/eth_poller.rs @@ -1,6 +1,7 @@ use crate::{ actors::dr_database::{ - DrDatabase, DrInfoBridge, DrState, GetLastDrId, SetDrInfoBridge, SetDrState, WitnetQueryStatus + DrDatabase, DrInfoBridge, DrState, GetLastDrId, SetDrInfoBridge, SetDrState, + WitnetQueryStatus, }, config::Config, }; @@ -47,10 +48,7 @@ impl Actor for EthPoller { fn started(&mut self, ctx: &mut Self::Context) { log::debug!("EthPoller actor has been started!"); - self.check_new_requests_from_ethereum( - ctx, - Duration::from_millis(self.polling_rate_ms), - ); + self.check_new_requests_from_ethereum(ctx, Duration::from_millis(self.polling_rate_ms)); } } @@ -62,7 +60,11 @@ impl SystemService for EthPoller {} impl EthPoller { /// Initialize `PeersManager` taking the configuration from a `Config` structure - pub fn from_config(config: &Config, web3: Web3, wrb_contract: Arc>) -> Self { + pub fn from_config( + config: &Config, + web3: Web3, + wrb_contract: Arc>, + ) -> Self { Self { web3: Some(web3), wrb_contract: Some(wrb_contract), @@ -78,7 +80,7 @@ impl EthPoller { let wrb_contract = self.wrb_contract.clone().unwrap(); let skip_first = U256::from(self.skip_first); let max_batch_size = self.max_batch_size; - + // Check requests let fut = async move { let dr_database_addr = DrDatabase::from_registry(); @@ -103,9 +105,7 @@ impl EthPoller { let last_dr_id = dr_database_addr.send(GetLastDrId).await; - if let (Ok(mut next_dr_id), Ok(Ok(mut last_dr_id))) = - (next_dr_id, last_dr_id) - { + if let (Ok(mut next_dr_id), Ok(Ok(mut last_dr_id))) = (next_dr_id, last_dr_id) { if last_dr_id < skip_first { log::debug!( "Skipping first {} queries as per SKIP_FIRST config param", @@ -119,8 +119,8 @@ impl EthPoller { } let init_index = usize::try_from(last_dr_id + 1).unwrap(); let last_index = usize::try_from(next_dr_id).unwrap(); - let ids = init_index .. last_index; - let ids: Vec = ids.map(|id| { Token::Uint(id.into()) }).collect(); + let ids = init_index..last_index; + let ids: Vec = ids.map(|id| Token::Uint(id.into())).collect(); let queries_status: Result, web3::contract::Error> = wrb_contract .query( @@ -128,10 +128,10 @@ impl EthPoller { Token::Tuple(ids.clone()), None, contract::Options::default(), - None + None, ) .await; - + if let Ok(queries_status) = queries_status { let mut posted_ids: Vec = vec![]; for (pos, status) in queries_status.iter().enumerate() { @@ -144,11 +144,9 @@ impl EthPoller { log::info!("[{}]: not yet reported.", query_id); posted_ids.push(Token::Uint(query_id)); } - WitnetQueryStatus::Reported - | WitnetQueryStatus::Finalized - => { + WitnetQueryStatus::Reported | WitnetQueryStatus::Finalized => { log::debug!("[{}]: already reported.", query_id); - dr_database_addr.do_send(SetDrState { + dr_database_addr.do_send(SetDrState { dr_id: query_id, dr_state: DrState::Finished, }); @@ -156,15 +154,16 @@ impl EthPoller { } } if !posted_ids.is_empty() { - let dr_bytecodes: Result, web3::contract::Error> = wrb_contract - .query( - "extractWitnetDataRequests", - Token::Tuple(posted_ids.clone()), - None, - contract::Options::default(), - None - ) - .await; + let dr_bytecodes: Result, web3::contract::Error> = + wrb_contract + .query( + "extractWitnetDataRequests", + Token::Tuple(posted_ids.clone()), + None, + contract::Options::default(), + None, + ) + .await; if let Ok(dr_bytecodes) = dr_bytecodes { for (pos, dr_id) in posted_ids.iter().enumerate() { dr_database_addr.do_send(SetDrInfoBridge( @@ -172,7 +171,7 @@ impl EthPoller { DrInfoBridge { dr_bytes: dr_bytecodes[pos].to_owned(), ..Default::default() - } + }, )); } } else { @@ -185,9 +184,9 @@ impl EthPoller { } else { log::error!( "Fail to get status of queries #{} to #{}: {}", - init_index, - last_index, - queries_status.unwrap_err().to_string() + init_index, + last_index, + queries_status.unwrap_err().to_string() ); } } diff --git a/bridges/centralized-ethereum/src/actors/wit_poller.rs b/bridges/centralized-ethereum/src/actors/wit_poller.rs index a971361b2..0552ec84c 100644 --- a/bridges/centralized-ethereum/src/actors/wit_poller.rs +++ b/bridges/centralized-ethereum/src/actors/wit_poller.rs @@ -21,7 +21,7 @@ use witnet_util::timestamp::get_timestamp; pub struct WitPoller { witnet_client: Option>, witnet_dr_txs_polling_rate_ms: u64, - witnet_dr_txs_timeout_ms: u64 + witnet_dr_txs_timeout_ms: u64, } impl Drop for WitPoller { @@ -40,7 +40,10 @@ impl Actor for WitPoller { fn started(&mut self, ctx: &mut Self::Context) { log::debug!("WitPoller actor has been started!"); - self.check_tally_pending_drs(ctx, Duration::from_millis(self.witnet_dr_txs_polling_rate_ms)) + self.check_tally_pending_drs( + ctx, + Duration::from_millis(self.witnet_dr_txs_polling_rate_ms), + ) } } @@ -57,14 +60,14 @@ impl WitPoller { Self { witnet_client: Some(node_client), witnet_dr_txs_polling_rate_ms: config.witnet_dr_txs_polling_rate_ms, - witnet_dr_txs_timeout_ms: config.witnet_dr_txs_timeout_ms + witnet_dr_txs_timeout_ms: config.witnet_dr_txs_timeout_ms, } } fn check_tally_pending_drs(&self, ctx: &mut Context, period: Duration) { let witnet_client = self.witnet_client.clone().unwrap(); let timeout_secs = i64::try_from(self.witnet_dr_txs_timeout_ms / 1000).unwrap(); - + let fut = async move { let dr_database_addr = DrDatabase::from_registry(); let dr_reporter_addr = DrReporter::from_registry(); @@ -106,16 +109,21 @@ impl WitPoller { &tally.hash(), dr_tx_hash ); - + let result = tally.tally.clone(); // Get timestamp of the epoch at which all data request commit txs // were incuded in the Witnet blockchain: - let dr_timestamp = - match get_dr_timestamp(witnet_client.clone(), dr_block_hash, dr_commits_round).await { - Ok(timestamp) => timestamp, - Err(()) => continue, - }; - + let dr_timestamp = match get_dr_timestamp( + witnet_client.clone(), + dr_block_hash, + dr_commits_round, + ) + .await + { + Ok(timestamp) => timestamp, + Err(()) => continue, + }; + dr_reporter_msgs.push(Report { dr_id, dr_timestamp, @@ -128,15 +136,17 @@ impl WitPoller { // the data request is being resolved, just not yet } Err(e) => { - log::error!("[{}]: cannot deserialize dataRequestReport([{}]): {:?}", - dr_id, + log::error!( + "[{}]: cannot deserialize dataRequestReport([{}]): {:?}", + dr_id, dr_tx_hash, e ); } }; } else { - log::debug!("[{}]: dataRequestReport([{}]) call error: {}", + log::debug!( + "[{}]: dataRequestReport([{}]) call error: {}", dr_id, dr_tx_hash, report.unwrap_err().to_string() @@ -145,8 +155,9 @@ impl WitPoller { let elapsed_secs = current_timestamp - dr_tx_creation_timestamp; if elapsed_secs >= timeout_secs { - log::debug!("[{}]: retrying new dr_tx after {} secs", - dr_id, + log::debug!( + "[{}]: retrying new dr_tx after {} secs", + dr_id, elapsed_secs ); DrDatabase::from_registry() @@ -162,7 +173,6 @@ impl WitPoller { .await .unwrap(); } - } dr_reporter_addr @@ -226,8 +236,8 @@ async fn get_dr_timestamp( checkpoints_period: consensus_constants.checkpoints_period, }; let timestamp = convert_block_epoch_to_timestamp( - epoch_constants, - block_epoch + u32::from(dr_commits_round + 1) + epoch_constants, + block_epoch + u32::from(dr_commits_round + 1), ); Ok(timestamp) diff --git a/bridges/centralized-ethereum/src/config.rs b/bridges/centralized-ethereum/src/config.rs index 6e8dfd2de..eb79db3cb 100644 --- a/bridges/centralized-ethereum/src/config.rs +++ b/bridges/centralized-ethereum/src/config.rs @@ -13,7 +13,6 @@ use witnet_data_structures::chain::Environment; #[derive(Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Config { - /// Ethereum account used to report data request results pub eth_from: H160, /// Ethereum account balance under which alerts will be logged @@ -37,7 +36,7 @@ pub struct Config { pub eth_txs_timeout_ms: u64, /// Address of the WitnetRequestsBoard contract pub eth_witnet_oracle: H160, - + /// Minimum collateral required on data requests read from the WitnetOracle contract pub witnet_dr_min_collateral_nanowits: u64, /// Maximium data request transaction fee assumed by the bridge diff --git a/bridges/centralized-ethereum/src/main.rs b/bridges/centralized-ethereum/src/main.rs index 132a21c20..c6f20d8f7 100644 --- a/bridges/centralized-ethereum/src/main.rs +++ b/bridges/centralized-ethereum/src/main.rs @@ -97,7 +97,8 @@ fn run(callback: fn()) -> Result<(), String> { let wrb_contract = Arc::new(wrb_contract); // Start EthPoller actor - let eth_poller_addr = EthPoller::from_config(&config, web3.clone(), wrb_contract.clone()).start(); + let eth_poller_addr = + EthPoller::from_config(&config, web3.clone(), wrb_contract.clone()).start(); SystemRegistry::set(eth_poller_addr); // Start DrReporter actor @@ -106,8 +107,8 @@ fn run(callback: fn()) -> Result<(), String> { // Start Json-RPC actor connected to Witnet node let node_client = JsonRpcClient::start(&config.witnet_jsonrpc_socket.to_string()) - .expect("Json-RPC Client actor failed to started"); - + .expect("Json-RPC Client actor failed to started"); + // Start WitPoller actor let wit_poller_addr = WitPoller::from_config(&config, node_client.clone()).start(); SystemRegistry::set(wit_poller_addr); diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index ef8ddd7a9..303dd595c 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -1896,7 +1896,10 @@ pub enum RADType { impl RADType { pub fn is_http(&self) -> bool { - matches!(self, RADType::HttpGet | RADType::HttpPost | RADType::HttpHead) + matches!( + self, + RADType::HttpGet | RADType::HttpPost | RADType::HttpHead + ) } } @@ -2059,8 +2062,10 @@ impl RADRetrieve { &[Field::Kind, Field::Url, Field::Script], &[Field::Body, Field::Headers], ) - }, - RADType::HttpHead => check(&[Field::Kind, Field::Url, Field::Script], &[Field::Headers]) + } + RADType::HttpHead => { + check(&[Field::Kind, Field::Url, Field::Script], &[Field::Headers]) + } } } diff --git a/rad/src/lib.rs b/rad/src/lib.rs index 056323721..9c5667c0f 100644 --- a/rad/src/lib.rs +++ b/rad/src/lib.rs @@ -25,7 +25,7 @@ use crate::{ create_radon_script_from_filters_and_reducer, execute_radon_script, unpack_radon_script, RadonScriptExecutionSettings, }, - types::{array::RadonArray, bytes::RadonBytes, string::RadonString, map::RadonMap, RadonTypes}, + types::{array::RadonArray, bytes::RadonBytes, map::RadonMap, string::RadonString, RadonTypes}, user_agents::UserAgent, }; use core::convert::From; @@ -181,12 +181,18 @@ fn headers_response_with_data_report( context: &mut ReportContext, settings: RadonScriptExecutionSettings, ) -> Result> { - let headers: BTreeMap = response.split("\r\n").map(|line| { - let parts: Vec<&str> = line.split(":").map(|part| part.trim()).collect(); - // todo: check there are two parts, and two parts only - // todo: make sure that values from repeated keys get appended within a RadonArray - (String::from(parts[0]), RadonTypes::from(RadonString::from(parts[1]))) - }).collect(); + let headers: BTreeMap = response + .split("\r\n") + .map(|line| { + let parts: Vec<&str> = line.split(":").map(|part| part.trim()).collect(); + // todo: check there are two parts, and two parts only + // todo: make sure that values from repeated keys get appended within a RadonArray + ( + String::from(parts[0]), + RadonTypes::from(RadonString::from(parts[1])), + ) + }) + .collect(); let input = RadonTypes::from(RadonMap::from(headers)); let radon_script = unpack_radon_script(&retrieve.script)?; @@ -216,10 +222,10 @@ pub fn run_retrieval_with_data_report( RADType::Rng => rng_response_with_data_report(response, context), RADType::HttpPost => { string_response_with_data_report(retrieve, response, context, settings) - }, + } RADType::HttpHead => { headers_response_with_data_report(retrieve, response, context, settings) - }, + } _ => Err(RadError::UnknownRetrieval), } } @@ -281,7 +287,7 @@ async fn http_response( builder.method("POST").uri(&retrieve.url), WitnetHttpBody::from(retrieve.body.clone()), ) - }, + } RADType::HttpHead => ( builder.method("HEAD").uri(&retrieve.url), WitnetHttpBody::empty(), From c5697df183d6dfbe6ad53742820eb3041f5436d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Fri, 1 Mar 2024 17:19:15 +0100 Subject: [PATCH 64/83] chore(c-bridge): cargo clippy --fix --- bridges/centralized-ethereum/src/actors/dr_database.rs | 4 ++-- bridges/centralized-ethereum/src/actors/dr_sender.rs | 2 +- data_structures/src/radon_error.rs | 2 +- rad/src/lib.rs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/dr_database.rs b/bridges/centralized-ethereum/src/actors/dr_database.rs index 5f9ec3058..2faeb7f06 100644 --- a/bridges/centralized-ethereum/src/actors/dr_database.rs +++ b/bridges/centralized-ethereum/src/actors/dr_database.rs @@ -184,7 +184,7 @@ impl Handler for DrDatabase { fn handle(&mut self, msg: SetDrInfoBridge, ctx: &mut Self::Context) -> Self::Result { let SetDrInfoBridge(dr_id, dr_info) = msg; - let dr_state = dr_info.dr_state.clone(); + let dr_state = dr_info.dr_state; self.dr.insert(dr_id, dr_info); self.max_dr_id = cmp::max(self.max_dr_id, dr_id); @@ -257,7 +257,7 @@ impl Handler for DrDatabase { Entry::Vacant(entry) => { entry.insert(DrInfoBridge { dr_bytes: vec![], - dr_state: dr_state, + dr_state, dr_tx_hash: None, dr_tx_creation_timestamp: None, }); diff --git a/bridges/centralized-ethereum/src/actors/dr_sender.rs b/bridges/centralized-ethereum/src/actors/dr_sender.rs index 8e22bbb38..a046cd9ea 100644 --- a/bridges/centralized-ethereum/src/actors/dr_sender.rs +++ b/bridges/centralized-ethereum/src/actors/dr_sender.rs @@ -203,7 +203,7 @@ impl DrSender { .await .unwrap(); - return witnet_node_pkh; + witnet_node_pkh }; ctx.spawn(fut.into_actor(self).then(move |node_pkh, _act, ctx| { diff --git a/data_structures/src/radon_error.rs b/data_structures/src/radon_error.rs index 7d3176629..8f43df30a 100644 --- a/data_structures/src/radon_error.rs +++ b/data_structures/src/radon_error.rs @@ -67,7 +67,7 @@ pub enum RadonErrors { /// The request is rejected on the grounds that it may cause the submitter to spend or stake an /// amount of value that is unjustifiably high when compared with the reward they will be getting BridgePoorIncentives = 0xE1, - /// The request result length exceeds a bridge contract defined limit + /// The request result length exceeds the bridge limit BridgeOversizedResult = 0xE2, // This should not exist: /// Some tally error is not intercepted but should diff --git a/rad/src/lib.rs b/rad/src/lib.rs index 9c5667c0f..3c3b836d0 100644 --- a/rad/src/lib.rs +++ b/rad/src/lib.rs @@ -184,7 +184,7 @@ fn headers_response_with_data_report( let headers: BTreeMap = response .split("\r\n") .map(|line| { - let parts: Vec<&str> = line.split(":").map(|part| part.trim()).collect(); + let parts: Vec<&str> = line.split(':').map(|part| part.trim()).collect(); // todo: check there are two parts, and two parts only // todo: make sure that values from repeated keys get appended within a RadonArray ( From f1491afcf6278ff2a6bb1226c4066d5e3741df47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Fri, 1 Mar 2024 17:37:04 +0100 Subject: [PATCH 65/83] chore: bump main crate version to 2.0.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5af240e5c..c2ea7b80d 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -5298,7 +5298,7 @@ dependencies = [ [[package]] name = "witnet" -version = "1.7.1" +version = "2.0.0" dependencies = [ "ansi_term", "bytecount", diff --git a/Cargo.toml b/Cargo.toml index 826fd49c8..8d25a95bc 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "witnet" -version = "1.7.1" +version = "2.0.0" authors = ["Witnet Foundation "] publish = false repository = "witnet/witnet-rust" From e93f9bdb25969f3564736874685fc39866f83dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Tue, 5 Mar 2024 18:12:13 +0100 Subject: [PATCH 66/83] fix: wip... --- bridges/centralized-ethereum/src/actors/eth_poller.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/eth_poller.rs b/bridges/centralized-ethereum/src/actors/eth_poller.rs index f33d33927..f22b203fa 100644 --- a/bridges/centralized-ethereum/src/actors/eth_poller.rs +++ b/bridges/centralized-ethereum/src/actors/eth_poller.rs @@ -114,14 +114,16 @@ impl EthPoller { last_dr_id = skip_first; } if last_dr_id < next_dr_id { - if last_dr_id + max_batch_size > next_dr_id { + if next_dr_id > last_dr_id + max_batch_size { next_dr_id = last_dr_id + max_batch_size; } let init_index = usize::try_from(last_dr_id + 1).unwrap(); - let last_index = usize::try_from(next_dr_id).unwrap(); + let last_index = usize::try_from(next_dr_id + 1).unwrap(); let ids = init_index..last_index; let ids: Vec = ids.map(|id| Token::Uint(id.into())).collect(); + log::debug!("getQueryStatusBatch params => {}", Token::Tuple(ids.clone())); + let queries_status: Result, web3::contract::Error> = wrb_contract .query( "getQueryStatusBatch", From f4f236f57d9352c80bcd753ac6bfb82fb5ef69f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Thu, 7 Mar 2024 17:19:52 +0100 Subject: [PATCH 67/83] chore: upgrade bridge example toml --- bridges/centralized-ethereum/.env.example | 2 +- witnet_centralized_ethereum_bridge.toml | 75 ++++++++++++----------- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/bridges/centralized-ethereum/.env.example b/bridges/centralized-ethereum/.env.example index fc601256d..3392d2cd6 100644 --- a/bridges/centralized-ethereum/.env.example +++ b/bridges/centralized-ethereum/.env.example @@ -3,7 +3,7 @@ # into the environment using some external tool WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_FROM: "0x8c49CAfC4542D9EA9107D4E48412ACEd2A68aA77" -WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_FROM_BALANCE_LIMIT: 100000000000000000 # 0.1 ETH +WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_FROM_BALANCE_THRESHOLD: 100000000000000000 # 0.1 ETH WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_GAS_LIMITS: "report_result = 3_000_000" # (optional) WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_JSONRPC_URL: "http://gateway:8535" WITNET_CENTRALIZED_ETHEREUM_BRIDGE_ETH_MAX_BATCH_SIZE: 64 diff --git a/witnet_centralized_ethereum_bridge.toml b/witnet_centralized_ethereum_bridge.toml index 3d35da96a..3a47efa82 100644 --- a/witnet_centralized_ethereum_bridge.toml +++ b/witnet_centralized_ethereum_bridge.toml @@ -1,59 +1,60 @@ -# Address of the witnet node JSON-RPC server -witnet_jsonrpc_socket = "127.0.0.1:21338" +# Ethereum account used to create the transactions +eth_from = "0x8c49CAfC4542D9EA9107D4E48412ACEd2A68aA77" -# Url of the ethereum client -eth_jsonrpc_url = "http://127.0.0.1:8544" +# Ethereum account balance under which alerts will be logged +eth_from_balance_threshold = 100000000000000000 -# Address of the WitnetRequestsBoard deployed contract -eth_witnet_oracle = "0x6cE42a35C61ccfb42907EEE57eDF14Bb69C7fEF4" +# Url of the ethereum client +eth_jsonrpc_url = "http://127.0.0.1:8503" -# Address of a Request Example deployed contract -request_example_contract_addr = "0xEaA9e7Ea612b169f5b41cfF86dA6322f57264a19" +# Max number of queries to be batched together +eth_max_batch_size = 64 -# Ethereum account used to create the transactions -eth_from = "0x8d86Bc475bEDCB08179c5e6a4d494EbD3b44Ea8B" +# Price of $nanoWit in Wei, used to improve estimation of report profits +eth_nanowit_wei_price = 1 -# Period to check for new requests in the WRB +# Polling period for checking new queries in the WitnetOracle contract eth_new_drs_polling_rate_ms = 45_000 -# Period to check for completed requests in Witnet -witnet_tallies_polling_rate_ms = 45_000 +# Number of block confirmations needed to assume finality when sending transactions to ethereum +eth_txs_confirmations = 2 -# Period to post new requests to Witnet -witnet_polling_rate_ms = 45_000 +# Max time to wait for an ethereum transaction to be confirmed before returning an error +eth_txs_timeout_ms = 900000 -# If the data request has been sent to witnet but it is not included in a block, retry after this many milliseconds -witnet_dr_tx_retry_timeout_ms = 600_000 # 10 minutes +# Address of the WitnetRequestsBoard deployed contract +eth_witnet_oracle = "0x77703aE126B971c9946d562F41Dd47071dA00777" -# Maximum data request result size (in bytes) -# TODO: Choose a proper value -witnet_dr_max_result_size = 100 -# Max time to wait for an ethereum transaction to be confirmed before returning an error -eth_txs_timeout_ms = 900_000 # 15 minutes +# Minimum collateral required on data requests read from the WitnetOracle contract +witnet_dr_min_collateral_nanowits = 20000000000 -# Max value that will be accepted by the bridge node in a data request -# This is the maximum amount that the relayer is willing to lose per one data request -witnet_dr_max_value_nanowits = 100_000_000_000 +# Maximium data request transaction fee assumed by the bridge +witnet_dr_max_fee_nanowits = 100000 -# Running in the witnet testnet? -witnet_testnet = false +# Maximum data request result size (in bytes) will accept to report +witnet_dr_max_result_size = 64 -# Number of block confirmations needed to assume finality when sending transactions to ethereum -eth_txs_confirmations = 1 +# Maximum data request value that the bridge will accept to relay +witnet_dr_max_value_nanowits = 100000000000 + +# Polling period for checking resolution of data requests in the Witnet blockchain +witnet_dr_txs_polling_rate_ms = 45000 -# Miner fee for the witnet data request transactions, in nanowits -witnet_dr_max_fee_nanowits = 10_000 +# Max time to wait for data request resolutions, in milliseconds +witnet_dr_txs_timeout_ms = 600000 -# Max ratio between the gas price recommended by the provider and the gas price of the requests in the WRB -# That is, the bridge will refrain from paying more than these times the gas price originally set forth by the requesters. -report_result_max_network_gas_price_ratio = 1.0 +# Address of the witnet node JSON-RPC server +witnet_jsonrpc_socket = "127.0.0.1:21338" +# Running in the witnet testnet? +witnet_testnet = false + + +[eth_gas_limits] # Gas limits for some methods. # To let the client estimate, comment out the fields -[eth_gas_limits] -post_data_request = 10000000 -report_result = 2000000 +#report_result = 2000000 [storage] # Path of the folder where RocksDB storage files will be written to. From a7d970d09cd5fc7764f9d1b235ceb3e71187131f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Thu, 7 Mar 2024 17:21:33 +0100 Subject: [PATCH 68/83] chore: upgrade bridge/wrb_abi --- bridges/wrb_abi.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/bridges/wrb_abi.json b/bridges/wrb_abi.json index 598af5866..6301e2155 100644 --- a/bridges/wrb_abi.json +++ b/bridges/wrb_abi.json @@ -69,6 +69,11 @@ ], "name": "estimateReportEarnings", "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, { "internalType": "uint256", "name": "", @@ -284,5 +289,24 @@ "outputs": [], "stateMutability": "payable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isReporter", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" } ] \ No newline at end of file From 71be57fedbd5eb75b42a868e2641d5315eee03a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Thu, 7 Mar 2024 17:25:41 +0100 Subject: [PATCH 69/83] fix(c-bridge): web3 calls parameters --- .../src/actors/dr_reporter.rs | 25 ++++++++++--------- .../src/actors/eth_poller.rs | 11 ++++---- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/dr_reporter.rs b/bridges/centralized-ethereum/src/actors/dr_reporter.rs index 573014978..45320f2a9 100644 --- a/bridges/centralized-ethereum/src/actors/dr_reporter.rs +++ b/bridges/centralized-ethereum/src/actors/dr_reporter.rs @@ -468,15 +468,17 @@ async fn split_by_gas_limit( .function("reportResultBatch") .and_then(|f| f.encode_input(ð_report_result_batch_params.into_tokens())); - let estimated_profit: Result = wrb_contract + let params = ( + Token::Array(query_ids), + Token::Bytes(eth_report_result_batch_msg_data.unwrap_or_default()), + Token::Uint(eth_gas_price), + Token::Uint(eth_nanowit_wei_price), + ); + + let estimated_profit: Result<(U256, U256), web3::contract::Error> = wrb_contract .query( "estimateReportEarnings", - Token::Tuple(vec![ - Token::Tuple(query_ids), - Token::Bytes(eth_report_result_batch_msg_data.unwrap_or_default()), - Token::Uint(eth_gas_price), - Token::Uint(eth_nanowit_wei_price), - ]), + params, eth_from, contract::Options::with(|opt| { opt.gas = eth_max_gas.map(Into::into); @@ -487,13 +489,12 @@ async fn split_by_gas_limit( .await; match estimated_profit { - Ok(estimated_profit) if estimated_profit > U256::from(0) => { + Ok((revenues, expenses)) => { log::debug!( - "reportResultBatch (x{} drs) estimated profit: {:?} ETH", + "reportResultBatch (x{} drs) estimated profit: {} - {} ETH", batch_params.len(), - Unit::Wei(&estimated_profit.to_string()) - .to_eth_str() - .unwrap_or_default(), + Unit::Wei(&revenues.to_string()).to_eth_str().unwrap_or_default(), + Unit::Wei(&expenses.to_string()).to_eth_str().unwrap_or_default(), ); v.push((batch_params, estimated_gas)); continue; diff --git a/bridges/centralized-ethereum/src/actors/eth_poller.rs b/bridges/centralized-ethereum/src/actors/eth_poller.rs index f22b203fa..f5305a003 100644 --- a/bridges/centralized-ethereum/src/actors/eth_poller.rs +++ b/bridges/centralized-ethereum/src/actors/eth_poller.rs @@ -122,12 +122,10 @@ impl EthPoller { let ids = init_index..last_index; let ids: Vec = ids.map(|id| Token::Uint(id.into())).collect(); - log::debug!("getQueryStatusBatch params => {}", Token::Tuple(ids.clone())); - - let queries_status: Result, web3::contract::Error> = wrb_contract + let queries_status: Result, web3::contract::Error> = wrb_contract .query( "getQueryStatusBatch", - Token::Tuple(ids.clone()), + ids.clone(), None, contract::Options::default(), None, @@ -138,7 +136,8 @@ impl EthPoller { let mut posted_ids: Vec = vec![]; for (pos, status) in queries_status.iter().enumerate() { let query_id = ids[pos].to_owned().into_uint().unwrap(); - match WitnetQueryStatus::from_code(*status) { + let status: u8 = status.to_string().parse().unwrap(); + match WitnetQueryStatus::from_code(status) { WitnetQueryStatus::Unknown => { log::debug!("[{}]: not available.", query_id); } @@ -160,7 +159,7 @@ impl EthPoller { wrb_contract .query( "extractWitnetDataRequests", - Token::Tuple(posted_ids.clone()), + posted_ids.clone(), None, contract::Options::default(), None, From b842f391a3c1e4a052457c388bb9e4b399157cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Thu, 7 Mar 2024 17:26:27 +0100 Subject: [PATCH 70/83] chore(c-bridge): polish logs --- Cargo.lock | 2 +- .../src/actors/dr_database.rs | 2 +- .../src/actors/dr_reporter.rs | 50 ++++++++++--------- .../src/actors/dr_sender.rs | 19 ++++--- .../src/actors/eth_poller.rs | 15 +++--- .../src/actors/wit_poller.rs | 14 +++--- node/Cargo.toml | 2 +- 7 files changed, 56 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c2ea7b80d..94be117ae 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -5478,7 +5478,7 @@ dependencies = [ [[package]] name = "witnet_node" -version = "1.7.1" +version = "2.0.0" dependencies = [ "actix", "ansi_term", diff --git a/bridges/centralized-ethereum/src/actors/dr_database.rs b/bridges/centralized-ethereum/src/actors/dr_database.rs index 2faeb7f06..67046d5eb 100644 --- a/bridges/centralized-ethereum/src/actors/dr_database.rs +++ b/bridges/centralized-ethereum/src/actors/dr_database.rs @@ -122,7 +122,7 @@ impl Actor for DrDatabase { |dr_database_from_storage, act, _| match dr_database_from_storage { Ok(dr_database_from_storage) => { if let Some(mut dr_database_from_storage) = dr_database_from_storage { - log::info!("Load database from storage"); + log::info!("Database loaded from storage"); act.dr = std::mem::take(&mut dr_database_from_storage.dr); act.max_dr_id = dr_database_from_storage.max_dr_id; } else { diff --git a/bridges/centralized-ethereum/src/actors/dr_reporter.rs b/bridges/centralized-ethereum/src/actors/dr_reporter.rs index 45320f2a9..1f141bb59 100644 --- a/bridges/centralized-ethereum/src/actors/dr_reporter.rs +++ b/bridges/centralized-ethereum/src/actors/dr_reporter.rs @@ -123,7 +123,7 @@ impl Handler for DrReporter { msg.reports.retain(|report| { if self.pending_dr_reports.contains(&report.dr_id) { // Timeout is not over yet, no action is needed - log::debug!("[{}]: currently being reported...", report.dr_id); + log::debug!("[{}] => ignored as it's currently being reported", report.dr_id); false } else { @@ -228,14 +228,13 @@ impl Handler for DrReporter { ) .await; - log::debug!( - "[{:?}] will be reported in {} transactions", - dr_ids, - batched_reports.len() + log::info!( + "{:?} will be reported in {} transactions", + dr_ids, batched_reports.len(), ); for (batched_report, eth_gas_limit) in batched_reports { - log::debug!("Executing reportResultBatch {:?}", batched_report); + // log::debug!("Executing reportResultBatch {:?}", batched_report); let receipt_fut = wrb_contract.call_with_confirmations( "reportResultBatch", @@ -251,7 +250,7 @@ impl Handler for DrReporter { let receipt = tokio::time::timeout(eth_tx_timeout, receipt_fut).await; match receipt { Ok(Ok(receipt)) => { - log::debug!("[{:?}]: tx receipt: {:?}", dr_ids, receipt); + log::debug!("{:?} <> {:?}", dr_ids, receipt); match handle_receipt(&receipt).await { Ok(()) => { let mut dismissed_dr_reports: HashSet = Default::default(); @@ -261,7 +260,7 @@ impl Handler for DrReporter { { if dismissed_dr_reports.insert(dismissed_dr_id) { log::warn!( - "[{}]: dismissed => {}", + "[{}] >< dismissed due to \"{}\" ...", dismissed_dr_id, reason ); @@ -282,7 +281,7 @@ impl Handler for DrReporter { } else { // Finalize data requests that got successfully reported log::info!( - "[{}]: success => drTallyTxHash: {}", + "[{}] <= dr_tally_tx = {}", report.dr_id, report.dr_tally_tx_hash ); @@ -298,8 +297,7 @@ impl Handler for DrReporter { } Err(()) => { log::error!( - "[{:?}]: evm tx failed: {}", - dr_ids, + "reportResultBatch(..) tx reverted: {}", receipt.transaction_hash ); } @@ -309,13 +307,16 @@ impl Handler for DrReporter { // Error in call_with_confirmations log::error!( "{}: {:?}", - format!("reportResultBatch{:?}", &batched_report), + format!("Cannot call reportResultBatch{:?}", &batched_report), e ); } Err(elapsed) => { // Timeout is over - log::warn!("[{:?}]: evm tx timeout after {}", dr_ids, elapsed); + log::warn!( + "Timeout ({} secs) when calling reportResultBatch{:?}", + elapsed, &batched_report + ); } } } @@ -323,15 +324,15 @@ impl Handler for DrReporter { if let Ok(x) = eth.balance(eth_from, None).await { if x < eth_from_balance { log::warn!( - "EVM address {} loss: -{} ETH", + "EVM address {} loss = -{} ETH", eth_from, Unit::Wei(&(eth_from_balance - x).to_string()) .to_eth_str() .unwrap_or_default() ); - } else { + } else if x > eth_from_balance { log::debug!( - "EVM address {} revenue: +{} ETH", + "EVM address {} revenue = +{} ETH", eth_from, Unit::Wei(&(x - eth_from_balance).to_string()) .to_eth_str() @@ -425,8 +426,9 @@ async fn split_by_gas_limit( if let Err(e) = estimated_gas { if batch_params.len() <= 1 { // Skip this single-query batch if still not possible to estimate gas - log::error!("Cannot estimate gas limit: {:?}", e); - log::warn!("Skipping report: {:?}", batch_params); + log::error!("Cannot estimate gas limit: {:?}", e); + log::warn!("Skipping report batch: {:?}", batch_params); + } else { // Split batch in half if gas estimation is not possible let (batch_tuples_1, batch_tuples_2) = @@ -440,7 +442,7 @@ async fn split_by_gas_limit( let estimated_gas = estimated_gas.unwrap(); log::debug!( - "reportResultBatch (x{} drs) estimated gas: {:?}", + "reportResultBatch (x{} drs) estimated gas: {:?}", batch_params.len(), estimated_gas ); @@ -499,11 +501,11 @@ async fn split_by_gas_limit( v.push((batch_params, estimated_gas)); continue; } - Ok(_) => { - if batch_params.len() <= 1 { - log::warn!("Skipping unprofitable report: {:?}", batch_params); - } - } + // Ok(_) => { + // if batch_params.len() <= 1 { + // log::warn!("Skipping unprofitable report: {:?}", batch_params); + // } + // } Err(e) => { if batch_params.len() <= 1 { log::error!("Cannot estimate report profit: {:?}", e); diff --git a/bridges/centralized-ethereum/src/actors/dr_sender.rs b/bridges/centralized-ethereum/src/actors/dr_sender.rs index a046cd9ea..7fc91d107 100644 --- a/bridges/centralized-ethereum/src/actors/dr_sender.rs +++ b/bridges/centralized-ethereum/src/actors/dr_sender.rs @@ -92,7 +92,11 @@ impl DrSender { let res = witnet_client.send(req).await; witnet_node_pkh = match res { Ok(Ok(res)) => match serde_json::from_value::(res) { - Ok(pkh) => Some(pkh), + Ok(pkh) => { + log::info!("Pkh is {}", pkh); + + Some(pkh) + } Err(_) => None, }, Ok(Err(_)) => { @@ -108,10 +112,12 @@ impl DrSender { }; } else { // TODO: alert if number of big enough utxos is less number of drs to broadcast - // let req = jsonrpc::Request::method("getUtxoInfo") + // let req = jsonrpc::Request::method("getBalance") // .timeout(Duration::from_millis(5_000)) - // .params(witnet_node_pkh.unwrap()) + // .params(witnet_node_pkh.clone().unwrap()) // .expect("getUtxoInfo params failed serialization"); + // let res = witnet_client.send(req).await; + // log::debug!("Balance of {:?}: {:?} nanoWIT", witnet_node_pkh.unwrap(), res) } // process latest drs added or set as New in the database @@ -145,6 +151,7 @@ impl DrSender { Ok(dr_tx) => { match serde_json::from_value::(dr_tx) { Ok(dr_tx) => { + log::info!("[{}] => dr_tx = {}", dr_id, dr_tx.hash()); // Save dr_tx_hash in database and set state to Pending dr_database_addr .send(SetDrInfoBridge( @@ -161,14 +168,14 @@ impl DrSender { } Err(e) => { // Unexpected error deserializing hash - panic!("[{}]: cannot deserialize dr_tx: {}", dr_id, e); + panic!("[{}] >< cannot deserialize dr_tx: {}", dr_id, e); } } } Err(e) => { // Error sending transaction: node not synced, not enough balance, etc. // Do nothing, will retry later. - log::error!("[{}]: cannot broadcast dr_tx: {}", dr_id, e); + log::error!("[{}] >< cannot broadcast dr_tx: {}", dr_id, e); continue; } } @@ -176,7 +183,7 @@ impl DrSender { Err(err) => { // Error deserializing or validating data request: mark data request as // error and report error as result to ethereum. - log::error!("[{}]: unacceptable data request bytecode: {}", dr_id, err); + log::error!("[{}] >< unacceptable data request bytecode: {}", dr_id, err); let result = err.encode_cbor(); // In this case there is no data request transaction, so // we set both the dr_tx_hash and dr_tally_tx_hash to zero values. diff --git a/bridges/centralized-ethereum/src/actors/eth_poller.rs b/bridges/centralized-ethereum/src/actors/eth_poller.rs index f5305a003..cea40d6e6 100644 --- a/bridges/centralized-ethereum/src/actors/eth_poller.rs +++ b/bridges/centralized-ethereum/src/actors/eth_poller.rs @@ -75,12 +75,13 @@ impl EthPoller { } fn check_new_requests_from_ethereum(&self, ctx: &mut Context, period: Duration) { - log::debug!("Checking posted queries on the WitnetOracle contract..."); let wrb_contract = self.wrb_contract.clone().unwrap(); let skip_first = U256::from(self.skip_first); let max_batch_size = self.max_batch_size; + log::debug!("Polling WitnetOracle at {:?}", wrb_contract.address()); + // Check requests let fut = async move { let dr_database_addr = DrDatabase::from_registry(); @@ -118,7 +119,7 @@ impl EthPoller { next_dr_id = last_dr_id + max_batch_size; } let init_index = usize::try_from(last_dr_id + 1).unwrap(); - let last_index = usize::try_from(next_dr_id + 1).unwrap(); + let last_index = usize::try_from(next_dr_id).unwrap(); let ids = init_index..last_index; let ids: Vec = ids.map(|id| Token::Uint(id.into())).collect(); @@ -139,14 +140,14 @@ impl EthPoller { let status: u8 = status.to_string().parse().unwrap(); match WitnetQueryStatus::from_code(status) { WitnetQueryStatus::Unknown => { - log::debug!("[{}]: not available.", query_id); + log::warn!("Skipped unavailable query [{}]", query_id); } WitnetQueryStatus::Posted => { - log::info!("[{}]: not yet reported.", query_id); + log::info!("Detected new query [{}]", query_id); posted_ids.push(Token::Uint(query_id)); } WitnetQueryStatus::Reported | WitnetQueryStatus::Finalized => { - log::debug!("[{}]: already reported.", query_id); + log::info!("Skipped already solved query [{}]", query_id); dr_database_addr.do_send(SetDrState { dr_id: query_id, dr_state: DrState::Finished, @@ -177,7 +178,7 @@ impl EthPoller { } } else { log::error!( - "Fail to extract Witnet Data Request bytecodes from queries {:?}", + "Fail to extract Witnet request bytecodes from queries {:?}", posted_ids ); } @@ -186,7 +187,7 @@ impl EthPoller { log::error!( "Fail to get status of queries #{} to #{}: {}", init_index, - last_index, + next_dr_id, queries_status.unwrap_err().to_string() ); } diff --git a/bridges/centralized-ethereum/src/actors/wit_poller.rs b/bridges/centralized-ethereum/src/actors/wit_poller.rs index 0552ec84c..7c7320d87 100644 --- a/bridges/centralized-ethereum/src/actors/wit_poller.rs +++ b/bridges/centralized-ethereum/src/actors/wit_poller.rs @@ -104,9 +104,8 @@ impl WitPoller { .. })) => { log::info!( - "[{}]: found tally {} for dr_tx {}", + "[{}] <= dr_tx = {}", dr_id, - &tally.hash(), dr_tx_hash ); @@ -137,7 +136,7 @@ impl WitPoller { } Err(e) => { log::error!( - "[{}]: cannot deserialize dataRequestReport([{}]): {:?}", + "[{}] => cannot deserialize dr_tx = {}: {:?}", dr_id, dr_tx_hash, e @@ -146,17 +145,16 @@ impl WitPoller { }; } else { log::debug!( - "[{}]: dataRequestReport([{}]) call error: {}", + "[{}] <> dr_tx = {}", dr_id, - dr_tx_hash, - report.unwrap_err().to_string() + dr_tx_hash ); } let elapsed_secs = current_timestamp - dr_tx_creation_timestamp; if elapsed_secs >= timeout_secs { - log::debug!( - "[{}]: retrying new dr_tx after {} secs", + log::warn!( + "[{}] => will retry new dr_tx after {} secs", dr_id, elapsed_secs ); diff --git a/node/Cargo.toml b/node/Cargo.toml index a5a0064df..57fe55a7a 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "witnet_node" -version = "1.7.1" +version = "2.0.0" authors = ["Witnet Foundation "] workspace = ".." description = "node component" From afd6a39e33f609d092bfdd9c34109677881c3604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Thu, 7 Mar 2024 17:34:37 +0100 Subject: [PATCH 71/83] chore: upgrade binary crate versions to 2.0.0 --- Cargo.lock | 6 +++--- data_structures/Cargo.toml | 2 +- toolkit/Cargo.toml | 2 +- wallet/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94be117ae..f77fc518f 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -5411,7 +5411,7 @@ dependencies = [ [[package]] name = "witnet_data_structures" -version = "1.7.1" +version = "2.0.0" dependencies = [ "bech32", "bencher", @@ -5585,7 +5585,7 @@ dependencies = [ [[package]] name = "witnet_toolkit" -version = "1.7.1" +version = "2.0.0" dependencies = [ "failure", "hex", @@ -5629,7 +5629,7 @@ dependencies = [ [[package]] name = "witnet_wallet" -version = "1.7.1" +version = "2.0.0" dependencies = [ "actix", "async-jsonrpc-client", diff --git a/data_structures/Cargo.toml b/data_structures/Cargo.toml index a8e0c4f39..d12761817 100644 --- a/data_structures/Cargo.toml +++ b/data_structures/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Witnet Foundation "] description = "data structures component" edition = "2021" name = "witnet_data_structures" -version = "1.7.1" +version = "2.0.0" workspace = ".." [features] diff --git a/toolkit/Cargo.toml b/toolkit/Cargo.toml index e6dce5291..7ac7b72d4 100644 --- a/toolkit/Cargo.toml +++ b/toolkit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "witnet_toolkit" -version = "1.7.1" +version = "2.0.0" authors = ["Adán SDPC "] edition = "2021" diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index bb1f7ef9b..5a7221a5f 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -2,7 +2,7 @@ authors = ["Witnet Foundation "] edition = "2021" name = "witnet_wallet" -version = "1.7.1" +version = "2.0.0" workspace = ".." [dependencies] From 0acccabac1887c10c4ab2a65c461bd3d5a22b58b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Thu, 7 Mar 2024 17:40:30 +0100 Subject: [PATCH 72/83] chore: cargo fmt --all --- .../src/actors/dr_reporter.rs | 22 +++++++++++++------ .../src/actors/eth_poller.rs | 1 - .../src/actors/wit_poller.rs | 12 ++-------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/dr_reporter.rs b/bridges/centralized-ethereum/src/actors/dr_reporter.rs index 1f141bb59..9acb1d6de 100644 --- a/bridges/centralized-ethereum/src/actors/dr_reporter.rs +++ b/bridges/centralized-ethereum/src/actors/dr_reporter.rs @@ -123,7 +123,10 @@ impl Handler for DrReporter { msg.reports.retain(|report| { if self.pending_dr_reports.contains(&report.dr_id) { // Timeout is not over yet, no action is needed - log::debug!("[{}] => ignored as it's currently being reported", report.dr_id); + log::debug!( + "[{}] => ignored as it's currently being reported", + report.dr_id + ); false } else { @@ -230,7 +233,8 @@ impl Handler for DrReporter { log::info!( "{:?} will be reported in {} transactions", - dr_ids, batched_reports.len(), + dr_ids, + batched_reports.len(), ); for (batched_report, eth_gas_limit) in batched_reports { @@ -314,8 +318,9 @@ impl Handler for DrReporter { Err(elapsed) => { // Timeout is over log::warn!( - "Timeout ({} secs) when calling reportResultBatch{:?}", - elapsed, &batched_report + "Timeout ({} secs) when calling reportResultBatch{:?}", + elapsed, + &batched_report ); } } @@ -428,7 +433,6 @@ async fn split_by_gas_limit( // Skip this single-query batch if still not possible to estimate gas log::error!("Cannot estimate gas limit: {:?}", e); log::warn!("Skipping report batch: {:?}", batch_params); - } else { // Split batch in half if gas estimation is not possible let (batch_tuples_1, batch_tuples_2) = @@ -495,8 +499,12 @@ async fn split_by_gas_limit( log::debug!( "reportResultBatch (x{} drs) estimated profit: {} - {} ETH", batch_params.len(), - Unit::Wei(&revenues.to_string()).to_eth_str().unwrap_or_default(), - Unit::Wei(&expenses.to_string()).to_eth_str().unwrap_or_default(), + Unit::Wei(&revenues.to_string()) + .to_eth_str() + .unwrap_or_default(), + Unit::Wei(&expenses.to_string()) + .to_eth_str() + .unwrap_or_default(), ); v.push((batch_params, estimated_gas)); continue; diff --git a/bridges/centralized-ethereum/src/actors/eth_poller.rs b/bridges/centralized-ethereum/src/actors/eth_poller.rs index cea40d6e6..9c62d42bb 100644 --- a/bridges/centralized-ethereum/src/actors/eth_poller.rs +++ b/bridges/centralized-ethereum/src/actors/eth_poller.rs @@ -75,7 +75,6 @@ impl EthPoller { } fn check_new_requests_from_ethereum(&self, ctx: &mut Context, period: Duration) { - let wrb_contract = self.wrb_contract.clone().unwrap(); let skip_first = U256::from(self.skip_first); let max_batch_size = self.max_batch_size; diff --git a/bridges/centralized-ethereum/src/actors/wit_poller.rs b/bridges/centralized-ethereum/src/actors/wit_poller.rs index 7c7320d87..b3525495f 100644 --- a/bridges/centralized-ethereum/src/actors/wit_poller.rs +++ b/bridges/centralized-ethereum/src/actors/wit_poller.rs @@ -103,11 +103,7 @@ impl WitPoller { current_commit_round: dr_commits_round, .. })) => { - log::info!( - "[{}] <= dr_tx = {}", - dr_id, - dr_tx_hash - ); + log::info!("[{}] <= dr_tx = {}", dr_id, dr_tx_hash); let result = tally.tally.clone(); // Get timestamp of the epoch at which all data request commit txs @@ -144,11 +140,7 @@ impl WitPoller { } }; } else { - log::debug!( - "[{}] <> dr_tx = {}", - dr_id, - dr_tx_hash - ); + log::debug!("[{}] <> dr_tx = {}", dr_id, dr_tx_hash); } let elapsed_secs = current_timestamp - dr_tx_creation_timestamp; From ce8e041f4d9d1f82a1bdec512ff83ba723bd0fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Mon, 13 May 2024 10:13:41 +0200 Subject: [PATCH 73/83] fix(bridge): dismiss unavailable queries detected by eth_poller --- bridges/centralized-ethereum/src/actors/eth_poller.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bridges/centralized-ethereum/src/actors/eth_poller.rs b/bridges/centralized-ethereum/src/actors/eth_poller.rs index 9c62d42bb..859dba80b 100644 --- a/bridges/centralized-ethereum/src/actors/eth_poller.rs +++ b/bridges/centralized-ethereum/src/actors/eth_poller.rs @@ -140,6 +140,10 @@ impl EthPoller { match WitnetQueryStatus::from_code(status) { WitnetQueryStatus::Unknown => { log::warn!("Skipped unavailable query [{}]", query_id); + dr_database_addr.do_send(SetDrState { + dr_id: query_id, + dr_state: DrState::Dismissed, + }); } WitnetQueryStatus::Posted => { log::info!("Detected new query [{}]", query_id); From 3d6efdc669451b5902fb8878321d73f9f04b6f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Mon, 13 May 2024 13:39:37 +0200 Subject: [PATCH 74/83] feat(c-bridge): loop on query status batches until head is reached --- .../src/actors/eth_poller.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/eth_poller.rs b/bridges/centralized-ethereum/src/actors/eth_poller.rs index 859dba80b..8849f4232 100644 --- a/bridges/centralized-ethereum/src/actors/eth_poller.rs +++ b/bridges/centralized-ethereum/src/actors/eth_poller.rs @@ -105,7 +105,7 @@ impl EthPoller { let last_dr_id = dr_database_addr.send(GetLastDrId).await; - if let (Ok(mut next_dr_id), Ok(Ok(mut last_dr_id))) = (next_dr_id, last_dr_id) { + if let (Ok(next_dr_id), Ok(Ok(mut last_dr_id))) = (next_dr_id, last_dr_id) { if last_dr_id < skip_first { log::debug!( "Skipping first {} queries as per SKIP_FIRST config param", @@ -113,15 +113,19 @@ impl EthPoller { ); last_dr_id = skip_first; } - if last_dr_id < next_dr_id { - if next_dr_id > last_dr_id + max_batch_size { - next_dr_id = last_dr_id + max_batch_size; - } + while last_dr_id < next_dr_id { let init_index = usize::try_from(last_dr_id + 1).unwrap(); - let last_index = usize::try_from(next_dr_id).unwrap(); + let last_index = match next_dr_id.cmp(&(last_dr_id + max_batch_size)) { + std::cmp::Ordering::Greater => { + usize::try_from(last_dr_id + max_batch_size).unwrap() + } + _ => usize::try_from(next_dr_id).unwrap(), + }; let ids = init_index..last_index; let ids: Vec = ids.map(|id| Token::Uint(id.into())).collect(); + last_dr_id += U256::from(max_batch_size); + let queries_status: Result, web3::contract::Error> = wrb_contract .query( "getQueryStatusBatch", From 5db1043cbc2a056c5c6bd0116f9f3d1fd0373c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Mon, 13 May 2024 13:40:04 +0200 Subject: [PATCH 75/83] chore(c-bridge): attend pr review comments --- .../src/actors/dr_reporter.rs | 42 ++++++++++--------- .../src/actors/dr_sender.rs | 6 --- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/dr_reporter.rs b/bridges/centralized-ethereum/src/actors/dr_reporter.rs index 9acb1d6de..1919bdc72 100644 --- a/bridges/centralized-ethereum/src/actors/dr_reporter.rs +++ b/bridges/centralized-ethereum/src/actors/dr_reporter.rs @@ -101,7 +101,7 @@ pub struct DrReporterMsg { pub struct Report { /// Data Request's unique query id as known by the WitnetOracle contract pub dr_id: DrId, - /// Timestamp at which reported result was actually generated + /// Timestamp at which the reported result was actually generated pub dr_timestamp: u64, /// Hash of the Data Request Transaction in the Witnet blockchain pub dr_tx_hash: Hash, @@ -327,23 +327,26 @@ impl Handler for DrReporter { } if let Ok(x) = eth.balance(eth_from, None).await { - if x < eth_from_balance { - log::warn!( - "EVM address {} loss = -{} ETH", - eth_from, - Unit::Wei(&(eth_from_balance - x).to_string()) - .to_eth_str() - .unwrap_or_default() - ); - } else if x > eth_from_balance { - log::debug!( - "EVM address {} revenue = +{} ETH", - eth_from, - Unit::Wei(&(x - eth_from_balance).to_string()) - .to_eth_str() - .unwrap_or_default() - ); - eth_from_balance_alert = false; + match x.cmp(ð_from_balance) { + std::cmp::Ordering::Less => { + log::warn!( + "EVM address {} loss = -{} ETH", + eth_from, + Unit::Wei(&(eth_from_balance - x).to_string()) + .to_eth_str() + .unwrap_or_default() + ); + } + std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => { + log::debug!( + "EVM address {} revenue = +{} ETH", + eth_from, + Unit::Wei(&(x - eth_from_balance).to_string()) + .to_eth_str() + .unwrap_or_default() + ); + eth_from_balance_alert = false; + } } } @@ -386,10 +389,11 @@ fn parse_batch_report_error_log( match (&query_id.value, &reason.value) { (Token::Uint(query_id), Token::String(reason)) => Some((*query_id, reason.to_string())), _ => { - panic!( + log::error!( "Invalid BatchReportError params: {:?}", batch_report_error_log_params ); + None } } } diff --git a/bridges/centralized-ethereum/src/actors/dr_sender.rs b/bridges/centralized-ethereum/src/actors/dr_sender.rs index 7fc91d107..7798d1d06 100644 --- a/bridges/centralized-ethereum/src/actors/dr_sender.rs +++ b/bridges/centralized-ethereum/src/actors/dr_sender.rs @@ -112,12 +112,6 @@ impl DrSender { }; } else { // TODO: alert if number of big enough utxos is less number of drs to broadcast - // let req = jsonrpc::Request::method("getBalance") - // .timeout(Duration::from_millis(5_000)) - // .params(witnet_node_pkh.clone().unwrap()) - // .expect("getUtxoInfo params failed serialization"); - // let res = witnet_client.send(req).await; - // log::debug!("Balance of {:?}: {:?} nanoWIT", witnet_node_pkh.unwrap(), res) } // process latest drs added or set as New in the database From b26a88e69b45a2b8c0da655d44d5cfa52d984559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Fri, 31 May 2024 13:45:07 +0200 Subject: [PATCH 76/83] feat(c-brige): force min collateral --- .../src/actors/dr_sender.rs | 32 +++++++++++++++---- data_structures/src/data_request.rs | 1 + 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/dr_sender.rs b/bridges/centralized-ethereum/src/actors/dr_sender.rs index 7798d1d06..6e4090b0b 100644 --- a/bridges/centralized-ethereum/src/actors/dr_sender.rs +++ b/bridges/centralized-ethereum/src/actors/dr_sender.rs @@ -11,6 +11,7 @@ use std::{fmt, time::Duration}; use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; use witnet_data_structures::{ chain::{tapi::current_active_wips, DataRequestOutput, Hashable}, + data_request::calculate_reward_collateral_ratio, error::TransactionError, proto::ProtobufConvert, radon_error::RadonErrors, @@ -177,7 +178,7 @@ impl DrSender { Err(err) => { // Error deserializing or validating data request: mark data request as // error and report error as result to ethereum. - log::error!("[{}] >< unacceptable data request bytecode: {}", dr_id, err); + log::error!("[{}] >< unacceptable data request: {}", dr_id, err); let result = err.encode_cbor(); // In this case there is no data request transaction, so // we set both the dr_tx_hash and dr_tally_tx_hash to zero values. @@ -310,12 +311,11 @@ fn deserialize_and_validate_dr_bytes( match DataRequestOutput::from_pb_bytes(dr_bytes) { Err(e) => Err(DrSenderError::Deserialization { msg: e.to_string() }), Ok(dr_output) => { - let required_reward_collateral_ratio = - PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; + let mut dr_output = dr_output.clone(); validate_data_request_output( &dr_output, - dr_output.collateral, // we don't want to ever alter the dro_hash - required_reward_collateral_ratio, + dr_min_collateral_nanowits, // dro_hash may be altered if dr_output.collateral goes below this value + PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO, ¤t_active_wips(), ) .map_err(|e| match e { @@ -326,7 +326,27 @@ fn deserialize_and_validate_dr_bytes( })?; // Collateral value validation - // If collateral is equal to 0 means that is equal to collateral_minimum value + if dr_output.collateral < dr_min_collateral_nanowits { + // modify data request's collateral if below some minimum, + // while maintaining same reward collateral ratio in such case: + let reward_collateral_ratio = calculate_reward_collateral_ratio( + dr_output.collateral, + dr_min_collateral_nanowits, + dr_output.witness_reward, + ); + let dro_hash = dr_output.hash(); + let dro_prev_collateral = dr_output.collateral; + let dro_prev_witness_reward = dr_output.witness_reward; + dr_output.collateral = dr_min_collateral_nanowits; + dr_output.witness_reward = calculate_reward_collateral_ratio( + dr_min_collateral_nanowits, + dr_min_collateral_nanowits, + reward_collateral_ratio, + ); + log::warn!("DRO [{}]: witnessing collateral ({}) increased to mininum ({})", dro_hash, dro_prev_collateral, dr_min_collateral_nanowits); + log::warn!("DRO [{}]: witnessing reward ({}) proportionally increased ({})", dro_hash, dro_prev_witness_reward, dr_output.witness_reward) + + } if (dr_output.collateral != 0) && (dr_output.collateral < dr_min_collateral_nanowits) { return Err(DrSenderError::InvalidCollateral { msg: format!( diff --git a/data_structures/src/data_request.rs b/data_structures/src/data_request.rs index 4e74ad0b8..f7d375116 100644 --- a/data_structures/src/data_request.rs +++ b/data_structures/src/data_request.rs @@ -517,6 +517,7 @@ pub fn calculate_reward_collateral_ratio( witness_reward: u64, ) -> u64 { let dr_collateral = if collateral == 0 { + // if collateral is equal to 0 means that is equal to collateral_minimum value collateral_minimum } else { collateral From 5ec6673c98f0a1f34553002d309d9945508a9033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Tue, 4 Jun 2024 16:41:23 +0200 Subject: [PATCH 77/83] chore: attend pr review comments --- .../src/actors/dr_sender.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/dr_sender.rs b/bridges/centralized-ethereum/src/actors/dr_sender.rs index 6e4090b0b..0f80f64f2 100644 --- a/bridges/centralized-ethereum/src/actors/dr_sender.rs +++ b/bridges/centralized-ethereum/src/actors/dr_sender.rs @@ -311,7 +311,7 @@ fn deserialize_and_validate_dr_bytes( match DataRequestOutput::from_pb_bytes(dr_bytes) { Err(e) => Err(DrSenderError::Deserialization { msg: e.to_string() }), Ok(dr_output) => { - let mut dr_output = dr_output.clone(); + let mut dr_output = dr_output; validate_data_request_output( &dr_output, dr_min_collateral_nanowits, // dro_hash may be altered if dr_output.collateral goes below this value @@ -343,9 +343,18 @@ fn deserialize_and_validate_dr_bytes( dr_min_collateral_nanowits, reward_collateral_ratio, ); - log::warn!("DRO [{}]: witnessing collateral ({}) increased to mininum ({})", dro_hash, dro_prev_collateral, dr_min_collateral_nanowits); - log::warn!("DRO [{}]: witnessing reward ({}) proportionally increased ({})", dro_hash, dro_prev_witness_reward, dr_output.witness_reward) - + log::warn!( + "DRO [{}]: witnessing collateral ({}) increased to minimum ({})", + dro_hash, + dro_prev_collateral, + dr_min_collateral_nanowits + ); + log::warn!( + "DRO [{}]: witnessing reward ({}) proportionally increased ({})", + dro_hash, + dro_prev_witness_reward, + dr_output.witness_reward + ) } if (dr_output.collateral != 0) && (dr_output.collateral < dr_min_collateral_nanowits) { return Err(DrSenderError::InvalidCollateral { From c756357474c492ab05dc35931e4a0c676858e1b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Mon, 19 Aug 2024 14:18:23 +0200 Subject: [PATCH 78/83] feat(c-bridge): add new config params --- bridges/centralized-ethereum/src/config.rs | 12 ++++++++++++ witnet_centralized_ethereum_bridge.toml | 14 +++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/bridges/centralized-ethereum/src/config.rs b/bridges/centralized-ethereum/src/config.rs index eb79db3cb..bc895a1e8 100644 --- a/bridges/centralized-ethereum/src/config.rs +++ b/bridges/centralized-ethereum/src/config.rs @@ -37,6 +37,12 @@ pub struct Config { /// Address of the WitnetRequestsBoard contract pub eth_witnet_oracle: H160, + /// Let the dog out? + pub watch_dog_enabled: bool, + /// Watch dog polling rate + #[serde(default = "default_watch_dog_polling_rate_ms")] + pub watch_dog_polling_rate_ms: u64, + /// Minimum collateral required on data requests read from the WitnetOracle contract pub witnet_dr_min_collateral_nanowits: u64, /// Maximium data request transaction fee assumed by the bridge @@ -53,6 +59,8 @@ pub struct Config { pub witnet_jsonrpc_socket: SocketAddr, /// Running in the witnet testnet? pub witnet_testnet: bool, + /// Bridge UTXO min value threshold + pub witnet_utxo_min_value_threshold: u64, /// Storage #[serde(deserialize_with = "nested_toml_if_using_envy")] @@ -69,6 +77,10 @@ fn default_max_batch_size() -> u16 { 256 } +fn default_watch_dog_polling_rate_ms() -> u64 { + 900_000 +} + /// Gas limits for some methods. If missing, let the client estimate #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] diff --git a/witnet_centralized_ethereum_bridge.toml b/witnet_centralized_ethereum_bridge.toml index 3a47efa82..1f3909e31 100644 --- a/witnet_centralized_ethereum_bridge.toml +++ b/witnet_centralized_ethereum_bridge.toml @@ -25,18 +25,23 @@ eth_txs_timeout_ms = 900000 # Address of the WitnetRequestsBoard deployed contract eth_witnet_oracle = "0x77703aE126B971c9946d562F41Dd47071dA00777" +# Let the dog out? +watch_dog_enabled = true + +# Polling period for checking and tracing global status +watch_dog_polling_rate_ms = 5_000 # Minimum collateral required on data requests read from the WitnetOracle contract -witnet_dr_min_collateral_nanowits = 20000000000 +witnet_dr_min_collateral_nanowits = 20_000_000_000 # Maximium data request transaction fee assumed by the bridge -witnet_dr_max_fee_nanowits = 100000 +witnet_dr_max_fee_nanowits = 100_000 # Maximum data request result size (in bytes) will accept to report witnet_dr_max_result_size = 64 # Maximum data request value that the bridge will accept to relay -witnet_dr_max_value_nanowits = 100000000000 +witnet_dr_max_value_nanowits = 100_000_000_000 # Polling period for checking resolution of data requests in the Witnet blockchain witnet_dr_txs_polling_rate_ms = 45000 @@ -50,6 +55,8 @@ witnet_jsonrpc_socket = "127.0.0.1:21338" # Running in the witnet testnet? witnet_testnet = false +# Bridge UTXO min value threshold +witnet_utxo_min_value_threshold = 2_000_000_000 [eth_gas_limits] # Gas limits for some methods. @@ -59,3 +66,4 @@ witnet_testnet = false [storage] # Path of the folder where RocksDB storage files will be written to. db_path = ".witnet_bridge/storage" + From 2b07db17af5f6f9dbab1981707a17efd167f6c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Mon, 19 Aug 2024 14:18:37 +0200 Subject: [PATCH 79/83] feat(c-bridge): count drs per state on dr_database --- .../src/actors/dr_database.rs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/bridges/centralized-ethereum/src/actors/dr_database.rs b/bridges/centralized-ethereum/src/actors/dr_database.rs index 67046d5eb..880e962d4 100644 --- a/bridges/centralized-ethereum/src/actors/dr_database.rs +++ b/bridges/centralized-ethereum/src/actors/dr_database.rs @@ -179,6 +179,13 @@ impl Message for SetDrState { type Result = Result<(), ()>; } +/// Count number of data requests in given state +pub struct CountDrsPerState; + +impl Message for CountDrsPerState { + type Result = Result<(u64, u64, u64, u64), ()>; +} + impl Handler for DrDatabase { type Result = (); @@ -274,6 +281,28 @@ impl Handler for DrDatabase { } } +impl Handler for DrDatabase { + type Result = Result<(u64, u64, u64, u64), ()>; + + fn handle(&mut self, _msg: CountDrsPerState, _ctx: &mut Self::Context) -> Self::Result { + let mut drs_new = u64::default(); + let mut drs_pending = u64::default(); + let mut drs_finished = u64::default(); + let mut drs_dismissed = u64::default(); + + self.dr.iter().for_each(|(_dr_id, dr_info)| { + match dr_info.dr_state { + DrState::New => drs_new += 1, + DrState::Pending => drs_pending += 1, + DrState::Finished => drs_finished += 1, + DrState::Dismissed => drs_dismissed += 1, + }; + }); + + Ok((drs_new, drs_pending, drs_finished, drs_dismissed)) + } +} + /// Required trait for being able to retrieve DrDatabase address from system registry impl actix::Supervised for DrDatabase {} From 782064ea0485d3ef97bef33338bc1d89c47fd948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Mon, 19 Aug 2024 14:20:20 +0200 Subject: [PATCH 80/83] feat(c-bridge): implement new watch_dog actor --- .../centralized-ethereum/src/actors/mod.rs | 3 + .../src/actors/watch_dog.rs | 364 ++++++++++++++++++ bridges/centralized-ethereum/src/main.rs | 17 +- 3 files changed, 380 insertions(+), 4 deletions(-) create mode 100644 bridges/centralized-ethereum/src/actors/watch_dog.rs diff --git a/bridges/centralized-ethereum/src/actors/mod.rs b/bridges/centralized-ethereum/src/actors/mod.rs index 94a2b84d2..0f80bbc0c 100644 --- a/bridges/centralized-ethereum/src/actors/mod.rs +++ b/bridges/centralized-ethereum/src/actors/mod.rs @@ -12,3 +12,6 @@ pub mod eth_poller; /// wit_poller actor module pub mod wit_poller; + +/// watch_dog actor module +pub mod watch_dog; diff --git a/bridges/centralized-ethereum/src/actors/watch_dog.rs b/bridges/centralized-ethereum/src/actors/watch_dog.rs new file mode 100644 index 000000000..1c6633148 --- /dev/null +++ b/bridges/centralized-ethereum/src/actors/watch_dog.rs @@ -0,0 +1,364 @@ +use crate::{ + actors::dr_database::{CountDrsPerState, DrDatabase}, + config::Config, +}; +use actix::prelude::*; +use async_jsonrpc_client::{transports::tcp::TcpSocket, Transport}; +use futures_util::compat::Compat01As03; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; +use web3::{ + contract::Contract, + transports::Http, + types::{H160, U256}, +}; +use witnet_net::client::tcp::{jsonrpc, JsonRpcClient}; +use witnet_node::utils::stop_system_if_panicking; + +/// EthPoller actor reads periodically new requests from the WRB Contract and includes them +/// in the DrDatabase +#[derive(Default)] +pub struct WatchDog { + /// JSON RPC connection to Wit/node + pub wit_jsonrpc_socket: String, + /// Bridge UTXO min value threshold + pub wit_utxo_min_value_threshold: u64, + /// Web3 object + pub eth_jsonrpc_url: String, + /// Web3 signer address + pub eth_account: H160, + /// WitOracle bridge contract + pub eth_contract: Option>>, + /// Polling period for global status + pub polling_rate_ms: u64, + /// Instant at which the actor is created + pub start_ts: Option, + /// Eth balance upon first metric report: + pub start_eth_balance: Option, + /// Wit balance upon last refund + pub start_wit_balance: Option, +} + +#[derive(Serialize, Deserialize)] +struct WatchDogOutput { + pub running_secs: u64, +} + +impl Drop for WatchDog { + fn drop(&mut self) { + log::trace!("Dropping WatchDog"); + stop_system_if_panicking("WatchDog"); + } +} + +/// Make actor from EthPoller +impl Actor for WatchDog { + /// Every actor has to provide execution Context in which it can run. + type Context = Context; + + /// Method to be executed when the actor is started + fn started(&mut self, ctx: &mut Self::Context) { + log::debug!("WatchDog actor has been started!"); + + self.watch_global_status(None, None, ctx, Duration::from_millis(self.polling_rate_ms)); + } +} + +/// Required trait for being able to retrieve WatchDog address from system registry +impl actix::Supervised for WatchDog {} +impl SystemService for WatchDog {} + +impl WatchDog { + /// Initialize from config + pub fn from_config(config: &Config, eth_contract: Arc>) -> Self { + Self { + wit_jsonrpc_socket: config.witnet_jsonrpc_socket.to_string(), + wit_utxo_min_value_threshold: config.witnet_utxo_min_value_threshold, + eth_account: config.eth_from, + eth_contract: Some(eth_contract), + eth_jsonrpc_url: config.eth_jsonrpc_url.clone(), + polling_rate_ms: config.watch_dog_polling_rate_ms, + start_ts: Some(Instant::now()), + start_eth_balance: None, + start_wit_balance: None, + } + } + + fn watch_global_status( + &mut self, + eth_balance: Option, + wit_balance: Option, + ctx: &mut Context, + period: Duration, + ) { + if self.start_eth_balance.is_none() && eth_balance.is_some() { + self.start_eth_balance = eth_balance; + } + if let Some(wit_balance) = wit_balance { + if wit_balance > self.start_wit_balance.unwrap_or_default() { + self.start_wit_balance = Some(wit_balance); + log::warn!("Wit account refunded to {} $WIT", wit_balance); + } + } + let start_eth_balance = self.start_eth_balance; + let start_wit_balance = self.start_wit_balance; + let wit_jsonrpc_socket = self.wit_jsonrpc_socket.clone(); + let wit_utxo_min_value_threshold = self.wit_utxo_min_value_threshold; + let eth_jsonrpc_url = self.eth_jsonrpc_url.clone(); + let eth_account = self.eth_account; + let eth_contract_address = self.eth_contract.clone().unwrap().address(); + let running_secs = self.start_ts.unwrap().elapsed().as_secs(); + + let fut = async move { + let mut status = "up-and-running".to_string(); + + if let Err(err) = check_wit_connection_status(&wit_jsonrpc_socket).await { + status = err; + } + let wit_client = JsonRpcClient::start(&wit_jsonrpc_socket) + .expect("cannot start JSON/WIT connection"); + let wit_account = match fetch_wit_account(&wit_client).await { + Ok(pkh) => pkh, + Err(err) => { + if status.eq("up-and-running") { + status = err; + } + None + } + }; + + let wit_balance = match wit_account.clone() { + Some(pkh) => match fetch_wit_account_balance(&wit_client, pkh.as_str()).await { + Ok(wit_balance) => wit_balance, + Err(err) => { + if status.eq("up-and-running") { + status = err; + } + None + } + }, + None => None, + }; + + let wit_utxos_above_threshold = match wit_account.clone() { + Some(pkh) => { + match fetch_wit_account_count_utxos_above( + &wit_client, + pkh.as_str(), + wit_utxo_min_value_threshold, + ) + .await + { + Ok(wit_utxos_above_threshold) => wit_utxos_above_threshold, + Err(err) => { + if status.eq("up-and-running") { + status = err; + } + None + } + } + } + None => None, + }; + + let eth_balance = match check_eth_account_balance(ð_jsonrpc_url, eth_account).await { + Ok(Some(eth_balance)) => { + let eth_balance: f64 = eth_balance.to_string().parse().unwrap_or_default(); + //Some(Unit::Wei(ð_balance.to_string()).to_eth_str().unwrap_or_default()), + Some(eth_balance / 1000000000000000000.0) + } + Ok(None) => None, + Err(err) => { + if status.eq("up-and-running") { + status = err; + } + None + } + }; + + let dr_database = DrDatabase::from_registry(); + let (_, drs_pending, drs_finished, _) = + dr_database.send(CountDrsPerState).await.unwrap().unwrap(); + + let mut metrics: String = "{".to_string(); + metrics.push_str(&format!("\"drsFinished\": {drs_finished}, ")); + metrics.push_str(&format!("\"drsPending\": {drs_pending}, ")); + metrics.push_str(&format!("\"evmAccount\": \"{eth_account}\", ")); + if eth_balance.is_some() { + let eth_balance = eth_balance.unwrap(); + metrics.push_str(&format!("\"evmBalance\": {:.5}, ", eth_balance)); + metrics.push_str(&format!("\"evmContract\": \"{eth_contract_address}\", ")); + if let Some(start_eth_balance) = start_eth_balance { + let eth_hourly_earnings = + ((eth_balance - start_eth_balance) / running_secs as f64) * 3600_f64; + metrics.push_str(&format!( + "\"evmHourlyEarnings\": {:.5}, ", + eth_hourly_earnings + )); + } + } + if wit_account.is_some() { + metrics.push_str(&format!("\"witAccount\": {:?}, ", wit_account.unwrap())); + } + if wit_balance.is_some() { + let wit_balance = wit_balance.unwrap(); + metrics.push_str(&format!("\"witBalance\": {:.5}, ", wit_balance)); + if let Some(start_wit_balance) = start_wit_balance { + let wit_hourly_expenditure = + ((start_wit_balance - wit_balance) / running_secs as f64) * 3600_f64; + metrics.push_str(&format!( + "\"witHourlyExpenditure\": {:.1}, ", + wit_hourly_expenditure + )); + } + } + metrics.push_str(&format!("\"witNodeSocket\": \"{}\", ", wit_jsonrpc_socket)); + if wit_utxos_above_threshold.is_some() { + metrics.push_str(&format!( + "\"witUtxosAboveThreshold\": {}, ", + wit_utxos_above_threshold.unwrap() + )); + } + metrics.push_str(&format!("\"runningSecs\": {running_secs}, ")); + metrics.push_str(&format!("\"status\": \"{status}\"")); + metrics.push_str("}}"); + log::info!("{metrics}"); + + (eth_balance, wit_balance) + }; + + ctx.spawn( + fut.into_actor(self) + .then(move |(eth_balance, wit_balance), _act, ctx| { + // Schedule next iteration only when finished, + // as to avoid multiple tasks running in parallel + ctx.run_later(period, move |act, ctx| { + act.watch_global_status(eth_balance, wit_balance, ctx, period); + }); + actix::fut::ready(()) + }), + ); + } +} + +async fn check_eth_account_balance( + eth_jsonrpc_url: &str, + eth_account: H160, +) -> Result, String> { + let web3_http = web3::transports::Http::new(eth_jsonrpc_url) + .map_err(|_e| "evm-disconnect".to_string()) + .unwrap(); + + let web3 = web3::Web3::new(web3_http); + match web3.eth().syncing().await { + Ok(syncing) => match syncing { + web3::types::SyncState::NotSyncing => { + match web3.eth().balance(eth_account, None).await { + Ok(balance) => Ok(Some(balance)), + _ => Ok(None), + } + } + web3::types::SyncState::Syncing(_) => Err("evm-syncing".to_string()), + }, + Err(_e) => Err("evm-errors".to_string()), + } +} + +async fn check_wit_connection_status(wit_jsonrpc_socket: &str) -> Result<(), String> { + let (_handle, wit_client) = TcpSocket::new(wit_jsonrpc_socket).unwrap(); + let wit_client = Arc::new(wit_client); + let res = wit_client.execute("syncStatus", json!(null)); + let res = Compat01As03::new(res); + let res = tokio::time::timeout(Duration::from_secs(5), res).await; + + match res { + Ok(Ok(_)) => Ok(()), + Ok(Err(_)) => Err("wit-syncing".to_string()), + Err(_elapse) => Err("wit-disconnect".to_string()), + } +} + +async fn fetch_wit_account(wit_client: &Addr) -> Result, String> { + let req = jsonrpc::Request::method("getPkh").timeout(Duration::from_secs(5)); + let res = wit_client.send(req).await; + match res { + Ok(Ok(res)) => match serde_json::from_value::(res) { + Ok(pkh) => Ok(Some(pkh)), + Err(_) => Ok(None), + }, + Ok(Err(_)) => Ok(None), + Err(_) => Err("wit-errors-getPkh".to_string()), + } +} + +async fn fetch_wit_account_balance( + wit_client: &Addr, + wit_account: &str, +) -> Result, String> { + let req = jsonrpc::Request::method("getBalance") + .timeout(Duration::from_secs(5)) + .params(vec![wit_account, "true"]) + .expect("getBalance wrong params"); + + let res = wit_client.send(req).await; + let res = match res { + Ok(res) => res, + Err(_) => { + return Err("wit-errors-getBalance".to_string()); + } + }; + + match res { + Ok(value) => match value.get("total") { + Some(value) => match value.as_f64() { + Some(value) => Ok(Some(value / 1000000000.0)), + None => Ok(None), + }, + None => Ok(None), + }, + Err(_) => Err("wit-errors-getBalance".to_string()), + } +} + +async fn fetch_wit_account_count_utxos_above( + wit_client: &Addr, + wit_account: &str, + threshold: u64, +) -> Result, String> { + let req = jsonrpc::Request::method("getUtxoInfo") + .timeout(Duration::from_secs(5)) + .params(wit_account) + .expect("getUtxoInfo wrong params"); + + let res = wit_client.send(req).await; + let res = match res { + Ok(res) => res, + Err(_) => { + return Err("wit-errors-getUtxoInfo".to_string()); + } + }; + + match res { + Ok(utxo_info) => { + if let Some(utxos) = utxo_info["utxos"].as_array() { + let mut counter: u64 = u64::default(); + for utxo in utxos { + if let Some(value) = utxo["value"].as_u64() { + if value >= threshold { + counter += 1; + } + } + } + + Ok(Some(counter)) + } else { + Ok(None) + } + } + Err(_) => Err("wit-errors-getUtxoInfo".to_string()), + } +} diff --git a/bridges/centralized-ethereum/src/main.rs b/bridges/centralized-ethereum/src/main.rs index c6f20d8f7..c8e6138d4 100644 --- a/bridges/centralized-ethereum/src/main.rs +++ b/bridges/centralized-ethereum/src/main.rs @@ -7,7 +7,7 @@ use structopt::StructOpt; use witnet_centralized_ethereum_bridge::{ actors::{ dr_database::DrDatabase, dr_reporter::DrReporter, dr_sender::DrSender, - eth_poller::EthPoller, wit_poller::WitPoller, + eth_poller::EthPoller, watch_dog::WatchDog, wit_poller::WitPoller, }, check_ethereum_node_running, check_witnet_node_running, config, create_wrb_contract, }; @@ -83,6 +83,7 @@ fn run(callback: fn()) -> Result<(), String> { check_ethereum_node_running(&config.eth_jsonrpc_url) .await .expect("ethereum node not running"); + check_witnet_node_running(&config.witnet_jsonrpc_socket.to_string()) .await .expect("witnet node not running"); @@ -94,6 +95,7 @@ fn run(callback: fn()) -> Result<(), String> { // Web3 contract using HTTP transport with an Ethereum client let (web3, wrb_contract) = create_wrb_contract(&config.eth_jsonrpc_url, config.eth_witnet_oracle); + let wrb_contract = Arc::new(wrb_contract); // Start EthPoller actor @@ -102,25 +104,32 @@ fn run(callback: fn()) -> Result<(), String> { SystemRegistry::set(eth_poller_addr); // Start DrReporter actor - let dr_reporter_addr = DrReporter::from_config(&config, web3, wrb_contract).start(); + let dr_reporter_addr = + DrReporter::from_config(&config, web3.clone(), wrb_contract.clone()).start(); SystemRegistry::set(dr_reporter_addr); // Start Json-RPC actor connected to Witnet node let node_client = JsonRpcClient::start(&config.witnet_jsonrpc_socket.to_string()) - .expect("Json-RPC Client actor failed to started"); + .expect("JSON WIT/RPC node client failed to start"); // Start WitPoller actor let wit_poller_addr = WitPoller::from_config(&config, node_client.clone()).start(); SystemRegistry::set(wit_poller_addr); // Start DrSender actor - let dr_sender_addr = DrSender::from_config(&config, node_client).start(); + let dr_sender_addr = DrSender::from_config(&config, node_client.clone()).start(); SystemRegistry::set(dr_sender_addr); // Initialize Storage Manager let mut node_config = NodeConfig::default(); node_config.storage.db_path = config.storage.db_path.clone(); storage_mngr::start_from_config(node_config); + + // Start WatchDog actor + if config.watch_dog_enabled { + let watch_dog_addr = WatchDog::from_config(&config, wrb_contract.clone()).start(); + SystemRegistry::set(watch_dog_addr); + } }); // Run system From 6203494c65984806be8ad50aa84182b182c9c1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Tue, 20 Aug 2024 12:46:54 +0200 Subject: [PATCH 81/83] chore: attend pr review comments --- .../src/actors/watch_dog.rs | 257 +++++++++--------- 1 file changed, 135 insertions(+), 122 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/watch_dog.rs b/bridges/centralized-ethereum/src/actors/watch_dog.rs index 1c6633148..f9cdfe0e8 100644 --- a/bridges/centralized-ethereum/src/actors/watch_dog.rs +++ b/bridges/centralized-ethereum/src/actors/watch_dog.rs @@ -14,7 +14,7 @@ use std::{ use web3::{ contract::Contract, transports::Http, - types::{H160, U256}, + types::H160, }; use witnet_net::client::tcp::{jsonrpc, JsonRpcClient}; use witnet_node::utils::stop_system_if_panicking; @@ -68,6 +68,32 @@ impl Actor for WatchDog { } } +#[derive(Debug)] +enum WatchDogStatus { + // Add all of them here + UpAndRunning, + EvmDisconnect, + EvmSyncing, + EvmErrors, + WitDisconnect, + WitSyncing, + WitErrors, +} + +impl WatchDogStatus { + fn to_string(&self) -> String { + match self { + WatchDogStatus::UpAndRunning => "up-and-running".to_string(), + WatchDogStatus::EvmDisconnect => "evm-disconnect".to_string(), + WatchDogStatus::EvmSyncing => "evm-syncing".to_string(), + WatchDogStatus::EvmErrors => format!("evm-errors"), + WatchDogStatus::WitDisconnect => "wit-disconnect".to_string(), + WatchDogStatus::WitSyncing => "wit-syncing".to_string(), + WatchDogStatus::WitErrors => format!("wit-errors"), + } + } +} + /// Required trait for being able to retrieve WatchDog address from system registry impl actix::Supervised for WatchDog {} impl SystemService for WatchDog {} @@ -114,66 +140,34 @@ impl WatchDog { let running_secs = self.start_ts.unwrap().elapsed().as_secs(); let fut = async move { - let mut status = "up-and-running".to_string(); + let mut status = WatchDogStatus::UpAndRunning; if let Err(err) = check_wit_connection_status(&wit_jsonrpc_socket).await { status = err; } - let wit_client = JsonRpcClient::start(&wit_jsonrpc_socket) - .expect("cannot start JSON/WIT connection"); - let wit_account = match fetch_wit_account(&wit_client).await { - Ok(pkh) => pkh, + let wit_client = match JsonRpcClient::start(&wit_jsonrpc_socket) { + Ok(client) => client, + Err(_) => return (None, None), + }; + let (wit_account, wit_balance, wit_utxos_above_threshold) = match fetch_wit_info( + &wit_client, + wit_utxo_min_value_threshold + ).await { + Ok((wit_account, wit_balance, wit_utxos_above_threshold)) => { + (wit_account, wit_balance, wit_utxos_above_threshold) + } Err(err) => { - if status.eq("up-and-running") { + if status == WatchDogStatus::UpAndRunning { status = err; } - None - } - }; - - let wit_balance = match wit_account.clone() { - Some(pkh) => match fetch_wit_account_balance(&wit_client, pkh.as_str()).await { - Ok(wit_balance) => wit_balance, - Err(err) => { - if status.eq("up-and-running") { - status = err; - } - None - } - }, - None => None, - }; - - let wit_utxos_above_threshold = match wit_account.clone() { - Some(pkh) => { - match fetch_wit_account_count_utxos_above( - &wit_client, - pkh.as_str(), - wit_utxo_min_value_threshold, - ) - .await - { - Ok(wit_utxos_above_threshold) => wit_utxos_above_threshold, - Err(err) => { - if status.eq("up-and-running") { - status = err; - } - None - } - } + (None, None, None) } - None => None, }; let eth_balance = match check_eth_account_balance(ð_jsonrpc_url, eth_account).await { - Ok(Some(eth_balance)) => { - let eth_balance: f64 = eth_balance.to_string().parse().unwrap_or_default(); - //Some(Unit::Wei(ð_balance.to_string()).to_eth_str().unwrap_or_default()), - Some(eth_balance / 1000000000000000000.0) - } - Ok(None) => None, + Ok(eth_balance) => eth_balance, Err(err) => { - if status.eq("up-and-running") { + if status == WatchDogStatus::UpAndRunning { status = err; } None @@ -224,7 +218,7 @@ impl WatchDog { )); } metrics.push_str(&format!("\"runningSecs\": {running_secs}, ")); - metrics.push_str(&format!("\"status\": \"{status}\"")); + metrics.push_str(&format!("\"status\": \"{}\"", status.to_string())); metrics.push_str("}}"); log::info!("{metrics}"); @@ -248,9 +242,9 @@ impl WatchDog { async fn check_eth_account_balance( eth_jsonrpc_url: &str, eth_account: H160, -) -> Result, String> { +) -> Result, WatchDogStatus> { let web3_http = web3::transports::Http::new(eth_jsonrpc_url) - .map_err(|_e| "evm-disconnect".to_string()) + .map_err(|_e| WatchDogStatus::EvmDisconnect) .unwrap(); let web3 = web3::Web3::new(web3_http); @@ -258,17 +252,24 @@ async fn check_eth_account_balance( Ok(syncing) => match syncing { web3::types::SyncState::NotSyncing => { match web3.eth().balance(eth_account, None).await { - Ok(balance) => Ok(Some(balance)), + Ok(eth_balance) => { + let eth_balance: f64 = eth_balance.to_string().parse().unwrap_or_default(); + Ok(Some(eth_balance / 1000000000000000000.0)) + } _ => Ok(None), } } - web3::types::SyncState::Syncing(_) => Err("evm-syncing".to_string()), + web3::types::SyncState::Syncing(_) => Err(WatchDogStatus::EvmSyncing), }, - Err(_e) => Err("evm-errors".to_string()), + Err(e) => { + log::debug!("check_eth_account_balance => {}", e); + + Err(WatchDogStatus::EvmErrors) + } } } -async fn check_wit_connection_status(wit_jsonrpc_socket: &str) -> Result<(), String> { +async fn check_wit_connection_status(wit_jsonrpc_socket: &str) -> Result<(), WatchDogStatus> { let (_handle, wit_client) = TcpSocket::new(wit_jsonrpc_socket).unwrap(); let wit_client = Arc::new(wit_client); let res = wit_client.execute("syncStatus", json!(null)); @@ -277,88 +278,100 @@ async fn check_wit_connection_status(wit_jsonrpc_socket: &str) -> Result<(), Str match res { Ok(Ok(_)) => Ok(()), - Ok(Err(_)) => Err("wit-syncing".to_string()), - Err(_elapse) => Err("wit-disconnect".to_string()), + Ok(Err(_)) => Err(WatchDogStatus::WitSyncing), + Err(_elapse) => Err(WatchDogStatus::WitDisconnect), } } -async fn fetch_wit_account(wit_client: &Addr) -> Result, String> { +async fn fetch_wit_info ( + wit_client: &Addr, + wit_utxos_min_threshold: u64, +) -> Result<(Option, Option, Option), WatchDogStatus> { let req = jsonrpc::Request::method("getPkh").timeout(Duration::from_secs(5)); let res = wit_client.send(req).await; - match res { + let wit_account = match res { Ok(Ok(res)) => match serde_json::from_value::(res) { - Ok(pkh) => Ok(Some(pkh)), - Err(_) => Ok(None), + Ok(pkh) => Some(pkh), + Err(_) => None, }, - Ok(Err(_)) => Ok(None), - Err(_) => Err("wit-errors-getPkh".to_string()), - } -} - -async fn fetch_wit_account_balance( - wit_client: &Addr, - wit_account: &str, -) -> Result, String> { - let req = jsonrpc::Request::method("getBalance") - .timeout(Duration::from_secs(5)) - .params(vec![wit_account, "true"]) - .expect("getBalance wrong params"); - - let res = wit_client.send(req).await; - let res = match res { - Ok(res) => res, - Err(_) => { - return Err("wit-errors-getBalance".to_string()); + Ok(Err(_)) => None, + Err(err) => { + log::debug!("fetch_wit_info => {}", err); + return Err(WatchDogStatus::WitErrors); } }; - match res { - Ok(value) => match value.get("total") { - Some(value) => match value.as_f64() { - Some(value) => Ok(Some(value / 1000000000.0)), - None => Ok(None), - }, - None => Ok(None), - }, - Err(_) => Err("wit-errors-getBalance".to_string()), - } -} - -async fn fetch_wit_account_count_utxos_above( - wit_client: &Addr, - wit_account: &str, - threshold: u64, -) -> Result, String> { - let req = jsonrpc::Request::method("getUtxoInfo") - .timeout(Duration::from_secs(5)) - .params(wit_account) - .expect("getUtxoInfo wrong params"); - - let res = wit_client.send(req).await; - let res = match res { - Ok(res) => res, - Err(_) => { - return Err("wit-errors-getUtxoInfo".to_string()); + let wit_account_balance = match wit_account.clone() { + Some(wit_account) => { + let req = jsonrpc::Request::method("getBalance") + .timeout(Duration::from_secs(5)) + .params(wit_account) + .expect("getBalance wrong params"); + let res = wit_client.send(req).await; + let res = match res { + Ok(res) => res, + Err(err) => { + log::debug!("fetch_wit_info => {}", err); + return Err(WatchDogStatus::WitErrors); + } + }; + match res { + Ok(value) => match value.get("total") { + Some(value) => match value.as_f64() { + Some(value) => Some(value / 1000000000.0), + None => None, + }, + None => None, + }, + Err(err) => { + log::debug!("fetch_wit_info => {}", err); + return Err(WatchDogStatus::WitErrors); + } + } } + None => None, }; - match res { - Ok(utxo_info) => { - if let Some(utxos) = utxo_info["utxos"].as_array() { - let mut counter: u64 = u64::default(); - for utxo in utxos { - if let Some(value) = utxo["value"].as_u64() { - if value >= threshold { - counter += 1; + let wit_utxos_above_threshold = match wit_account.clone() { + Some(wit_account) => { + let req = jsonrpc::Request::method("getUtxoInfo") + .timeout(Duration::from_secs(5)) + .params(wit_account) + .expect("getUtxoInfo wrong params"); + let res = wit_client.send(req).await; + let res = match res { + Ok(res) => res, + Err(err) => { + log::debug!("fetch_wit_info => {}", err); + return Err(WatchDogStatus::WitErrors); + } + }; + match res { + Ok(utxo_info) => { + if let Some(utxos) = utxo_info["utxos"].as_array() { + let mut counter: u64 = u64::default(); + for utxo in utxos { + if let Some(value) = utxo["value"].as_u64() { + if value >= wit_utxos_min_threshold { + counter += 1; + } + } } + + Some(counter) + } else { + None } } - - Ok(Some(counter)) - } else { - Ok(None) + Err(err) => { + log::debug!("fetch_wit_info => {}", err); + return Err(WatchDogStatus::WitErrors); + } } } - Err(_) => Err("wit-errors-getUtxoInfo".to_string()), - } + None => None, + }; + + + Ok((wit_account, wit_account_balance, wit_utxos_above_threshold)) } From a8fadb71d6379076102f4a90d3a13f9d6a790b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Tue, 20 Aug 2024 18:03:53 +0200 Subject: [PATCH 82/83] fix(c-bridge): use wit/rpc client instead of tcp::socket to check wit connection status --- .../src/actors/watch_dog.rs | 168 ++++++++++-------- 1 file changed, 91 insertions(+), 77 deletions(-) diff --git a/bridges/centralized-ethereum/src/actors/watch_dog.rs b/bridges/centralized-ethereum/src/actors/watch_dog.rs index f9cdfe0e8..eb500c6dd 100644 --- a/bridges/centralized-ethereum/src/actors/watch_dog.rs +++ b/bridges/centralized-ethereum/src/actors/watch_dog.rs @@ -3,10 +3,6 @@ use crate::{ config::Config, }; use actix::prelude::*; -use async_jsonrpc_client::{transports::tcp::TcpSocket, Transport}; -use futures_util::compat::Compat01As03; -use serde::{Deserialize, Serialize}; -use serde_json::json; use std::{ sync::Arc, time::{Duration, Instant}, @@ -23,7 +19,9 @@ use witnet_node::utils::stop_system_if_panicking; /// in the DrDatabase #[derive(Default)] pub struct WatchDog { - /// JSON RPC connection to Wit/node + /// JSON WIT/RPC client connection to Wit/node + pub wit_client: Option>, + /// JSON WIT/RPC socket address pub wit_jsonrpc_socket: String, /// Bridge UTXO min value threshold pub wit_utxo_min_value_threshold: u64, @@ -43,11 +41,6 @@ pub struct WatchDog { pub start_wit_balance: Option, } -#[derive(Serialize, Deserialize)] -struct WatchDogOutput { - pub running_secs: u64, -} - impl Drop for WatchDog { fn drop(&mut self) { log::trace!("Dropping WatchDog"); @@ -68,28 +61,31 @@ impl Actor for WatchDog { } } -#[derive(Debug)] +#[derive(Debug, PartialEq)] enum WatchDogStatus { - // Add all of them here - UpAndRunning, EvmDisconnect, - EvmSyncing, EvmErrors, + EvmSyncing, + WitAlmostSynced, + WitErrors, WitDisconnect, WitSyncing, - WitErrors, + WitWaitingConsensus, + UpAndRunning } impl WatchDogStatus { fn to_string(&self) -> String { match self { - WatchDogStatus::UpAndRunning => "up-and-running".to_string(), WatchDogStatus::EvmDisconnect => "evm-disconnect".to_string(), - WatchDogStatus::EvmSyncing => "evm-syncing".to_string(), WatchDogStatus::EvmErrors => format!("evm-errors"), + WatchDogStatus::EvmSyncing => "evm-syncing".to_string(), + WatchDogStatus::WitAlmostSynced => "wit-almost-synced".to_string(), WatchDogStatus::WitDisconnect => "wit-disconnect".to_string(), - WatchDogStatus::WitSyncing => "wit-syncing".to_string(), WatchDogStatus::WitErrors => format!("wit-errors"), + WatchDogStatus::WitSyncing => "wit-syncing".to_string(), + WatchDogStatus::WitWaitingConsensus => "wit-waiting-consensus".to_string(), + WatchDogStatus::UpAndRunning => "up-and-running".to_string(), } } } @@ -102,6 +98,7 @@ impl WatchDog { /// Initialize from config pub fn from_config(config: &Config, eth_contract: Arc>) -> Self { Self { + wit_client: JsonRpcClient::start(config.witnet_jsonrpc_socket.to_string().as_str()).ok(), wit_jsonrpc_socket: config.witnet_jsonrpc_socket.to_string(), wit_utxo_min_value_threshold: config.witnet_utxo_min_value_threshold, eth_account: config.eth_from, @@ -132,6 +129,7 @@ impl WatchDog { } let start_eth_balance = self.start_eth_balance; let start_wit_balance = self.start_wit_balance; + let wit_client = self.wit_client.clone(); let wit_jsonrpc_socket = self.wit_jsonrpc_socket.clone(); let wit_utxo_min_value_threshold = self.wit_utxo_min_value_threshold; let eth_jsonrpc_url = self.eth_jsonrpc_url.clone(); @@ -142,27 +140,58 @@ impl WatchDog { let fut = async move { let mut status = WatchDogStatus::UpAndRunning; - if let Err(err) = check_wit_connection_status(&wit_jsonrpc_socket).await { - status = err; - } - let wit_client = match JsonRpcClient::start(&wit_jsonrpc_socket) { - Ok(client) => client, - Err(_) => return (None, None), - }; - let (wit_account, wit_balance, wit_utxos_above_threshold) = match fetch_wit_info( - &wit_client, - wit_utxo_min_value_threshold - ).await { - Ok((wit_account, wit_balance, wit_utxos_above_threshold)) => { - (wit_account, wit_balance, wit_utxos_above_threshold) + let dr_database = DrDatabase::from_registry(); + let (_, drs_pending, drs_finished, _) = + dr_database.send(CountDrsPerState).await.unwrap().unwrap(); + + let mut metrics: String = "{".to_string(); + metrics.push_str(&format!("\"drsFinished\": {drs_finished}, ")); + metrics.push_str(&format!("\"drsPending\": {drs_pending}, ")); + metrics.push_str(&format!("\"evmAccount\": \"{eth_account}\", ")); + + if let Some(wit_client) = wit_client { + if let Err(err) = check_wit_connection_status(&wit_client).await { + status = err; } - Err(err) => { - if status == WatchDogStatus::UpAndRunning { - status = err; + + let (wit_account, wit_balance, wit_utxos_above_threshold) = match fetch_wit_info( + &wit_client, + wit_utxo_min_value_threshold + ).await { + Ok((wit_account, wit_balance, wit_utxos_above_threshold)) => { + (wit_account, wit_balance, wit_utxos_above_threshold) } - (None, None, None) + Err(err) => { + if status == WatchDogStatus::UpAndRunning { + status = err; + } + (None, None, None) + } + }; + + if wit_account.is_some() { + metrics.push_str(&format!("\"witAccount\": {:?}, ", wit_account.unwrap())); } - }; + if wit_balance.is_some() { + let wit_balance = wit_balance.unwrap(); + metrics.push_str(&format!("\"witBalance\": {:.5}, ", wit_balance)); + if let Some(start_wit_balance) = start_wit_balance { + let wit_hourly_expenditure = + ((start_wit_balance - wit_balance) / running_secs as f64) * 3600_f64; + metrics.push_str(&format!( + "\"witHourlyExpenditure\": {:.1}, ", + wit_hourly_expenditure + )); + } + } + metrics.push_str(&format!("\"witNodeSocket\": \"{wit_jsonrpc_socket}\", ")); + if wit_utxos_above_threshold.is_some() { + metrics.push_str(&format!( + "\"witUtxosAboveThreshold\": {}, ", + wit_utxos_above_threshold.unwrap() + )); + } + } let eth_balance = match check_eth_account_balance(ð_jsonrpc_url, eth_account).await { Ok(eth_balance) => eth_balance, @@ -174,14 +203,6 @@ impl WatchDog { } }; - let dr_database = DrDatabase::from_registry(); - let (_, drs_pending, drs_finished, _) = - dr_database.send(CountDrsPerState).await.unwrap().unwrap(); - - let mut metrics: String = "{".to_string(); - metrics.push_str(&format!("\"drsFinished\": {drs_finished}, ")); - metrics.push_str(&format!("\"drsPending\": {drs_pending}, ")); - metrics.push_str(&format!("\"evmAccount\": \"{eth_account}\", ")); if eth_balance.is_some() { let eth_balance = eth_balance.unwrap(); metrics.push_str(&format!("\"evmBalance\": {:.5}, ", eth_balance)); @@ -195,33 +216,12 @@ impl WatchDog { )); } } - if wit_account.is_some() { - metrics.push_str(&format!("\"witAccount\": {:?}, ", wit_account.unwrap())); - } - if wit_balance.is_some() { - let wit_balance = wit_balance.unwrap(); - metrics.push_str(&format!("\"witBalance\": {:.5}, ", wit_balance)); - if let Some(start_wit_balance) = start_wit_balance { - let wit_hourly_expenditure = - ((start_wit_balance - wit_balance) / running_secs as f64) * 3600_f64; - metrics.push_str(&format!( - "\"witHourlyExpenditure\": {:.1}, ", - wit_hourly_expenditure - )); - } - } - metrics.push_str(&format!("\"witNodeSocket\": \"{}\", ", wit_jsonrpc_socket)); - if wit_utxos_above_threshold.is_some() { - metrics.push_str(&format!( - "\"witUtxosAboveThreshold\": {}, ", - wit_utxos_above_threshold.unwrap() - )); - } + metrics.push_str(&format!("\"runningSecs\": {running_secs}, ")); metrics.push_str(&format!("\"status\": \"{}\"", status.to_string())); metrics.push_str("}}"); log::info!("{metrics}"); - + (eth_balance, wit_balance) }; @@ -269,17 +269,31 @@ async fn check_eth_account_balance( } } -async fn check_wit_connection_status(wit_jsonrpc_socket: &str) -> Result<(), WatchDogStatus> { - let (_handle, wit_client) = TcpSocket::new(wit_jsonrpc_socket).unwrap(); - let wit_client = Arc::new(wit_client); - let res = wit_client.execute("syncStatus", json!(null)); - let res = Compat01As03::new(res); - let res = tokio::time::timeout(Duration::from_secs(5), res).await; - +async fn check_wit_connection_status(wit_client: &Addr) -> Result<(), WatchDogStatus> { + let req = jsonrpc::Request::method("syncStatus").timeout(Duration::from_secs(5)); + let res = wit_client.send(req).await; match res { - Ok(Ok(_)) => Ok(()), - Ok(Err(_)) => Err(WatchDogStatus::WitSyncing), - Err(_elapse) => Err(WatchDogStatus::WitDisconnect), + Ok(Ok(result)) => { + if let Some(node_state) = result["node_state"].as_str() { + match node_state { + "Synced" => Ok(()), + "AlmostSynced" => Err(WatchDogStatus::WitAlmostSynced), + "WaitingConsensus" => Err(WatchDogStatus::WitWaitingConsensus), + _ => Err(WatchDogStatus::WitSyncing) + } + } else { + log::debug!("check_wit_connection_status => unknown node_state"); + Err(WatchDogStatus::WitErrors) + } + } + Ok(Err(err)) => { + log::debug!("check_wit_connection_status => {}", err); + Err(WatchDogStatus::WitDisconnect) + } + Err(err) => { + log::debug!("check_wit_connection_status => {}", err); + Err(WatchDogStatus::WitDisconnect) + } } } From 092ba768457d810383628c047fb2d24b27a5e353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20D=C3=ADaz?= Date: Tue, 20 Aug 2024 18:17:46 +0200 Subject: [PATCH 83/83] chore: fix deps to create::staking::helpers --- data_structures/src/staking/errors.rs | 2 +- data_structures/src/wit.rs | 2 +- node/src/actors/messages.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data_structures/src/staking/errors.rs b/data_structures/src/staking/errors.rs index d0c26600b..03ac013a8 100644 --- a/data_structures/src/staking/errors.rs +++ b/data_structures/src/staking/errors.rs @@ -1,4 +1,4 @@ -use crate::staking::aux::StakeKey; +use crate::staking::helpers::StakeKey; use failure::Fail; use std::{ convert::From, diff --git a/data_structures/src/wit.rs b/data_structures/src/wit.rs index c9974003b..0023df3ea 100644 --- a/data_structures/src/wit.rs +++ b/data_structures/src/wit.rs @@ -2,7 +2,7 @@ use std::{fmt, ops::*}; use serde::{Deserialize, Serialize}; -use crate::{chain::Epoch, staking::aux::Power}; +use crate::{chain::Epoch, staking::helpers::Power}; /// 1 nanowit is the minimal unit of value /// 1 wit = 10^9 nanowits diff --git a/node/src/actors/messages.rs b/node/src/actors/messages.rs index 3b31b0ee7..efb18b360 100644 --- a/node/src/actors/messages.rs +++ b/node/src/actors/messages.rs @@ -27,7 +27,7 @@ use witnet_data_structures::{ }, fee::{deserialize_fee_backwards_compatible, Fee}, radon_report::RadonReport, - staking::{aux::StakeKey, stakes::QueryStakesKey}, + staking::{helpers::StakeKey, stakes::QueryStakesKey}, transaction::{ CommitTransaction, DRTransaction, RevealTransaction, StakeTransaction, Transaction, VTTransaction,