diff --git a/Cargo.lock b/Cargo.lock index 8d309b31e..4ecf42300 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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" + [[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" @@ -723,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", @@ -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" @@ -1722,6 +1754,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" @@ -2328,7 +2369,7 @@ dependencies = [ "num-complex", "num-integer", "num-iter", - "num-rational", + "num-rational 0.4.1", "num-traits", ] @@ -2388,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" @@ -2989,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" @@ -3487,9 +3549,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "secp256k1" -version = "0.22.2" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295642060261c80709ac034f52fca8e5a9fa2c7d341ded5cdb164b7c33768b2a" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ "secp256k1-sys", "serde", @@ -3497,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", ] @@ -3873,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.78", + "quote 1.0.35", + "rustversion", + "syn 2.0.48", +] + [[package]] name = "subtle" version = "1.0.0" @@ -5054,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", ] @@ -5091,11 +5172,12 @@ dependencies = [ "failure", "futures 0.3.30", "hex", - "itertools", + "itertools 0.8.2", "lazy_static", "log 0.4.20", "num-format", "prettytable-rs", + "qrcode", "sentry", "serde", "serde_json", @@ -5207,7 +5289,7 @@ dependencies = [ "failure", "futures 0.3.30", "hex", - "itertools", + "itertools 0.8.2", "lazy_static", "log 0.4.20", "num-traits", @@ -5221,6 +5303,8 @@ dependencies = [ "serde", "serde_cbor", "serde_json", + "strum", + "strum_macros", "vrf", "witnet_crypto", "witnet_protected", @@ -5273,7 +5357,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 +5480,7 @@ dependencies = [ "bencher", "failure", "hex", - "itertools", + "itertools 0.11.0", "log 0.4.20", "url", "witnet_config", @@ -5420,7 +5504,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/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/config/src/config.rs b/config/src/config.rs index 72e68f2d6..d18e5e8cf 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(ty = "Protocol")] + #[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 f90390484..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 @@ -486,6 +495,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/crypto/Cargo.toml b/crypto/Cargo.toml index a1831753a..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"] } +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/Cargo.toml b/data_structures/Cargo.toml index 264649bcd..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" } @@ -51,3 +53,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..d2d96edc3 --- /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.as_str(), 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.as_str(), 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.as_str(), coins, epoch).unwrap(); + + i += 1; + + if i == stakers { + break; + } + } + + i = 1; + + b.iter(|| { + let address = format!("{i}"); + let _power = stakes.query_power(address.as_str(), Capability::Mining, i); + + i += 1; + }) +} + +benchmark_main!(benches); +benchmark_group!(benches, populate, rank, query_power); 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/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/capabilities.rs b/data_structures/src/capabilities.rs new file mode 100644 index 000000000..c3bee6efb --- /dev/null +++ b/data_structures/src/capabilities.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; + +#[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, Deserialize, PartialEq, Serialize)] +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/chain/mod.rs b/data_structures/src/chain/mod.rs index 8f905f885..95c75c811 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, @@ -17,18 +11,21 @@ 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; -use partial_struct::PartialStruct; use serde::{Deserialize, Serialize}; + +use partial_struct::PartialStruct; use witnet_crypto::{ hash::{calculate_sha256, Sha256}, key::ExtendedSK, merkle::merkle_tree_root as crypto_merkle_tree_root, secp256k1::{ - ecdsa::Signature as Secp256k1_Signature, PublicKey as Secp256k1_PublicKey, - SecretKey as Secp256k1_SecretKey, + self, + ecdsa::{RecoverableSignature, RecoveryId, Signature as Secp256k1_Signature}, + Message, PublicKey as Secp256k1_PublicKey, SecretKey as Secp256k1_SecretKey, }, }; use witnet_protected::Protected; @@ -42,19 +39,31 @@ use crate::{ TransactionError, }, get_environment, - proto::{schema::witnet, ProtobufConvert}, + proto::{ + versioning::{ProtocolVersion, Versioned}, + ProtobufConvert, + }, + staking::prelude::*, superblock::SuperBlockState, transaction::{ CommitTransaction, DRTransaction, DRTransactionBody, Memoized, MintTransaction, - RevealTransaction, TallyTransaction, Transaction, TxInclusionProof, VTTransaction, + RevealTransaction, StakeTransaction, TallyTransaction, Transaction, TxInclusionProof, + UnstakeTransaction, VTTransaction, }, transaction::{ MemoHash, MemoizedHashable, BETA, COMMIT_WEIGHT, OUTPUT_SIZE, REVEAL_WEIGHT, TALLY_WEIGHT, }, utxo_pool::{OldUnspentOutputsPool, OwnUnspentOutputsPool, UnspentOutputsPool}, vrf::{BlockEligibilityClaim, DataRequestEligibilityClaim}, + wit::Wit, }; +/// 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` @@ -156,7 +165,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, @@ -359,7 +368,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 @@ -372,7 +381,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 @@ -386,7 +395,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, @@ -402,7 +411,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, @@ -416,6 +425,10 @@ 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, + /// A list of signed unstake transactions + pub unstake_txns: Vec, } impl Block { @@ -444,6 +457,8 @@ impl Block { commit_txns: vec![], reveal_txns: vec![], tally_txns: vec![], + stake_txns: vec![], + unstake_txns: vec![], }; /// Function to calculate a merkle tree from a transaction vector @@ -468,6 +483,8 @@ 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), + unstake_hash_merkle_root: merkle_tree_root(&txns.unstake_txns), }; Block::new( @@ -502,8 +519,28 @@ 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 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.dr_weight() + self.vt_weight() + self.st_weight() + self.ut_weight() + } + + pub fn is_genesis(&self, genesis: &Hash) -> bool { + self.hash().eq(genesis) } } @@ -517,6 +554,8 @@ impl BlockTransactions { + self.commit_txns.len() + self.reveal_txns.len() + self.tally_txns.len() + + self.stake_txns.len() + + self.unstake_txns.len() } /// Returns true if this block contains no transactions @@ -528,6 +567,8 @@ impl BlockTransactions { && self.commit_txns.is_empty() && 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` @@ -559,6 +600,16 @@ impl BlockTransactions { .get(i as usize) .cloned() .map(Transaction::Tally), + TransactionPointer::Stake(i) => self + .stake_txns + .get(i as usize) + .cloned() + .map(Transaction::Stake), + TransactionPointer::Unstake(i) => self + .unstake_txns + .get(i as usize) + .cloned() + .map(Transaction::Unstake), } } @@ -601,6 +652,16 @@ 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())); + } + 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 } @@ -614,7 +675,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(ProtocolVersion::guess()) + .unwrap() } fn memoized_hash(&self) -> &MemoHash { @@ -652,7 +715,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. @@ -668,7 +731,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, @@ -682,6 +745,10 @@ 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, + /// 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 @@ -710,6 +777,8 @@ 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), + unstake_hash_merkle_root: merkle_tree_root(&txns.unstake_txns), } } } @@ -720,7 +789,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, @@ -853,7 +922,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, @@ -911,7 +980,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), @@ -943,11 +1012,30 @@ 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 #[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, @@ -1039,7 +1127,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), @@ -1047,7 +1135,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, + ]) } } @@ -1118,8 +1209,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); @@ -1129,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 @@ -1169,11 +1268,20 @@ 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], } +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() @@ -1307,7 +1415,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, } @@ -1329,7 +1437,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, @@ -1341,9 +1449,21 @@ 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")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::DataRequestOutput")] pub struct DataRequestOutput { /// Data request structure pub data_request: RADRequest, @@ -1407,6 +1527,45 @@ impl DataRequestOutput { } } +#[derive(Debug, Default, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, ProtobufConvert)] +#[protobuf_convert(pb = "crate::proto::schema::witnet::StakeOutput")] +pub struct StakeOutput { + pub authorization: KeyedSignature, + pub key: StakeKey, + pub value: u64, +} + +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 { @@ -1436,7 +1595,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, @@ -1444,6 +1603,78 @@ pub struct KeyedSignature { pub public_key: PublicKey, } +impl KeyedSignature { + 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], + ) -> 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) + .map_err(|e| Secp256k1ConversionError::Secp256k1 { inner: e })?; + + 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], + ) -> 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], + ) -> Result<[u8; 65], Secp256k1ConversionError> { + let mut recoverable_bytes = [0; 65]; + 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)?; + + if recovered.public_key == self.public_key { + break; + } + } + + Ok(recoverable_bytes) + } +} + /// Public Key data structure #[derive(Debug, Default, Eq, PartialEq, Clone, Hash, Serialize, Deserialize)] pub struct PublicKey { @@ -1491,6 +1722,14 @@ impl PublicKey { } } +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 { @@ -1511,7 +1750,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, @@ -1530,7 +1769,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, @@ -1538,7 +1777,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, @@ -1655,7 +1894,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 @@ -1689,7 +1931,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 { @@ -1866,7 +2108,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, @@ -1887,7 +2132,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 { @@ -1913,7 +2158,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, @@ -1947,6 +2195,8 @@ impl From for RADTally { 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 { @@ -2003,6 +2253,10 @@ 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, + // 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 @@ -2023,6 +2277,19 @@ 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, + 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 { @@ -2039,17 +2306,27 @@ impl Default for TransactionsPool { output_pointer_map: Default::default(), 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 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, + // 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 required_reward_collateral_ratio: u64::MAX, unconfirmed_transactions: Default::default(), + st_transactions: Default::default(), + sorted_st_index: Default::default(), + ut_transactions: Default::default(), + sorted_ut_index: Default::default(), } } } @@ -2082,7 +2359,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 +2399,8 @@ impl TransactionsPool { && self.dr_transactions.is_empty() && 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. @@ -2138,12 +2417,20 @@ impl TransactionsPool { output_pointer_map, 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(); @@ -2157,7 +2444,13 @@ impl TransactionsPool { output_pointer_map.clear(); *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. @@ -2202,6 +2495,48 @@ 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() + } + + /// 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(); @@ -2243,6 +2578,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(_st) => Ok(self.st_contains(&tx_hash)), + Transaction::Unstake(_ut) => Ok(self.ut_contains(&tx_hash)), } } @@ -2335,6 +2672,53 @@ 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) + } + + /// 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. @@ -2469,6 +2853,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 +2929,73 @@ 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 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 { + 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 + }) + } + + /// 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) { @@ -2610,11 +3062,11 @@ 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 +3189,47 @@ 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)); + } + } + 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: {:?}", @@ -2785,6 +3278,24 @@ 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)) + } + + /// 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: @@ -2803,6 +3314,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 +3348,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, @@ -2878,6 +3391,16 @@ impl TransactionsPool { .get(hash) .map(|rt| Transaction::Reveal(rt.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())) + }) } /// Update unconfirmed transactions @@ -2902,6 +3425,12 @@ impl TransactionsPool { Transaction::DataRequest(_) => { let _x = self.dr_remove_inner(&hash, false); } + Transaction::Stake(_) => { + let _x = self.st_remove_inner(&hash, false); + } + Transaction::Unstake(_) => { + let _x = self.ut_remove_inner(&hash); + } _ => continue, } @@ -2915,12 +3444,16 @@ 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) /// 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, @@ -2983,7 +3516,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), @@ -3008,6 +3541,10 @@ pub enum TransactionPointer { Tally(u32), /// Mint Mint, + // Stake + Stake(u32), + // Unstake + Unstake(u32), } /// This is how transactions are stored in the database: hash of the containing block, plus index @@ -3271,6 +3808,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 { @@ -4021,7 +4560,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, )) } @@ -4052,7 +4591,8 @@ mod tests { }; use crate::{ - superblock::{mining_build_superblock, ARSIdentities}, + proto::versioning::{ProtocolVersion, VersionedHashable}, + superblock::{mining_build_superblock, ValidatorIdentities}, transaction::{CommitTransactionBody, RevealTransactionBody, VTTransactionBody}, }; @@ -4125,7 +4665,7 @@ mod tests { .iter() .map(|input| { DRTransaction::new( - DRTransactionBody::new(vec![*input], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![*input], DataRequestOutput::default(), vec![]), vec![], ) }) @@ -4160,7 +4700,20 @@ 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::V1_7).to_string(), + expected + ); + let expected = "29ef68357a5c861b9dbe043d351a28472ca450edcda25de4c9b80a4560a28c0f"; + assert_eq!( + block.versioned_hash(ProtocolVersion::V1_8).to_string(), + expected + ); + let expected = "29ef68357a5c861b9dbe043d351a28472ca450edcda25de4c9b80a4560a28c0f"; + assert_eq!( + block.versioned_hash(ProtocolVersion::V2_0).to_string(), + expected + ); } #[test] @@ -4433,7 +4986,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); @@ -4454,7 +5007,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); @@ -4599,14 +5152,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![], ); @@ -4676,14 +5229,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![], ); @@ -4777,11 +5330,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![], ); @@ -4843,7 +5396,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 @@ -5038,12 +5591,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![], )) @@ -5281,7 +5834,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 { @@ -5319,7 +5872,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 { @@ -5334,7 +5887,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 { @@ -5349,7 +5902,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 { @@ -5441,7 +5994,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 { @@ -5481,7 +6034,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 { @@ -5520,7 +6073,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 { @@ -5558,7 +6111,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 { @@ -5596,7 +6149,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 { @@ -5920,11 +6473,11 @@ mod tests { let expected_order = vec![p1_bls, p2_bls, p3_bls]; let ordered_identities = rep_engine.get_rep_ordered_ars_list(); - let ars_identities = ARSIdentities::new(ordered_identities); + let validator_identities = ValidatorIdentities::new(ordered_identities); assert_eq!( expected_order, - ars_identities.get_rep_ordered_bn256_list(&alt_keys) + validator_identities.get_ordered_bn256_list(&alt_keys) ); } @@ -5962,11 +6515,11 @@ mod tests { let expected_order = vec![p1_bls, p2_bls, p3_bls]; let ordered_identities = rep_engine.get_rep_ordered_ars_list(); - let ars_identities = ARSIdentities::new(ordered_identities); + let validator_identities = ValidatorIdentities::new(ordered_identities); assert_eq!( expected_order, - ars_identities.get_rep_ordered_bn256_list(&alt_keys) + validator_identities.get_ordered_bn256_list(&alt_keys) ); } @@ -6020,11 +6573,11 @@ mod tests { let expected_order = vec![p1_bls, p2_bls, p4_bls, p5_bls, p3_bls]; let ordered_identities = rep_engine.get_rep_ordered_ars_list(); - let ars_identities = ARSIdentities::new(ordered_identities); + let validator_identities = ValidatorIdentities::new(ordered_identities); assert_eq!( expected_order, - ars_identities.get_rep_ordered_bn256_list(&alt_keys) + validator_identities.get_ordered_bn256_list(&alt_keys) ); } diff --git a/data_structures/src/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/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/error.rs b/data_structures/src/error.rs index 18e807352..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, @@ -288,6 +290,40 @@ 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 ({})", + stake, min_stake + )] + StakeBelowMinimum { min_stake: u64, stake: u64 }, + /// Unstaking more than the total staked + #[fail( + display = "Tried to unstake more coins than the current stake ({} > {})", + 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 @@ -411,6 +447,24 @@ 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 }, + /// 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: ({}) ", + pkh + )] + RepeatedStakeOperator { pkh: PublicKeyHash }, /// Missing expected tallies #[fail( display = "{} expected tally transactions are missing in block candidate {}", @@ -432,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`" @@ -451,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 0b83c5cc9..67365e300 100644 --- a/data_structures/src/lib.rs +++ b/data_structures/src/lib.rs @@ -13,10 +13,16 @@ #[macro_use] extern crate protobuf_convert; -use crate::chain::Environment; -use lazy_static::lazy_static; use std::sync::RwLock; +use lazy_static::lazy_static; + +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; @@ -38,6 +44,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; @@ -69,6 +78,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. @@ -76,6 +88,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: RwLock = RwLock::new(ProtocolInfo::default()); } /// Environment in which we are running: mainnet or testnet. @@ -108,6 +123,44 @@ pub fn set_environment(environment: Environment) { } } +/// Protocol version that we are running. +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. + let protocol_info = PROTOCOL.read().unwrap(); + + if let Some(epoch) = epoch { + protocol_info.all_versions.version_for_epoch(epoch) + } else { + protocol_info.current_version + } +} + +/// 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(); + protocol_info.register(epoch, protocol_version); +} + +/// Set the protocol version that we are running. +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::*; @@ -118,4 +171,38 @@ mod tests { // addresses serialized as Bech32 will fail assert_eq!(get_environment(), Environment::Mainnet); } + + #[test] + 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 + let version = get_protocol_version(None); + assert_eq!(version, ProtocolVersion::V1_7); + + // Register the different protocol versions + 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)); + 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/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/proto/versioning.rs b/data_structures/src/proto/versioning.rs new file mode 100644 index 000000000..244c52037 --- /dev/null +++ b/data_structures/src/proto/versioning.rs @@ -0,0 +1,561 @@ +use failure::{Error, Fail}; +use protobuf::Message as _; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; +use strum_macros::{Display, EnumString}; + +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, + 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, Debug, Default)] +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) + .copied() + .unwrap_or_default() + } +} + +#[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 + #[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) + } +} + +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()?) + } + + /// 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_7 => Box::new(Self::LegacyType::from(pb)), + // Transition merkle roots need no transformation + V1_8 => 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_7 => Box::new(Self::LegacyType::from(pb)), + // All other block headers need no transformation + V1_8 | 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_7 => Box::new(Self::LegacyType::from(pb)), + V1_8 | 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 { + // 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() + } +} + +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() + } +} diff --git a/data_structures/src/staking/aux.rs b/data_structures/src/staking/aux.rs new file mode 100644 index 000000000..1ebb5c28c --- /dev/null +++ b/data_structures/src/staking/aux.rs @@ -0,0 +1,139 @@ +use std::fmt::{Debug, Display, Formatter}; +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>; + +/// 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, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub struct StakeKey
{ + /// A validator address. + pub validator: Address, + /// A withdrawer address. + pub withdrawer: Address, +} + +impl ProtobufConvert for StakeKey { + type ProtoStruct = crate::proto::schema::witnet::StakeKey; + + fn to_pb(&self) -> Self::ProtoStruct { + let mut proto = Self::ProtoStruct::new(); + proto.set_validator(self.validator.to_pb()); + proto.set_withdrawer(self.withdrawer.to_pb()); + + proto + } + + 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, + }) + } +} + +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(), + } + } +} + +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)] +pub struct CoinsAndAddresses { + /// An amount of coins. + pub coins: Coins, + /// A validator and withdrawer addresses pair. + pub addresses: StakeKey
, +} + +/// 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..869536de3 --- /dev/null +++ b/data_structures/src/staking/errors.rs @@ -0,0 +1,105 @@ +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, 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, + /// The minimum stakeable amount. + 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, + /// The latest Epoch. + 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, + /// The maximum Epoch. + 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 +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/mod.rs b/data_structures/src/staking/mod.rs new file mode 100644 index 000000000..1a5b21418 --- /dev/null +++ b/data_structures/src/staking/mod.rs @@ -0,0 +1,107 @@ +#![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)] +pub mod test { + use super::prelude::*; + + #[test] + 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/stake.rs b/data_structures/src/staking/stake.rs new file mode 100644 index 000000000..ea1926da9 --- /dev/null +++ b/data_structures/src/staking/stake.rs @@ -0,0 +1,138 @@ +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. +#[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +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 + Debug + Display + Sync + Send, + Coins: Copy + + From + + PartialOrd + + num_traits::Zero + + Add + + Sub + + Mul + + Mul + + Debug + + Display + + Send + + Sync, + Epoch: Copy + + Default + + num_traits::Saturating + + Sub + + From + + Debug + + Display + + Sync + + Send, + Power: Add + Div, + u64: From + From, +{ + /// 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, + ) -> 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 { + 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 = Epoch::from( + (u64::from(product_before + product_added) / u64::from(coins_after)) as u32, + ); + + 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, + ) -> StakingResult { + 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..8d10c27ab --- /dev/null +++ b/data_structures/src/staking/stakes.rs @@ -0,0 +1,800 @@ +use std::{ + cmp::PartialOrd, + collections::{btree_map::Entry, BTreeMap, HashSet}, + fmt::{Debug, Display}, + ops::{Add, Div, Mul, Sub}, +}; + +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::{chain::PublicKeyHash, get_environment, transaction::StakeTransaction, wit::Wit}; + +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 +/// counted and write-locked, we can have as many indexes here as we need at a negligible cost. +#[derive(Clone, Debug, Deserialize, Default, PartialEq, Serialize)] +pub struct Stakes +where + Address: Default + Ord, + Coins: Ord, + Epoch: Default, +{ + /// 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 + /// 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, + /// A listing of all active validators (mined a block or solved a data request) indexed by the validator address + /// and the epoch of when it was active as value + by_active: BTreeMap, +} + +impl Stakes +where + Address: Default + Send + Sync + Display, + Coins: Copy + + Default + + Ord + + From + + Into + + num_traits::Zero + + Add + + Sub + + Mul + + Mul + + Debug + + Send + + Sync + + Display, + Address: Clone + Ord + 'static + Debug, + Epoch: Copy + + Default + + num_traits::Saturating + + Sub + + From + + Debug + + Display + + Send + + Sync + + PartialOrd, + 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( + &mut self, + key: ISK, + coins: Coins, + epoch: Epoch, + ) -> StakingResult, Address, Coins, Epoch> + where + ISK: Into>, + { + let key = key.into(); + + // Find or create a matching stake entry + let stake = self.by_key.entry(key.clone()).or_default(); + + // Actually increase the number of coins + stake + .value + .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 coins_and_addresses = CoinsAndAddresses { + coins, + addresses: key, + }; + 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()) + } + + /// 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, + key: ISK, + capability: Capability, + epoch: Epoch, + ) -> StakingResult + where + ISK: Into>, + { + let key = key.into(); + + Ok(self + .by_key + .get(&key) + .ok_or(StakesError::EntryNotFound { key })? + .value + .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, Power)> + '_ { + self.by_coins + .iter() + .flat_map(move |(CoinsAndAddresses { addresses, .. }, stake)| { + stake + .value + .read() + .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( + &mut self, + key: ISK, + coins: Coins, + ) -> StakingResult + where + ISK: Into>, + { + 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().value.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_active.remove(&key.validator); + self.by_coins.remove(&CoinsAndAddresses { + coins: initial_coins, + addresses: key, + }); + } + + Ok(final_coins) + } else { + 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( + &mut self, + key: ISK, + capability: Capability, + current_epoch: Epoch, + ) -> StakingResult<(), Address, Coins, Epoch> + where + ISK: Into>, + { + let key = key.into(); + + let mut stake = self + .by_key + .get_mut(&key) + .ok_or(StakesError::EntryNotFound { key })? + .value + .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() + } + } + + /// 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), + } + } + + /// Return the total number of validators. + pub fn validator_count(&self, active_since: Option) -> usize { + match active_since { + None => self.by_active.len(), + Some(epoch) => { + let mut count = 0; + for (_, active) in self.by_active.iter() { + if *active >= epoch { + count += 1; + } + } + count + } + } + } + + /// Get a vector of all validator that were active since a given epoch + pub fn get_active_validators(&self, active_since: Epoch) -> Vec
{ + self.by_active + .iter() + .filter(|(_, epoch)| **epoch >= active_since) + .map(|(address, _)| address.clone()) + .collect() + } + + /// Update the active epoch of the validators passed as an argument + pub fn update_active_epoch(&mut self, validators: HashSet
, active: Epoch) { + for validator in validators { + self.by_active + .entry(validator) + .and_modify(|validator| *validator = active) + .or_insert(active); + } + } + + /// 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. +/// +/// 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 + + Debug + + Display + + Send + + Sync + + PartialOrd, + Power: Add + Copy + Default + Div + Ord + Debug, + 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); + + 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(()) +} + +/// 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 + + Debug + + Send + + Sync + + Display + + PartialOrd, + Power: Add + Copy + Default + Div + Ord + Debug, + Wit: Mul, + u64: From + From, +{ + for transaction in transactions { + process_stake_transaction(stakes, transaction, epoch)?; + } + + Ok(()) +} + +#[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"; + 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_charlie, Capability::Mining, 0), + Err(StakesError::EntryNotFound { + key: StakeKey { + validator: alice.into(), + withdrawer: charlie.into() + }, + }) + ); + assert_eq!( + 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_charlie, 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_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_charlie, 50, 300).unwrap(), + Stake::from_parts( + 150, + CapabilityMap { + mining: 166, + witnessing: 166 + } + ) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 299), + Ok(19_950) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 300), + Ok(20_100) + ); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 301), + Ok(20_250) + ); + assert_eq!( + 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_david, 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_charlie, Capability::Mining, 999), + Ok(124950) + ); + 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_charlie, Capability::Mining, 1_000), + Ok(125100) + ); + 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_charlie, Capability::Mining, 1_001), + Ok(125250) + ); + 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_charlie, Capability::Mining, 2_000), + Ok(275_100) + ); + assert_eq!( + stakes.query_power(bob_david, 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"; + 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's really start our test at epoch 100 + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 100), + Ok(1_000) + ); + assert_eq!( + 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_charlie, Capability::Witnessing, 100), + Ok(1_000) + ); + assert_eq!( + stakes.query_power(bob_david, Capability::Witnessing, 100), + Ok(1_600) + ); + assert_eq!( + stakes.query_power(charlie_erin, Capability::Witnessing, 100), + Ok(2_100) + ); + assert_eq!( + stakes.rank(Capability::Mining, 100).collect::>(), + [ + (charlie_erin.into(), 2100), + (bob_david.into(), 1600), + (alice_charlie.into(), 1000) + ] + ); + assert_eq!( + stakes.rank(Capability::Witnessing, 100).collect::>(), + [ + (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_erin, Capability::Mining, 101) + .unwrap(); + assert_eq!( + stakes.query_power(alice_charlie, Capability::Mining, 101), + Ok(1_010) + ); + assert_eq!( + 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_david, Capability::Witnessing, 101), + Ok(1_620) + ); + assert_eq!( + stakes.query_power(charlie_erin, Capability::Witnessing, 101), + Ok(2_130) + ); + assert_eq!( + stakes.rank(Capability::Mining, 101).collect::>(), + [ + (bob_david.into(), 1_620), + (alice_charlie.into(), 1_010), + (charlie_erin.into(), 0) + ] + ); + assert_eq!( + stakes.rank(Capability::Witnessing, 101).collect::>(), + [ + (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_charlie, Capability::Mining, 300), + Ok(3_000) + ); + assert_eq!( + 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_charlie, Capability::Witnessing, 300), + Ok(3_000) + ); + assert_eq!( + stakes.query_power(bob_david, Capability::Witnessing, 300), + Ok(5_600) + ); + assert_eq!( + stakes.query_power(charlie_erin, Capability::Witnessing, 300), + Ok(8_100) + ); + assert_eq!( + stakes.rank(Capability::Mining, 300).collect::>(), + [ + (charlie_erin.into(), 5_970), + (bob_david.into(), 5_600), + (alice_charlie.into(), 3_000) + ] + ); + assert_eq!( + stakes.rank(Capability::Witnessing, 300).collect::>(), + [ + (charlie_erin.into(), 8_100), + (bob_david.into(), 5_600), + (alice_charlie.into(), 3_000) + ] + ); + } + + #[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/data_structures/src/superblock.rs b/data_structures/src/superblock.rs index c627f84f7..94e22a3f2 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}, @@ -18,6 +10,16 @@ use witnet_crypto::{ 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, + proto::versioning::{ProtocolVersion, VersionedHashable}, +}; + /// Possible result of SuperBlockState::add_vote #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum AddSuperBlockVote { @@ -50,16 +52,16 @@ pub enum SuperBlockConsensus { Unknown, } -/// ARS identities +/// Validator identities #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -pub struct ARSIdentities { - // HashSet of the identities in a specific ARS +pub struct ValidatorIdentities { + // HashSet of the validator identities at a specific block height identities: HashSet, - // Ordered vector of the identities in a specific ARS + // Ordered vector of the most recently active validator identities at a specific block height ordered_identities: Vec, } -impl ARSIdentities { +impl ValidatorIdentities { pub fn len(&self) -> usize { self.identities.len() } @@ -69,13 +71,13 @@ impl ARSIdentities { } pub fn new(ordered_identities: Vec) -> Self { - ARSIdentities { + ValidatorIdentities { identities: ordered_identities.iter().cloned().collect(), ordered_identities, } } - pub fn get_rep_ordered_bn256_list(&self, alt_keys: &AltKeys) -> Vec { + pub fn get_ordered_bn256_list(&self, alt_keys: &AltKeys) -> Vec { self.ordered_identities .iter() .filter_map(|pkh| alt_keys.get_bn256(pkh).cloned()) @@ -223,10 +225,10 @@ impl SuperBlockVotesMempool { /// State related to superblocks #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] pub struct SuperBlockState { - // Structure of the current Active Reputation Set identities - ars_current_identities: ARSIdentities, - // Structure of the previous Active Reputation Set identities - ars_previous_identities: ARSIdentities, + // Structure of the current set of validator identities + validator_current_identities: ValidatorIdentities, + // Structure of the previous set of validator identities + validator_previous_identities: ValidatorIdentities, /// The most recently created superblock. This one is yet to be voted and decided upon. current_superblock: Option, // Current superblock beacon including the superblock hash created by this node @@ -244,8 +246,8 @@ impl SuperBlockState { pub fn new(superblock_genesis_hash: Hash, bootstrap_committee: Vec) -> Self { Self { signing_committee: bootstrap_committee.clone().into_iter().collect(), - ars_previous_identities: ARSIdentities::new(bootstrap_committee.clone()), - ars_current_identities: ARSIdentities::new(bootstrap_committee), + validator_previous_identities: ValidatorIdentities::new(bootstrap_committee.clone()), + validator_current_identities: ValidatorIdentities::new(bootstrap_committee), current_superblock_beacon: CheckpointBeacon { checkpoint: 0, hash_prev_block: superblock_genesis_hash, @@ -268,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 @@ -300,7 +306,7 @@ impl SuperBlockState { Some(true) => self.insert_vote(sbv.clone()), Some(false) => { if sbv.superblock_index == current_superblock_index - || self.ars_previous_identities.is_empty() + || self.validator_previous_identities.is_empty() { AddSuperBlockVote::NotInSigningCommittee } else { @@ -396,13 +402,13 @@ impl SuperBlockState { } } - fn update_ars_identities(&mut self, new_identities: ARSIdentities) { - self.ars_previous_identities = std::mem::take(&mut self.ars_current_identities); - self.ars_current_identities = new_identities; + fn update_validator_identities(&mut self, new_identities: ValidatorIdentities) { + self.validator_previous_identities = std::mem::take(&mut self.validator_current_identities); + self.validator_current_identities = new_identities; } /// Produces a `SuperBlock` that includes the blocks in `block_headers` if there is at least one of them. - /// `ars_identities` will be used to validate all the superblock votes received for the + /// `validator_identities` will be used to validate all the superblock votes received for the /// next superblock. The votes for the current superblock must be validated using them /// to calculate the superblock_signing_committee. /// The ordered bn256 keys will be merkelized and appended to the superblock @@ -410,7 +416,7 @@ impl SuperBlockState { pub fn build_superblock( &mut self, block_headers: &[BlockHeader], - ars_identities: ARSIdentities, + validator_identities: ValidatorIdentities, signing_committee_size: u32, superblock_index: u32, last_block_in_previous_superblock: Hash, @@ -418,9 +424,9 @@ impl SuperBlockState { sync_superblock: Option, block_epoch: Epoch, ) -> SuperBlock { - let key_leaves = hash_key_leaves(&ars_identities.get_rep_ordered_bn256_list(alt_keys)); + let key_leaves = hash_key_leaves(&validator_identities.get_ordered_bn256_list(alt_keys)); - self.update_ars_identities(ars_identities); + self.update_validator_identities(validator_identities); // During synchronization we use the superblock received as consensus by our outbounds // to have the right value of the signing committee size. From now on, we have all the values @@ -428,7 +434,7 @@ impl SuperBlockState { let superblock = if let Some(sb) = sync_superblock { // Before updating the superblock_beacon, calculate the signing committee let signing_committee = calculate_superblock_signing_committee( - self.ars_previous_identities.clone(), + self.validator_previous_identities.clone(), sb.signing_committee_length, superblock_index, self.current_superblock_beacon.hash_prev_block, @@ -447,7 +453,7 @@ impl SuperBlockState { } else { // Before updating the superblock_beacon, calculate the signing committee let signing_committee = calculate_superblock_signing_committee( - self.ars_previous_identities.clone(), + self.validator_previous_identities.clone(), signing_committee_size, superblock_index, self.current_superblock_beacon.hash_prev_block, @@ -556,18 +562,18 @@ impl SuperBlockState { /// Calculates the superblock signing committee for a given superblock hash and ars #[allow(clippy::cast_possible_truncation)] pub fn calculate_superblock_signing_committee( - ars_identities: ARSIdentities, + validator_identities: ValidatorIdentities, signing_committee_size: u32, current_superblock_index: u32, superblock_hash: Hash, block_epoch: Epoch, ) -> HashSet { // If the number of identities is lower than committee_size all the members of the ARS sign the superblock - if ars_identities.len() < usize::try_from(signing_committee_size).unwrap() { - ars_identities.identities + if validator_identities.len() < usize::try_from(signing_committee_size).unwrap() { + validator_identities.identities } else if after_second_hard_fork(block_epoch, get_environment()) { // Start counting the members of the subset from: - // sha256(superblock_hash || superblock_index) % ars_identities.len() + // sha256(superblock_hash || superblock_index) % validator_identities.len() let superblock_hash_and_index_bytes = [ superblock_hash.as_ref(), current_superblock_index.to_be_bytes().as_ref(), @@ -576,12 +582,12 @@ pub fn calculate_superblock_signing_committee( let superblock_hash_and_index_bytes_hashed = Hash::from(calculate_sha256(&superblock_hash_and_index_bytes)); let first = superblock_hash_and_index_bytes_hashed - .div_mod(ars_identities.len() as u64) + .div_mod(validator_identities.len() as u64) .1 as usize; // Get the subset let subset = magic_partition_2( - &ars_identities.ordered_identities, + &validator_identities.ordered_identities, first, signing_committee_size.try_into().unwrap(), superblock_hash_and_index_bytes_hashed.as_ref(), @@ -597,11 +603,11 @@ pub fn calculate_superblock_signing_committee( // Start counting the members of the subset from the superblock_hash plus superblock index hash let mut first = u32::from(first_byte_sb_hash) + u32::from(first_byte_index_hash); // We need to choose a first member from all the potential ARS members - first %= ars_identities.len() as u32; + first %= validator_identities.len() as u32; // Get the subset let subset = magic_partition( - &ars_identities.ordered_identities.to_vec(), + &validator_identities.ordered_identities.to_vec(), first.try_into().unwrap(), signing_committee_size.try_into().unwrap(), ); @@ -698,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) @@ -754,13 +760,17 @@ 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() { @@ -806,6 +816,8 @@ 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, + unstake_hash_merkle_root: default_hash, }, proof: default_proof, bn256_public_key: None, @@ -816,7 +828,7 @@ mod tests { default_hash, dr_merkle_root_1, 0, - block.hash(), + block.versioned_hash(ProtocolVersion::V1_7), default_hash, tally_merkle_root_1, ); @@ -855,6 +867,8 @@ 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, + unstake_hash_merkle_root: default_hash, }, proof: default_proof.clone(), bn256_public_key: None, @@ -870,6 +884,8 @@ 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, + unstake_hash_merkle_root: default_hash, }, proof: default_proof, bn256_public_key: None, @@ -880,7 +896,7 @@ mod tests { default_hash, expected_superblock_dr_root, 0, - block_2.hash(), + block_2.versioned_hash(ProtocolVersion::V1_7), default_hash, expected_superblock_tally_root, ); @@ -940,7 +956,7 @@ mod tests { let sb1 = sbs.build_superblock( &block_headers, - ARSIdentities::new(ars2), + ValidatorIdentities::new(ars2), 100, 0, Hash::default(), @@ -989,12 +1005,12 @@ mod tests { let block_headers = vec![BlockHeader::default()]; let pkhs = vec![create_pkh(1)]; let keys = vec![create_bn256(1)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let genesis_hash = Hash::default(); let sb1 = sbs.build_superblock( &block_headers, - ars_identities, + validator_identities, 100, 0, genesis_hash, @@ -1019,12 +1035,12 @@ mod tests { let genesis_hash = Hash::default(); let pkhs = vec![create_pkh(1)]; let keys = vec![create_bn256(1)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let first_superblock = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 0, genesis_hash, @@ -1036,7 +1052,7 @@ mod tests { let expected_superblock = SuperBlock::new( 0, hash_merkle_tree_root(&hash_key_leaves( - &ars_identities.get_rep_ordered_bn256_list(&alt_keys), + &validator_identities.get_ordered_bn256_list(&alt_keys), )), Hash::default(), 0, @@ -1052,13 +1068,13 @@ mod tests { .unwrap(); let expected_sbs = SuperBlockState { - ars_current_identities: ars_identities, + ars_current_identities: validator_identities, signing_committee: HashSet::new(), current_superblock_beacon: CheckpointBeacon { checkpoint: 0, hash_prev_block: expected_superblock_hash, }, - ars_previous_identities: ARSIdentities::default(), + ars_previous_identities: ValidatorIdentities::default(), ..Default::default() }; assert_eq!(sbs, expected_sbs); @@ -1071,14 +1087,14 @@ mod tests { let block_headers = vec![BlockHeader::default()]; let pkhs = vec![create_pkh(1)]; let keys = vec![create_bn256(1)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let genesis_hash = Hash::default(); sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 0, genesis_hash, @@ -1090,7 +1106,7 @@ mod tests { let expected_second_superblock = SuperBlock::new( 1, hash_merkle_tree_root(&hash_key_leaves( - &ars_identities.get_rep_ordered_bn256_list(&alt_keys), + &validator_identities.get_ordered_bn256_list(&alt_keys), )), Hash::default(), 1, @@ -1102,13 +1118,13 @@ mod tests { assert_eq!( sbs.build_superblock( &[], - ars_identities.clone(), + validator_identities.clone(), 100, 1, genesis_hash, &alt_keys, None, - 1 + 1, ), expected_second_superblock ); @@ -1119,8 +1135,8 @@ mod tests { checkpoint: 1, hash_prev_block: expected_second_superblock.hash(), }; - expected_sbs.signing_committee = ars_identities.identities.iter().cloned().collect(); - expected_sbs.ars_previous_identities = ars_identities; + expected_sbs.signing_committee = validator_identities.identities.iter().cloned().collect(); + expected_sbs.ars_previous_identities = validator_identities; assert_eq!(sbs, expected_sbs); } @@ -1140,13 +1156,13 @@ mod tests { let block_headers = vec![BlockHeader::default()]; let pkhs = vec![create_pkh(1)]; let keys = vec![create_bn256(1)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let genesis_hash = Hash::default(); let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 0, genesis_hash, @@ -1163,7 +1179,7 @@ mod tests { let _sb2 = sbs.build_superblock( &block_headers, - ars_identities, + validator_identities, 100, 1, genesis_hash, @@ -1197,9 +1213,9 @@ mod tests { let p1 = PublicKey::from_bytes([1; 33]); let pkhs = vec![p1.pkh()]; let keys = vec![create_bn256(1)]; - let ars0 = ARSIdentities::new(vec![]); - let ars1 = ARSIdentities::new(pkhs.clone()); - let ars2 = ARSIdentities::new(pkhs.clone()); + let ars0 = ValidatorIdentities::new(vec![]); + let ars1 = ValidatorIdentities::new(pkhs.clone()); + let ars2 = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); @@ -1260,9 +1276,9 @@ mod tests { let p1 = PublicKey::from_bytes([1; 33]); let pkhs = vec![p1.pkh()]; let keys = vec![create_bn256(1)]; - let ars0 = ARSIdentities::new(vec![]); - let ars1 = ARSIdentities::new(pkhs.clone()); - let ars2 = ARSIdentities::new(pkhs.clone()); + let ars0 = ValidatorIdentities::new(vec![]); + let ars1 = ValidatorIdentities::new(pkhs.clone()); + let ars2 = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); // Superblock votes for index 0 cannot be validated because we do not know the ARS for index -1 @@ -1323,9 +1339,9 @@ mod tests { let p1 = PublicKey::from_bytes([1; 33]); let pkhs = vec![p1.pkh()]; let keys = vec![create_bn256(1)]; - let ars0 = ARSIdentities::new(vec![]); - let ars1 = ARSIdentities::new(pkhs.clone()); - let ars2 = ARSIdentities::new(pkhs.clone()); + let ars0 = ValidatorIdentities::new(vec![]); + let ars1 = ValidatorIdentities::new(pkhs.clone()); + let ars2 = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); // Superblock votes for index 0 cannot be validated because we do not know the ARS for index -1 @@ -1386,9 +1402,9 @@ mod tests { let p1 = PublicKey::from_bytes([1; 33]); let pkhs = vec![p1.pkh()]; let keys = vec![create_bn256(1)]; - let ars0 = ARSIdentities::new(vec![]); - let ars1 = ARSIdentities::new(pkhs.clone()); - let ars2 = ARSIdentities::new(pkhs.clone()); + let ars0 = ValidatorIdentities::new(vec![]); + let ars1 = ValidatorIdentities::new(pkhs.clone()); + let ars2 = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); // Superblock votes for index 0 cannot be validated because we do not know the ARS for index -1 @@ -1463,11 +1479,11 @@ mod tests { let p2 = PublicKey::from_bytes([2; 33]); let p3 = PublicKey::from_bytes([3; 33]); - let ars0 = ARSIdentities::new(vec![]); - let ars1 = ARSIdentities::new(vec![p1.pkh()]); - let ars2 = ARSIdentities::new(vec![p2.pkh()]); - let ars3 = ARSIdentities::new(vec![p3.pkh()]); - let ars4 = ARSIdentities::new(vec![]); + let ars0 = ValidatorIdentities::new(vec![]); + let ars1 = ValidatorIdentities::new(vec![p1.pkh()]); + let ars2 = ValidatorIdentities::new(vec![p2.pkh()]); + let ars3 = ValidatorIdentities::new(vec![p3.pkh()]); + let ars4 = ValidatorIdentities::new(vec![]); let pkhs = vec![p1.pkh(), p2.pkh(), p3.pkh()]; let keys = vec![create_bn256(1), create_bn256(2), create_bn256(3)]; let alt_keys = create_alt_keys(pkhs, keys); @@ -1618,7 +1634,7 @@ mod tests { let pkhs = vec![p1.pkh(), p2.pkh(), p3.pkh()]; let keys = vec![create_bn256(1), create_bn256(2), create_bn256(3)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let create_votes = |superblock_hash, superblock_index| { @@ -1643,7 +1659,7 @@ mod tests { // (because it does not exist). When adding a vote it will return NotInSigningCommittee let sb0 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 0, genesis_hash, @@ -1671,10 +1687,10 @@ mod tests { AddSuperBlockVote::NotInSigningCommittee ); - // Create a superblock with the ars_identities + // Create a superblock with the validator_identities let sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 1, genesis_hash, @@ -1703,7 +1719,7 @@ mod tests { let sb2 = sbs.build_superblock( &block_headers, - ars_identities, + validator_identities, 100, 2, genesis_hash, @@ -1749,9 +1765,9 @@ mod tests { ]; let alt_keys = create_alt_keys(pkhs1.clone(), keys); - let ars0 = ARSIdentities::new(pkhs0); - let ars1 = ARSIdentities::new(pkhs1); - let ars2 = ARSIdentities::new(pkhs2); + let ars0 = ValidatorIdentities::new(pkhs0); + let ars1 = ValidatorIdentities::new(pkhs1); + let ars2 = ValidatorIdentities::new(pkhs2); let create_votes = |superblock_hash, superblock_index| { let mut v1 = SuperBlockVote::new_unsigned(superblock_hash, superblock_index); @@ -1878,14 +1894,14 @@ mod tests { let pkhs = vec![p1.pkh(), p2.pkh(), p3.pkh()]; let keys = vec![create_bn256(1), create_bn256(2), create_bn256(3)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let block_headers = vec![BlockHeader::default()]; let genesis_hash = Hash::default(); let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 0, genesis_hash, @@ -1896,7 +1912,7 @@ mod tests { let expected_sb2 = mining_build_superblock( &block_headers, - &hash_key_leaves(&ars_identities.get_rep_ordered_bn256_list(&alt_keys)), + &hash_key_leaves(&validator_identities.get_ordered_bn256_list(&alt_keys)), 1, genesis_hash, 3, @@ -1913,7 +1929,7 @@ mod tests { // Create the second superblock afterwards let sb2 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 1, genesis_hash, @@ -1938,7 +1954,7 @@ mod tests { // set as "InvalidIndex" let _sb3 = sbs.build_superblock( &block_headers, - ars_identities, + validator_identities, 100, 2, genesis_hash, @@ -1966,7 +1982,7 @@ mod tests { let pkhs = vec![p1.pkh(), p2.pkh(), p3.pkh()]; let keys = vec![create_bn256(1), create_bn256(2), create_bn256(3)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let block_headers = vec![BlockHeader::default()]; @@ -1974,7 +1990,7 @@ mod tests { // superblock with index 0. let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 2, 0, genesis_hash, @@ -1985,7 +2001,7 @@ mod tests { // superblock with index 1 let sb2 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 2, 1, genesis_hash, @@ -1996,7 +2012,7 @@ mod tests { let expected_sb2 = mining_build_superblock( &block_headers, - &hash_key_leaves(&ars_identities.get_rep_ordered_bn256_list(&alt_keys)), + &hash_key_leaves(&validator_identities.get_ordered_bn256_list(&alt_keys)), 1, genesis_hash, 2, @@ -2044,13 +2060,13 @@ mod tests { let block_headers = vec![BlockHeader::default()]; let pkhs = vec![create_pkh(1)]; let keys = vec![create_bn256(1)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let genesis_hash = Hash::default(); let _sb1 = sbs.build_superblock( &block_headers, - ars_identities, + validator_identities, 100, 2, genesis_hash, @@ -2073,13 +2089,13 @@ mod tests { let block_headers = vec![BlockHeader::default()]; let pkhs = vec![create_pkh(1)]; let keys = vec![create_bn256(1)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let genesis_hash = Hash::default(); let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 2, genesis_hash, @@ -2090,7 +2106,7 @@ mod tests { let _sb2 = sbs.build_superblock( &block_headers, - ars_identities, + validator_identities, 2, 5, genesis_hash, @@ -2126,7 +2142,7 @@ mod tests { create_bn256(3), create_bn256(4), ]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let block_headers = vec![BlockHeader::default()]; @@ -2134,7 +2150,7 @@ mod tests { // superblock with index 0. let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 2, 0, genesis_hash, @@ -2145,7 +2161,7 @@ mod tests { // superblock with index 1 let sb2 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 2, 1, genesis_hash, @@ -2156,7 +2172,7 @@ mod tests { let expected_sb2 = mining_build_superblock( &block_headers, - &hash_key_leaves(&ars_identities.get_rep_ordered_bn256_list(&alt_keys)), + &hash_key_leaves(&validator_identities.get_ordered_bn256_list(&alt_keys)), 1, genesis_hash, 2, @@ -2207,14 +2223,14 @@ mod tests { let pkhs = vec![p1.pkh(), p2.pkh(), p3.pkh()]; let keys = vec![create_bn256(1), create_bn256(2), create_bn256(3)]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); let block_headers = vec![BlockHeader::default()]; let genesis_hash = Hash::default(); let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 100, 0, genesis_hash, @@ -2222,7 +2238,7 @@ mod tests { None, 1, ); - sbs.ars_previous_identities = ars_identities.clone(); + sbs.ars_previous_identities = validator_identities.clone(); let committee_size = 4; let subset = calculate_superblock_signing_committee( sbs.ars_previous_identities, @@ -2231,7 +2247,7 @@ mod tests { sbs.current_superblock_beacon.hash_prev_block, 1, ); - assert_eq!(ars_identities.len(), subset.len()); + assert_eq!(validator_identities.len(), subset.len()); } #[test] fn test_build_superblock_with_optional_superblock() { @@ -2252,7 +2268,7 @@ mod tests { create_bn256(4), create_bn256(5), ]; - let ars = ARSIdentities::new(pkhs.clone()); + let ars = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); sbs.ars_current_identities = ars.clone(); @@ -2326,14 +2342,14 @@ mod tests { p7.pkh(), p8.pkh(), ]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, vec![]); let block_headers = vec![BlockHeader::default()]; let genesis_hash = Hash::default(); let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 4, 0, genesis_hash, @@ -2341,7 +2357,7 @@ mod tests { None, 1, ); - sbs.ars_previous_identities = ars_identities; + sbs.ars_previous_identities = validator_identities; let committee_size = 4; let subset = calculate_superblock_signing_committee( sbs.ars_previous_identities, @@ -2371,14 +2387,14 @@ mod tests { let p3 = PublicKey::from_bytes([3; 33]); let pkhs = vec![p1.pkh(), p2.pkh(), p3.pkh()]; - let ars_identities = ARSIdentities::new(pkhs.clone()); + let validator_identities = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, vec![]); let block_headers = vec![BlockHeader::default()]; let genesis_hash = Hash::default(); let _sb1 = sbs.build_superblock( &block_headers, - ars_identities.clone(), + validator_identities.clone(), 2, 0, genesis_hash, @@ -2386,7 +2402,7 @@ mod tests { None, 1, ); - sbs.ars_previous_identities = ars_identities.clone(); + sbs.ars_previous_identities = validator_identities.clone(); let committee_size = 2; let subset = calculate_superblock_signing_committee( sbs.ars_previous_identities, @@ -2400,7 +2416,7 @@ mod tests { assert_eq!(subset, vec![p1.pkh(), p3.pkh()].into_iter().collect()); assert_eq!(usize::try_from(committee_size).unwrap(), subset.len()); - sbs.ars_previous_identities = ars_identities; + sbs.ars_previous_identities = validator_identities; let committee_size = 2; let subset_2 = calculate_superblock_signing_committee( sbs.ars_previous_identities.clone(), @@ -2552,12 +2568,12 @@ mod tests { #[test] fn test_get_beacon_2() { let superblock_state = SuperBlockState { - ars_current_identities: ARSIdentities::default(), + ars_current_identities: ValidatorIdentities::default(), current_superblock_beacon: CheckpointBeacon { checkpoint: 0, hash_prev_block: Hash::SHA256([1; 32]), }, - ars_previous_identities: ARSIdentities::default(), + ars_previous_identities: ValidatorIdentities::default(), ..Default::default() }; let beacon = superblock_state.get_beacon(); @@ -2574,12 +2590,12 @@ mod tests { #[test] fn test_get_beacon_3() { let superblock_state = SuperBlockState { - ars_current_identities: ARSIdentities::default(), + ars_current_identities: ValidatorIdentities::default(), current_superblock_beacon: CheckpointBeacon { checkpoint: 1, hash_prev_block: Hash::default(), }, - ars_previous_identities: ARSIdentities::default(), + ars_previous_identities: ValidatorIdentities::default(), ..Default::default() }; let beacon = superblock_state.get_beacon(); @@ -2629,9 +2645,9 @@ mod tests { create_bn256(4), create_bn256(5), ]; - let ars0 = ARSIdentities::new(vec![]); - let ars1 = ARSIdentities::new(pkhs.clone()); - let ars2 = ARSIdentities::new(pkhs.clone()); + let ars0 = ValidatorIdentities::new(vec![]); + let ars1 = ValidatorIdentities::new(pkhs.clone()); + let ars2 = ValidatorIdentities::new(pkhs.clone()); let alt_keys = create_alt_keys(pkhs, keys); diff --git a/data_structures/src/transaction.rs b/data_structures/src/transaction.rs index 4c98820e6..4c838eaa7 100644 --- a/data_structures/src/transaction.rs +++ b/data_structures/src/transaction.rs @@ -3,13 +3,18 @@ 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::{ Block, Bn256PublicKey, DataRequestOutput, Epoch, Hash, Hashable, Input, KeyedSignature, - PublicKeyHash, ValueTransferOutput, + PublicKeyHash, StakeOutput, ValueTransferOutput, }, + error::TransactionError, proto::{schema::witnet, ProtobufConvert}, vrf::DataRequestEligibilityClaim, }; @@ -18,6 +23,8 @@ 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_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; @@ -130,6 +137,8 @@ pub enum Transaction { Reveal(RevealTransaction), Tally(TallyTransaction), Mint(MintTransaction), + Stake(StakeTransaction), + Unstake(UnstakeTransaction), } impl From for Transaction { @@ -168,6 +177,18 @@ impl From for Transaction { } } +impl From for Transaction { + fn from(transaction: StakeTransaction) -> Self { + Self::Stake(transaction) + } +} + +impl From for Transaction { + fn from(transaction: UnstakeTransaction) -> Self { + Self::Unstake(transaction) + } +} + impl AsRef for Transaction { fn as_ref(&self) -> &Self { self @@ -249,6 +270,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 @@ -375,8 +404,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, @@ -386,6 +415,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 @@ -683,6 +724,152 @@ 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 { + 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, + 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 + /// 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_WEIGHT) + } +} + +#[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 validator: PublicKeyHash, + pub withdrawal: ValueTransferOutput, + + #[protobuf_convert(skip)] + #[serde(skip)] + hash: MemoHash, +} + +impl UnstakeTransactionBody { + /// Creates a new stake transaction body. + pub fn new(validator: PublicKeyHash, withdrawal: ValueTransferOutput) -> Self { + UnstakeTransactionBody { + validator, + 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() @@ -722,6 +909,24 @@ 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 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(); @@ -765,6 +970,17 @@ impl Hashable for RevealTransaction { } } +impl Hashable for StakeTransaction { + fn hash(&self) -> Hash { + self.body.hash() + } +} +impl Hashable for UnstakeTransaction { + fn hash(&self) -> Hash { + self.body.hash() + } +} + impl Hashable for Transaction { fn hash(&self) -> Hash { match self { @@ -774,6 +990,8 @@ impl Hashable for Transaction { Transaction::Reveal(tx) => tx.hash(), Transaction::Tally(tx) => tx.hash(), Transaction::Mint(tx) => tx.hash(), + Transaction::Stake(tx) => tx.hash(), + Transaction::Unstake(tx) => tx.hash(), } } } @@ -973,8 +1191,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(); @@ -994,8 +1212,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 31296b267..1731e1d23 100644 --- a/data_structures/src/transaction_factory.rs +++ b/data_structures/src/transaction_factory.rs @@ -6,14 +6,15 @@ use std::{ use serde::{Deserialize, Serialize}; +use crate::transaction::UnstakeTransactionBody; use crate::{ chain::{ - DataRequestOutput, Epoch, EpochConstants, Input, OutputPointer, PublicKeyHash, + DataRequestOutput, Epoch, EpochConstants, Input, OutputPointer, PublicKeyHash, StakeOutput, ValueTransferOutput, }, error::TransactionError, fee::{AbsoluteFee, Fee}, - transaction::{DRTransactionBody, VTTransactionBody, INPUT_SIZE}, + transaction::{DRTransactionBody, StakeTransactionBody, VTTransactionBody, INPUT_SIZE}, utxo_pool::{ NodeUtxos, NodeUtxosRef, OwnUnspentOutputsPool, UnspentOutputsPool, UtxoDiff, UtxoSelectionStrategy, @@ -21,7 +22,7 @@ use crate::{ wit::Wit, }; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct CollectedOutputs { pub pointers: Vec, pub resolved: Vec, @@ -75,6 +76,25 @@ impl NodeBalance { } } +#[derive(Clone, Debug)] +pub enum TransactionOutputs { + DataRequest((DataRequestOutput, Option)), + Stake((StakeOutput, Option)), + Unstake(ValueTransferOutput), + 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::Unstake(output) => vec![output], + 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 +180,135 @@ 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::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(); + current_weight = body.weight(); + } + }; + + match fee { + Fee::Absolute(absolute_fee) => { + let amount = output_value + .checked_add(absolute_fee.as_nanowits()) + .ok_or(TransactionError::FeeOverflow)?; + + // 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, + inputs, + output_value, + outputs: outputs.into(), + }) + } + 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 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() + } + _ => unreachable!(), + }; + + 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 +403,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 +580,8 @@ pub fn build_drt( Ok(DRTransactionBody::new( used_pointers.collect::>(), - outputs, dr_output, + outputs, )) } @@ -583,6 +732,67 @@ pub fn transaction_outputs_sum(outputs: &[ValueTransferOutput]) -> Result 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)] mod tests { use std::{ diff --git a/data_structures/src/types.rs b/data_structures/src/types.rs index fa6b0cd1b..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) => { @@ -75,6 +75,8 @@ 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")?, + Transaction::Unstake(_) => f.write_str("UNSTAKE_TRANSACTION")?, } write!(f, ": {}", tx.hash()) } 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/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/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/data_structures/tests/serializers.rs b/data_structures/tests/serializers.rs index 2b4ef28c7..df5467e6d 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,16 @@ 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::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_8).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::V2_0).unwrap(); assert_eq!(result, expected_buf); } @@ -365,7 +438,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/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/handlers.rs b/node/src/actors/chain_manager/handlers.rs index e45a46bcb..eb489c60a 100644 --- a/node/src/actors/chain_manager/handlers.rs +++ b/node/src/actors/chain_manager/handlers.rs @@ -9,14 +9,18 @@ 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}, + staking::errors::StakesError, + transaction::{DRTransaction, StakeTransaction, Transaction, VTTransaction}, transaction_factory::{self, NodeBalance}, types::LastBeacon, utxo_pool::{get_utxo_info, UtxoInfo}, @@ -29,13 +33,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, QueryStake, ReputationStats, Rewind, SendLastBeacon, SessionUnitResult, + SetLastBeacon, SetPeersLimits, SignalingInfo, SnapshotExport, SnapshotImport, + TryMineBlock, }, sessions_manager::SessionsManager, }, @@ -967,11 +972,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 +1008,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 { @@ -1277,6 +1293,81 @@ 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 !msg.dry_run && 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 = ::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/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 231bdfb9c..ab08278c1 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -15,7 +15,11 @@ 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}, @@ -28,12 +32,14 @@ use witnet_data_structures::{ DataRequestPool, }, error::TransactionError, - get_environment, + get_environment, get_protocol_version, + 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}, @@ -50,7 +56,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::{ @@ -112,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; @@ -229,6 +236,7 @@ impl ChainManager { ), max_vt_weight, max_dr_weight, + max_st_weight, beacon, eligibility_claim, &tally_transactions, @@ -246,7 +254,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)) @@ -808,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], @@ -836,14 +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(); + 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(); @@ -858,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, @@ -949,7 +969,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) { @@ -984,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( @@ -1000,6 +1053,23 @@ 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 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) + }; + + 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, @@ -1007,6 +1077,8 @@ pub fn build_block( commit_hash_merkle_root, reveal_hash_merkle_root, tally_hash_merkle_root, + stake_hash_merkle_root, + unstake_hash_merkle_root, }; let block_header = BlockHeader { @@ -1024,6 +1096,8 @@ pub fn build_block( commit_txns, reveal_txns, tally_txns, + stake_txns, + unstake_txns, }; (block_header, txns) @@ -1065,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(); @@ -1078,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, &[], @@ -1246,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(); @@ -1282,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, &[], @@ -1347,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(); @@ -1383,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, &[], @@ -1446,9 +1526,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![]); @@ -1462,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(); @@ -1498,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, &[], @@ -1542,9 +1624,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![]); @@ -1560,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(); @@ -1596,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/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index bc2c48602..99d17efbd 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::{ @@ -60,7 +61,7 @@ use witnet_data_structures::{ penalize_factor, priority::{Priorities, PriorityEngine, PriorityVisitor}, reputation_issuance, - tapi::{after_second_hard_fork, current_active_wips, in_emergency_period, ActiveWips}, + tapi::{after_second_hard_fork, current_active_wips, ActiveWips}, Alpha, AltKeys, Block, BlockHeader, Bn256PublicKey, ChainImport, ChainInfo, ChainState, CheckpointBeacon, CheckpointVRF, ConsensusConstants, DataRequestInfo, DataRequestOutput, DataRequestStage, Epoch, EpochConstants, Hash, Hashable, InventoryEntry, InventoryItem, @@ -70,7 +71,8 @@ use witnet_data_structures::{ data_request::DataRequestPool, get_environment, radon_report::{RadonReport, ReportContext}, - superblock::{ARSIdentities, AddSuperBlockVote, SuperBlockConsensus}, + staking::prelude::*, + superblock::{AddSuperBlockVote, SuperBlockConsensus, ValidatorIdentities}, transaction::{RevealTransaction, TallyTransaction, Transaction}, types::{ visitor::{StatefulVisitor, Visitor}, @@ -725,8 +727,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; @@ -904,6 +907,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(); @@ -968,7 +972,26 @@ impl ChainManager { let miner_pkh = block.block_header.proof.proof.pkh(); - // Do not update reputation when consolidating genesis block + // Track all active addresses to update the active epoch of all validators + let mut active_addresses: HashSet = HashSet::new(); + active_addresses.insert(miner_pkh); + + for ta_tx in &block.txns.tally_txns { + // Only update active epoch of honest validators + // If there was a liar or error committer, the last output is the creator of the data request + // This is not guaranteed to be a validator, so do not update its active epoch + let data_requester_output = + ta_tx.out_of_consensus.len() + ta_tx.error_committers.len() > 0; + for (i, output) in ta_tx.outputs.iter().enumerate() { + if data_requester_output && i == ta_tx.outputs.len() - 1 { + break; + } + active_addresses.insert(output.pkh); + } + } + stakes.update_active_epoch(active_addresses, block_epoch); + + // Do not update reputation or stakes when consolidating genesis block if block_hash != chain_info.consensus_constants.genesis_hash { update_reputation( reputation_engine, @@ -980,6 +1003,18 @@ impl ChainManager { block_epoch, self.own_pkh.unwrap_or_default(), ); + + 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); } // Update bn256 public keys with block information @@ -1860,30 +1895,11 @@ impl ChainManager { } let chain_info = act.chain_state.chain_info.as_ref().unwrap(); - let reputation_engine = act.chain_state.reputation_engine.as_ref().unwrap(); let last_superblock_signed_by_bootstrap = last_superblock_signed_by_bootstrap(&chain_info.consensus_constants); - let ars_members = - // Before reaching the epoch activity_period + collateral_age the bootstrap committee signs the superblock - // collateral_age is measured in blocks instead of epochs, but this only means that the period in which - // the bootstrap committee signs is at least epoch activity_period + collateral_age - if let Some(ars_members) = in_emergency_period(superblock_index, get_environment()) { - // Bootstrap committee - ars_members - } else if superblock_index >= last_superblock_signed_by_bootstrap { - reputation_engine.get_rep_ordered_ars_list() - } else { - chain_info - .consensus_constants - .bootstrapping_committee - .iter() - .map(|add| add.parse().expect("Malformed bootstrapping committee")) - .collect() - }; - - // Get the list of members of the ARS with reputation greater than 0 - // the list itself is ordered by decreasing reputation - let ars_identities = ARSIdentities::new(ars_members); + // Get the list of validators sorted by most recently active + let validators = act.chain_state.stakes.get_active_validators(block_epoch - 2000); + let ars_identities = ValidatorIdentities::new(validators); // After the second hard fork, the superblock committee size must be at least 50 let min_committee_size = if after_second_hard_fork(block_epoch, get_environment()) { 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, &[], diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index a408bc48d..76c4a1d6c 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -23,9 +23,11 @@ use serde::{Deserialize, Serialize}; use witnet_crypto::key::KeyPath; use witnet_data_structures::{ chain::{ - tapi::ActiveWips, Block, DataRequestOutput, Epoch, Hash, Hashable, PublicKeyHash, RADType, - StateMachine, SyncStatus, + tapi::ActiveWips, Block, DataRequestOutput, Epoch, Hash, Hashable, KeyedSignature, + PublicKeyHash, RADType, StakeOutput, StateMachine, SyncStatus, }, + get_environment, + staking::prelude::*, transaction::Transaction, vrf::VrfMessage, }; @@ -37,13 +39,14 @@ use crate::{ inventory_manager::{InventoryManager, InventoryManagerError}, json_rpc::Subscriptions, messages::{ - AddCandidates, AddPeers, AddTransaction, BuildDrt, BuildVtt, ClearPeers, DropAllPeers, + AddCandidates, AddPeers, AddTransaction, AuthorizeStake, BuildDrt, BuildStake, + 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, + GetState, GetSupplyInfo, GetUtxoInfo, InitializePeers, IsConfirmedBlock, QueryStake, + QueryStakesParams, Rewind, SnapshotExport, SnapshotImport, StakeAuthorization, }, peers_manager::PeersManager, sessions_manager::SessionsManager, @@ -136,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. @@ -266,6 +272,23 @@ 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()), + )) + }); + + 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> { @@ -1921,6 +1944,214 @@ 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 { + // 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 + }; + + // 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 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, + fee: params.fee, + utxo_strategy: params.utxo_strategy, + stake_output: StakeOutput { + authorization, + key, + value: params.value, + }, + }; + + ChainManager::from_registry() + .send(build_stake) + .map(|res| match res { + 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(internal_error) + } else { + serde_json::to_value(transaction).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 +} + +/// 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 { + // 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 +} + +/// 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 { @@ -2124,7 +2355,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); } @@ -2187,7 +2418,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, )) } @@ -2226,6 +2457,7 @@ mod tests { all_methods_vec, vec![ "addPeers", + "authorizeStake", "chainExport", "chainImport", "clearPeers", @@ -2256,6 +2488,7 @@ mod tests { "sendValue", "sign", "signalingInfo", + "stake", "syncStatus", "tryRequest", "witnet_subscribe", @@ -2274,6 +2507,7 @@ mod tests { let expected_sensitive_methods = vec![ "addPeers", + "authorizeStake", "clearPeers", "createVRF", "getPkh", @@ -2286,6 +2520,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 d11776e2b..fe605488d 100644 --- a/node/src/actors/messages.rs +++ b/node/src/actors/messages.rs @@ -21,18 +21,21 @@ use witnet_data_structures::{ priority::PrioritiesEstimate, tapi::{ActiveWips, BitVotesCounter}, Block, CheckpointBeacon, DataRequestInfo, DataRequestOutput, Epoch, EpochConstants, Hash, - InventoryEntry, InventoryItem, NodeStats, PointerToBlock, PublicKeyHash, - PublicKeyHashParseError, RADRequest, RADTally, Reputation, StateMachine, SuperBlock, - SuperBlockVote, SupplyInfo, ValueTransferOutput, + 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, + staking::{aux::StakeKey, stakes::QueryStakesKey}, transaction::{ - CommitTransaction, DRTransaction, RevealTransaction, Transaction, VTTransaction, + CommitTransaction, DRTransaction, RevealTransaction, StakeTransaction, Transaction, + VTTransaction, }, transaction_factory::NodeBalance, types::LastBeacon, utxo_pool::{UtxoInfo, UtxoSelectionStrategy}, + wit::Wit, }; use witnet_p2p::{ error::SessionsError, @@ -220,6 +223,134 @@ 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 { + /// One instance of `StakeOutput` + 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 `StakeTransaction` from a list of `ValueTransferOutput`s +#[derive(Clone, Debug, Default, 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, +} + +/// 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 { + /// 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: PublicKeyHash, + /// A node's signature of a withdrawer's address + pub signature: KeyedSignature, +} + +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 { @@ -1280,3 +1411,37 @@ 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), + } + } +} 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, 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()); diff --git a/schemas/witnet/witnet.proto b/schemas/witnet/witnet.proto index 64b1b04e0..2d6d83f3a 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; @@ -59,6 +113,8 @@ 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; + Hash unstake_hash_merkle_root = 8; } uint32 signals = 1; CheckpointBeacon beacon = 2; @@ -73,6 +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; } BlockHeader block_header = 1; @@ -229,6 +287,39 @@ message MintTransaction { repeated ValueTransferOutput outputs = 2; } +message StakeKey { + PublicKeyHash validator = 1; + PublicKeyHash withdrawer = 2; +} + +message StakeOutput { + uint64 value = 1; + StakeKey key = 2; + KeyedSignature authorization = 3; +} + +message StakeTransactionBody { + repeated Input inputs = 1; + StakeOutput output = 2; + optional ValueTransferOutput change = 3; +} + +message StakeTransaction { + StakeTransactionBody body = 1 ; + repeated KeyedSignature signatures = 2; +} + +message UnstakeTransactionBody { + PublicKeyHash validator = 1; + ValueTransferOutput withdrawal = 2; + ValueTransferOutput change = 3; +} + +message UnstakeTransaction { + UnstakeTransactionBody body = 1 ; + KeyedSignature signature = 2; +} + message Transaction { oneof kind { VTTransaction ValueTransfer = 1; @@ -237,6 +328,8 @@ message Transaction { RevealTransaction Reveal = 4; TallyTransaction Tally = 5; MintTransaction Mint = 6; + StakeTransaction Stake = 7; + UnstakeTransaction Unstake = 8; } } 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 c1a9ee6c9..cd345e139 100644 --- a/src/cli/node/json_rpc_client.rs +++ b/src/cli/node/json_rpc_client.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashMap, + collections::{BTreeSet, HashMap, HashSet}, convert::TryFrom, fmt, fs::File, @@ -14,7 +14,9 @@ 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 witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; use witnet_crypto::{ hash::calculate_sha256, @@ -29,8 +31,9 @@ use witnet_data_structures::{ SupplyInfo, SyncStatus, ValueTransferOutput, }, fee::Fee, + get_environment, proto::ProtobufConvert, - transaction::{DRTransaction, Transaction, VTTransaction}, + transaction::{DRTransaction, StakeTransaction, Transaction, VTTransaction}, transaction_factory::NodeBalance, types::SequentialId, utxo_pool::{UtxoInfo, UtxoSelectionStrategy}, @@ -38,8 +41,13 @@ 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}, + json_rpc::api::{ + AddrType, GetBlockChainParams, GetTransactionOutput, PeersResult, QueryStakesArgument, + }, + messages::{ + AuthorizeStake, BuildDrt, BuildStakeParams, BuildStakeResponse, BuildVtt, GetBalanceTarget, + GetReputationResult, MagicEither, SignalingInfo, StakeAuthorization, + }, }; use witnet_rad::types::RadonTypes; use witnet_util::{files::create_private_file, timestamp::pretty_print}; @@ -855,6 +863,184 @@ pub fn send_dr( Ok(()) } +#[allow(clippy::too_many_arguments)] +pub fn send_st( + addr: SocketAddr, + value: u64, + 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); + + // 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, + 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 (bsr, ..): (BuildStakeResponse, _) = + issue_method("stake", Some(dry_params), &mut stream, id.next())?; + let dry_weight = bsr.transaction.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)?; + } + + 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())?; + + // 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 + }; + + 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())?; + + 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!("The stake facts have not been confirmed. No stake transaction has been created."); + } + + Ok(()) +} + +pub fn authorize_st(addr: SocketAddr, withdrawer: Option) -> Result<(), failure::Error> { + 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())?; + + 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)?; + let auth_ascii = auth_qr + .render::() + .quiet_zone(true) + .dark_color(unicode::Dense1x2::Light) + .light_color(unicode::Dense1x2::Dark) + .build(); + + println!( + "Authorization code:\n{}\nQR code for myWitWallet:\n{}", + auth_string, auth_ascii + ); + + Ok(()) +} + pub fn master_key_export( addr: SocketAddr, write_to_path: Option<&Path>, @@ -1642,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, @@ -1849,7 +2063,6 @@ where id.unwrap_or(1) ); let response = send_request(stream, &request)?; - parse_response::(&response).map(|output| (output, (request, response))) } @@ -1900,6 +2113,84 @@ fn prompt_user_for_priority_selection( Ok(fee) } +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(); + + // 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(); + 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(); + + if ["Y", "YES"].contains(&selected.as_str()) { + return Ok(true); + } else if ["", "N", "NO"].contains(&selected.as_str()) { + break; + } + } + + Ok(false) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/cli/node/with_node.rs b/src/cli/node/with_node.rs index 41321f29c..0a45cbd3a 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; @@ -269,6 +269,32 @@ 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, + authorization, + withdrawer, + fee, + require_confirmation, + dry_run, + } => rpc::send_st( + node.unwrap_or(default_jsonrpc), + value, + authorization.map(MagicEither::Left), + withdrawer.map(MagicEither::Left), + fee.map(Fee::absolute_from_nanowits), + None, + require_confirmation, + dry_run, + ), + 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), } } @@ -730,6 +756,49 @@ 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, + /// Stake authorization code (the withdrawer address, signed by the validator node) + #[structopt(long = "authorization")] + authorization: Option, + /// Withdrawer + #[structopt(long = "withdrawer")] + 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, + }, + #[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, + }, + 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)] 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..4e395494a 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,6 +50,8 @@ 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 MIN_STAKE_NANOWITS: u64 = PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS; + 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 +438,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, @@ -1450,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, @@ -1486,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( @@ -1531,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![]); @@ -1576,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]); @@ -1622,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( @@ -1662,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( @@ -1707,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( @@ -1776,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( @@ -1812,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]); @@ -2210,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]); @@ -2278,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]); @@ -2336,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]); @@ -2390,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]); @@ -2426,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(); @@ -2517,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]); @@ -2568,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]); @@ -2619,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]); @@ -2680,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]); @@ -2733,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]); @@ -2781,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]); @@ -2828,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]); @@ -2899,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]); @@ -2930,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]); @@ -2967,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, @@ -2982,6 +2988,7 @@ fn test_empty_commit(c_tx: &CommitTransaction) -> Result<(), failure::Error> { block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, ) .map(|_| ()) } @@ -2998,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(); @@ -3011,7 +3019,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(); @@ -3036,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)?; @@ -3071,7 +3080,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(); @@ -3110,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, @@ -3124,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)); @@ -3158,7 +3169,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(); @@ -3186,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, @@ -3200,6 +3213,7 @@ fn test_commit_with_collateral( block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, ) .map(|_| ()) } @@ -3321,7 +3335,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(); @@ -3418,7 +3432,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; @@ -3431,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, @@ -3445,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)); @@ -3485,7 +3502,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(); @@ -3507,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, @@ -3521,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(), @@ -3856,7 +3876,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(); @@ -3892,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, @@ -3906,6 +3928,7 @@ fn commitment_collateral_zero_is_minimum() { block_number, minimum_reppoe_difficulty, ¤t_active_wips(), + superblock_period, ) .map(|_| ()) }; @@ -3945,7 +3968,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(); @@ -3983,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, @@ -3997,6 +4021,7 @@ fn commitment_timelock() { block_number, minimum_reppoe_difficulty, &active_wips, + superblock_period, ) .map(|_| ())?; @@ -4042,7 +4067,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(); @@ -4161,7 +4186,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(); @@ -4292,7 +4317,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(); @@ -4577,7 +4602,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(), @@ -4690,7 +4715,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(), @@ -4907,7 +4932,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(); @@ -5205,7 +5230,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(), @@ -8448,6 +8473,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![], 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] @@ -9098,7 +9224,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(); @@ -9191,7 +9317,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(); @@ -9360,7 +9486,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; @@ -9375,7 +9501,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]); @@ -9923,7 +10049,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]); @@ -9959,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_tx1 = DRTransaction::new(dr_tx_body, vec![drs]); @@ -9979,7 +10105,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]); @@ -10020,7 +10146,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]); @@ -10506,7 +10632,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(); @@ -10898,12 +11024,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); @@ -10940,7 +11066,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]); @@ -10979,12 +11105,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 2ed59872c..a0595d3c4 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -6,11 +6,14 @@ 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, }; use witnet_crypto::{ + hash, hash::{calculate_sha256, Sha256}, merkle::{merkle_tree_root as crypto_merkle_tree_root, ProgressiveMerkleTree}, signature::{verify, PublicKey, Signature}, @@ -21,17 +24,19 @@ use witnet_data_structures::{ 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, calculate_witness_reward_before_second_hard_fork, create_tally, DataRequestPool, }, error::{BlockError, DataRequestError, TransactionError}, + get_protocol_version, + proto::versioning::{ProtocolVersion, VersionedHashable}, radon_report::{RadonReport, ReportContext}, transaction::{ - CommitTransaction, DRTransaction, MintTransaction, RevealTransaction, TallyTransaction, - Transaction, VTTransaction, + CommitTransaction, DRTransaction, MintTransaction, RevealTransaction, StakeTransaction, + TallyTransaction, Transaction, UnstakeTransaction, VTTransaction, }, transaction_factory::{transaction_inputs_sum, transaction_outputs_sum}, types::visitor::Visitor, @@ -50,6 +55,12 @@ 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 = PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS; +const MAX_UNSTAKE_BLOCK_WEIGHT: u32 = 5_000; +const UNSTAKING_DELAY_SECONDS: u32 = 1_209_600; + /// Returns the fee of a value transfer transaction. /// /// The fee is the difference between the outputs and the inputs @@ -96,6 +107,43 @@ 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; + + if out_value > in_value { + Err(TransactionError::NegativeFee.into()) + } else { + Ok(in_value - out_value) + } +} + +/// 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) @@ -375,8 +423,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 +1175,133 @@ 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, + utxo_diff: &UtxoDiff<'_>, + epoch: Epoch, + epoch_constants: EpochConstants, + signatures_to_verify: &mut Vec, +) -> 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 { + Err(TransactionError::StakeBelowMinimum { + min_stake: MIN_STAKE_NANOWITS, + stake: st_tx.body.output.value, + })?; + } + + 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() { + Err(TransactionError::NoInputs { + tx_hash: st_tx.hash(), + })?; + } + + 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 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, @@ -1149,7 +1322,9 @@ pub fn validate_block_signature( let signature = keyed_signature.signature.clone().try_into()?; let public_key = keyed_signature.public_key.clone().try_into()?; - 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); @@ -1427,9 +1602,8 @@ 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 let mut total_fee = 0; // When validating genesis block, keep track of total value created @@ -1718,6 +1892,110 @@ pub fn validate_block_transactions( ); } + 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()); + } + + 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)); + // } + } + + (st_mt.root(), ut_mt.root()) + } else { + // Nullify stake and unstake merkle roots for the legacy protocol version + (hash::EMPTY_SHA256, hash::EMPTY_SHA256) + }; + // Validate Merkle Root let merkle_roots = BlockMerkleRoots { mint_hash: block.txns.mint.hash(), @@ -1726,9 +2004,16 @@ 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_root), + unstake_hash_merkle_root: Hash::from(ut_root), }; if merkle_roots != block.block_header.merkle_roots { + log::debug!( + "{:?} vs {:?}", + merkle_roots, + block.block_header.merkle_roots + ); Err(BlockError::NotValidMerkleTree.into()) } else { Ok(utxo_diff.take_diff()) @@ -1894,6 +2179,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 +2459,8 @@ 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), + unstake_hash_merkle_root: merkle_tree_root(&block.txns.unstake_txns), }; merkle_roots == block.block_header.merkle_roots 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..d11b42dbd 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, @@ -31,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 7309b428e..a7fb1d283 100644 --- a/wallet/src/db/encrypted/mod.rs +++ b/wallet/src/db/encrypted/mod.rs @@ -1,9 +1,13 @@ +use serde::de::DeserializeOwned; 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; @@ -101,3 +105,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/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..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 { @@ -1144,7 +1167,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); @@ -1490,6 +1513,8 @@ where Transaction::Reveal(_) => None, Transaction::Tally(_) => None, Transaction::Mint(_) => None, + Transaction::Stake(tx) => Some(&tx.body.inputs), + Transaction::Unstake(_) => None, }; let empty_hashset = HashSet::default(); @@ -2273,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 @@ -2359,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 84c9a9513..4286895ee 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, - TallyTransaction, Transaction, VTTransaction, VTTransactionBody, + StakeTransaction, TallyTransaction, Transaction, UnstakeTransaction, VTTransaction, + VTTransactionBody, }, utxo_pool::UtxoSelectionStrategy, }; @@ -121,6 +122,7 @@ pub struct Account { pub internal: ExtendedSK, } +#[derive(Debug)] pub struct WalletData { pub id: String, pub name: Option, @@ -322,6 +324,8 @@ pub enum TransactionHelper { Reveal(RevealTransaction), Tally(TallyTransaction), Mint(MintTransaction), + Stake(StakeTransaction), + Unstake(UnstakeTransaction), } impl From for TransactionHelper { @@ -337,6 +341,10 @@ 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), + Transaction::Unstake(unstaketransaction) => { + TransactionHelper::Unstake(unstaketransaction) + } } } } @@ -354,6 +362,10 @@ 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), + TransactionHelper::Unstake(unstaketransaction) => { + Transaction::Unstake(unstaketransaction) + } } } } @@ -413,7 +425,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) } }