diff --git a/Cargo.lock b/Cargo.lock index d4a58fb79..ff08f0229 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -412,6 +412,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + [[package]] name = "byteorder" version = "1.4.3" @@ -496,6 +502,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.26" @@ -555,6 +567,12 @@ dependencies = [ "bitflags", ] +[[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" @@ -1564,6 +1582,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" @@ -1707,6 +1739,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -2303,7 +2344,7 @@ dependencies = [ "num-complex", "num-integer", "num-iter", - "num-rational", + "num-rational 0.4.1", "num-traits", ] @@ -2358,6 +2399,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" @@ -2953,6 +3005,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" @@ -4967,11 +5029,12 @@ dependencies = [ "failure", "futures 0.3.28", "hex", - "itertools", + "itertools 0.8.2", "lazy_static", "log 0.4.19", "num-format", "prettytable-rs", + "qrcode", "sentry", "serde", "serde_json", @@ -5083,7 +5146,7 @@ dependencies = [ "failure", "futures 0.3.28", "hex", - "itertools", + "itertools 0.8.2", "lazy_static", "log 0.4.19", "num-traits", @@ -5149,7 +5212,7 @@ dependencies = [ "futures-util", "glob", "hex", - "itertools", + "itertools 0.8.2", "jsonrpc-core 18.0.0", "jsonrpc-pubsub 18.0.0", "log 0.4.19", @@ -5272,7 +5335,7 @@ dependencies = [ "bencher", "failure", "hex", - "itertools", + "itertools 0.11.0", "log 0.4.19", "url", "witnet_config", @@ -5296,7 +5359,7 @@ dependencies = [ "futures 0.3.28", "futures-util", "hex", - "itertools", + "itertools 0.8.2", "jsonrpc-core 15.1.0", "jsonrpc-pubsub 15.1.0", "log 0.4.19", diff --git a/Cargo.toml b/Cargo.toml index a2c92163b..055eead12 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/defaults.rs b/config/src/defaults.rs index 7fdb725ba..fabeedd85 100644 --- a/config/src/defaults.rs +++ b/config/src/defaults.rs @@ -486,6 +486,13 @@ pub const PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO: u64 = 125; // TODO: modify the value directly in ConsensusConstants pub const PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE: u32 = 13440; +/// Maximum weight units that a block can devote to `StakeTransaction`s. +pub const PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT: u32 = 10_000_000; + +/// Minimum amount of nanoWits that a `StakeTransaction` can add, and minimum amount that can be +/// left in stake by an `UnstakeTransaction`. +pub const PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS: u64 = 10_000_000_000_000; + /// Struct that will implement all the development defaults pub struct Development; diff --git a/data_structures/Cargo.toml b/data_structures/Cargo.toml index b8cc84375..be50ed341 100644 --- a/data_structures/Cargo.toml +++ b/data_structures/Cargo.toml @@ -51,3 +51,7 @@ rand_distr = "0.4.3" [[bench]] name = "sort_active_identities" harness = false + +[[bench]] +name = "staking" +harness = false diff --git a/data_structures/benches/staking.rs b/data_structures/benches/staking.rs new file mode 100644 index 000000000..8bbee63f8 --- /dev/null +++ b/data_structures/benches/staking.rs @@ -0,0 +1,85 @@ +#[macro_use] +extern crate bencher; +use bencher::Bencher; +use rand::Rng; +use witnet_data_structures::staking::prelude::*; + +fn populate(b: &mut Bencher) { + let mut stakes = Stakes::::default(); + let mut i = 1; + + b.iter(|| { + let address = format!("{i}"); + let coins = i; + let epoch = i; + stakes.add_stake(address, coins, epoch).unwrap(); + + i += 1; + }); +} + +fn rank(b: &mut Bencher) { + let mut stakes = Stakes::::default(); + let mut i = 1; + + let stakers = 100_000; + let rf = 10; + + let mut rng = rand::thread_rng(); + + loop { + let coins = i; + let epoch = i; + let address = format!("{}", rng.gen::()); + + stakes.add_stake(address, coins, epoch).unwrap(); + + i += 1; + + if i == stakers { + break; + } + } + + b.iter(|| { + let rank = stakes.rank(Capability::Mining, i); + let mut top = rank.take(usize::try_from(stakers / rf).unwrap()); + let _first = top.next(); + let _last = top.last(); + + i += 1; + }) +} + +fn query_power(b: &mut Bencher) { + let mut stakes = Stakes::::default(); + let mut i = 1; + + let stakers = 100_000; + + loop { + let coins = i; + let epoch = i; + let address = format!("{i}"); + + stakes.add_stake(address, coins, epoch).unwrap(); + + i += 1; + + if i == stakers { + break; + } + } + + i = 1; + + b.iter(|| { + let address = format!("{i}"); + let _power = stakes.query_power(&address, Capability::Mining, i); + + i += 1; + }) +} + +benchmark_main!(benches); +benchmark_group!(benches, populate, rank, query_power); diff --git a/data_structures/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..80cd8257b --- /dev/null +++ b/data_structures/src/capabilities.rs @@ -0,0 +1,44 @@ +#[repr(u8)] +#[derive(Clone, Copy, Debug)] +pub enum Capability { + /// The base block mining and superblock voting capability + Mining = 0, + /// The universal HTTP GET / HTTP POST / WIP-0019 RNG capability + Witnessing = 1, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct CapabilityMap +where + T: Default, +{ + pub mining: T, + pub witnessing: T, +} + +impl CapabilityMap +where + T: Copy + Default, +{ + #[inline] + pub fn get(&self, capability: Capability) -> T { + match capability { + Capability::Mining => self.mining, + Capability::Witnessing => self.witnessing, + } + } + + #[inline] + pub fn update(&mut self, capability: Capability, value: T) { + match capability { + Capability::Mining => self.mining = value, + Capability::Witnessing => self.witnessing = value, + } + } + + #[inline] + pub fn update_all(&mut self, value: T) { + self.mining = value; + self.witnessing = value; + } +} diff --git a/data_structures/src/chain/mod.rs b/data_structures/src/chain/mod.rs index a6a1de429..dc23868f9 100644 --- a/data_structures/src/chain/mod.rs +++ b/data_structures/src/chain/mod.rs @@ -1,9 +1,3 @@ -/// Keeps track of priority being used by transactions included in recent blocks, and provides -/// methods for estimating sensible priority values for future transactions. -pub mod priority; -/// Contains all TAPI related structures and business logic -pub mod tapi; - use std::{ cell::{Cell, RefCell}, cmp::Ordering, @@ -20,8 +14,9 @@ use bls_signatures_rs::{bn256, bn256::Bn256, MultiSignature}; use failure::Fail; use futures::future::BoxFuture; use ordered_float::OrderedFloat; -use partial_struct::PartialStruct; use serde::{Deserialize, Serialize}; + +use partial_struct::PartialStruct; use witnet_crypto::{ hash::{calculate_sha256, Sha256}, key::ExtendedSK, @@ -41,12 +36,13 @@ use crate::{ DataRequestError, EpochCalculationError, OutputPointerParseError, Secp256k1ConversionError, TransactionError, }, - get_environment, - proto::{schema::witnet, ProtobufConvert}, + get_environment, get_protocol_version, + proto::{versioning::Versioned, ProtobufConvert}, 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, @@ -55,6 +51,12 @@ use crate::{ vrf::{BlockEligibilityClaim, DataRequestEligibilityClaim}, }; +/// Keeps track of priority being used by transactions included in recent blocks, and provides +/// methods for estimating sensible priority values for future transactions. +pub mod priority; +/// Contains all TAPI related structures and business logic +pub mod tapi; + /// Define how the different structures should be hashed. pub trait Hashable { /// Calculate the hash of `self` @@ -156,7 +158,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 +361,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 +374,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 +388,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 +404,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 +418,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 +450,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 +476,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 +512,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 +547,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 +560,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 +593,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 +645,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 +668,9 @@ impl Hashable for BlockHeader { impl MemoizedHashable for Block { fn hashable_bytes(&self) -> Vec { - self.block_header.to_pb_bytes().unwrap() + self.block_header + .to_versioned_pb_bytes(get_protocol_version()) + .unwrap() } fn memoized_hash(&self) -> &MemoHash { @@ -652,7 +708,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 +724,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 +738,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 +770,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 +782,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 +915,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 +973,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), @@ -947,7 +1009,7 @@ impl Signature { /// ECDSA (over secp256k1) signature #[derive(Debug, Default, Eq, PartialEq, Clone, Hash, Serialize, Deserialize, ProtobufConvert)] -#[protobuf_convert(pb = "witnet::Secp256k1Signature")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::Secp256k1Signature")] pub struct Secp256k1Signature { /// The signature serialized in DER pub der: Vec, @@ -1039,7 +1101,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), @@ -1169,7 +1231,7 @@ pub type SHA256 = [u8; 32]; /// /// It is the first 20 bytes of the SHA256 hash of the PublicKey. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, ProtobufConvert, Ord, PartialOrd)] -#[protobuf_convert(pb = "witnet::PublicKeyHash")] +#[protobuf_convert(pb = "crate::proto::schema::witnet::PublicKeyHash")] pub struct PublicKeyHash { pub(crate) hash: [u8; 20], } @@ -1307,7 +1369,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 +1391,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 +1403,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 +1481,44 @@ impl DataRequestOutput { } } +#[derive(Debug, Default, Eq, PartialEq, Clone, Serialize, Deserialize, ProtobufConvert, Hash)] +#[protobuf_convert(pb = "crate::proto::schema::witnet::StakeOutput")] +pub struct StakeOutput { + pub value: u64, + pub authorization: KeyedSignature, +} + +impl StakeOutput { + #[inline] + pub fn weight(&self) -> u32 { + crate::transaction::STAKE_OUTPUT_WEIGHT + } +} + +pub enum Output { + DataRequest(DataRequestOutput), + Stake(StakeOutput), + ValueTransfer(ValueTransferOutput), +} + +impl Output { + pub fn value(&self) -> Result { + match self { + Output::DataRequest(output) => output.checked_total_value(), + Output::Stake(output) => Ok(output.value), + Output::ValueTransfer(output) => Ok(output.value), + } + } + + pub fn weight(&self) -> u32 { + match self { + Output::DataRequest(output) => output.weight(), + Output::Stake(output) => output.weight(), + Output::ValueTransfer(output) => output.weight(), + } + } +} + /// Information about the total supply #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SupplyInfo { @@ -1436,7 +1548,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, @@ -1485,6 +1597,15 @@ impl PublicKey { } } + pub fn from_str(serialized: &str) -> Self { + let mut pk = hex::decode(serialized).unwrap(); + pk.resize(33, 0); + let mut array_bytes = [0u8; 33]; + array_bytes.copy_from_slice(&pk[..33]); + + Self::from_bytes(array_bytes) + } + /// Returns the PublicKeyHash related to the PublicKey pub fn pkh(&self) -> PublicKeyHash { PublicKeyHash::from_public_key(self) @@ -1511,7 +1632,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 +1651,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 +1659,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 +1776,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 +1813,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 +1990,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 +2014,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 +2040,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 +2077,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 +2135,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 +2159,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 +2188,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 +2241,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() } @@ -2121,6 +2280,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. @@ -2137,12 +2298,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(); @@ -2156,7 +2325,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. @@ -2201,6 +2376,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(); @@ -2242,6 +2459,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)), } } @@ -2334,6 +2553,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. @@ -2468,6 +2734,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); } } } @@ -2543,6 +2810,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) { @@ -2609,11 +2943,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; @@ -2736,6 +3070,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: {:?}", @@ -2784,6 +3159,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: @@ -2802,6 +3195,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) @@ -2835,6 +3229,7 @@ impl TransactionsPool { /// pool.vt_retain(|tx| tx.body.outputs.len()>0); /// assert_eq!(pool.vt_len(), 1); /// ``` + // TODO: dead code pub fn vt_retain(&mut self, mut f: F) where F: FnMut(&VTTransaction) -> bool, @@ -2877,6 +3272,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 @@ -2901,6 +3306,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, } @@ -2914,12 +3325,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, @@ -2982,7 +3397,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), @@ -3007,6 +3422,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 @@ -4020,7 +4439,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, )) } @@ -4051,6 +4470,7 @@ mod tests { }; use crate::{ + proto::versioning::{ProtocolVersion, VersionedHashable}, superblock::{mining_build_superblock, ARSIdentities}, transaction::{CommitTransactionBody, RevealTransactionBody, VTTransactionBody}, }; @@ -4124,7 +4544,7 @@ mod tests { .iter() .map(|input| { DRTransaction::new( - DRTransactionBody::new(vec![*input], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![*input], DataRequestOutput::default(), vec![]), vec![], ) }) @@ -4159,7 +4579,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_6).to_string(), + expected + ); + let expected = "29ef68357a5c861b9dbe043d351a28472ca450edcda25de4c9b80a4560a28c0f"; + assert_eq!( + block.versioned_hash(ProtocolVersion::V1_7).to_string(), + expected + ); + let expected = "29ef68357a5c861b9dbe043d351a28472ca450edcda25de4c9b80a4560a28c0f"; + assert_eq!( + block.versioned_hash(ProtocolVersion::V2_0).to_string(), + expected + ); } #[test] @@ -4598,14 +5031,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![], ); @@ -4675,14 +5108,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![], ); @@ -4776,11 +5209,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![], ); @@ -4842,7 +5275,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 @@ -5037,12 +5470,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![], )) @@ -5280,7 +5713,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 { @@ -5318,7 +5751,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 { @@ -5333,7 +5766,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 { @@ -5348,7 +5781,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 { @@ -5440,7 +5873,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 { @@ -5480,7 +5913,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 { @@ -5519,7 +5952,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 { @@ -5557,7 +5990,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 { @@ -5595,7 +6028,7 @@ mod tests { witnesses: 2, ..Default::default() }; - let drb = DRTransactionBody::new(vec![], vec![], dro); + let drb = DRTransactionBody::new(vec![], dro, vec![]); let drt = DRTransaction::new( drb, vec![KeyedSignature { diff --git a/data_structures/src/data_request.rs b/data_structures/src/data_request.rs index fc3823037..4e74ad0b8 100644 --- a/data_structures/src/data_request.rs +++ b/data_structures/src/data_request.rs @@ -742,7 +742,7 @@ mod tests { ..DataRequestInfo::default() }; let dr_transaction = DRTransaction::new( - DRTransactionBody::new(vec![Input::default()], vec![], DataRequestOutput::default()), + DRTransactionBody::new(vec![Input::default()], DataRequestOutput::default(), vec![]), vec![KeyedSignature::default()], ); let dr_pointer = dr_transaction.hash(); @@ -777,7 +777,7 @@ mod tests { ..DataRequestOutput::default() }; let dr_transaction = DRTransaction::new( - DRTransactionBody::new(vec![Input::default()], vec![], dr_output), + DRTransactionBody::new(vec![Input::default()], dr_output, vec![]), vec![KeyedSignature::default()], ); let dr_pointer = dr_transaction.hash(); @@ -812,7 +812,7 @@ mod tests { ..DataRequestOutput::default() }; let dr_transaction = DRTransaction::new( - DRTransactionBody::new(vec![Input::default()], vec![], dr_output), + DRTransactionBody::new(vec![Input::default()], dr_output, vec![]), vec![KeyedSignature::default()], ); let dr_pointer = dr_transaction.hash(); diff --git a/data_structures/src/error.rs b/data_structures/src/error.rs index 1189507ce..f0bd23232 100644 --- a/data_structures/src/error.rs +++ b/data_structures/src/error.rs @@ -278,6 +278,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 ({})", + min_stake, stake + )] + StakeBelowMinimum { min_stake: u64, stake: u64 }, + /// Unstaking more than the total staked + #[fail( + display = "Unstaking ({}) more than the total staked ({})", + unstake, stake + )] + UnstakingMoreThanStaked { stake: u64, unstake: u64 }, + /// An stake output with zero value does not make sense + #[fail(display = "Transaction {} has a zero value stake output", tx_hash)] + ZeroValueStakeOutput { tx_hash: Hash }, + /// Invalid unstake signature + #[fail( + display = "Invalid unstake signature: ({}), withdrawal ({}), operator ({})", + signature, withdrawal, operator + )] + InvalidUnstakeSignature { + signature: Hash, + withdrawal: Hash, + operator: Hash, + }, + /// Invalid unstake time_lock + #[fail( + display = "The unstake timelock: ({}) is lower than the minimum unstaking delay ({})", + time_lock, unstaking_delay_seconds + )] + InvalidUnstakeTimelock { + time_lock: u64, + unstaking_delay_seconds: u32, + }, #[fail( display = "The reward-to-collateral ratio for this data request is {}, but must be equal or less than {}", reward_collateral_ratio, required_reward_collateral_ratio @@ -401,6 +435,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 {}", diff --git a/data_structures/src/lib.rs b/data_structures/src/lib.rs index 0b83c5cc9..c57f2386a 100644 --- a/data_structures/src/lib.rs +++ b/data_structures/src/lib.rs @@ -13,7 +13,7 @@ #[macro_use] extern crate protobuf_convert; -use crate::chain::Environment; +use crate::{chain::Environment, proto::versioning::ProtocolVersion}; use lazy_static::lazy_static; use std::sync::RwLock; @@ -38,6 +38,9 @@ pub mod fee; /// Module containing data_request structures pub mod data_request; +/// Module containing data structures for the staking functionality +pub mod staking; + /// Module containing superblock structures pub mod superblock; @@ -69,6 +72,9 @@ mod serialization_helpers; /// Provides convenient constants, structs and methods for handling values denominated in Wit. pub mod wit; +/// Provides support for segmented protocol capabilities. +pub mod capabilities; + lazy_static! { /// Environment in which we are running: mainnet or testnet. /// This is used for Bech32 serialization. @@ -76,6 +82,9 @@ lazy_static! { // can work without having to manually set the environment. // The default environment will also be used in tests. static ref ENVIRONMENT: RwLock = RwLock::new(Environment::Mainnet); + /// Protocol version that we are running. + /// default to legacy for now — it's the v2 bootstrapping module's responsibility to upgrade it. + static ref PROTOCOL_VERSION: RwLock = RwLock::new(ProtocolVersion::V1_6); } /// Environment in which we are running: mainnet or testnet. @@ -108,6 +117,34 @@ pub fn set_environment(environment: Environment) { } } +/// Protocol version that we are running. +pub fn get_protocol_version() -> ProtocolVersion { + // This unwrap is safe as long as the lock is not poisoned. + // The lock can only become poisoned when a writer panics. + // The only writer is the one used in `set_environment`, which should only + // be used during initialization. + *PROTOCOL_VERSION.read().unwrap() +} + +/// Set the protocol version that we are running. +/// This function should only be called once during initialization. +// Changing the environment in tests is not supported, as it can cause spurious failures: +// multiple tests can run in parallel and some tests might fail when the environment changes. +// But if you need to change the environment in some test, just create a separate thread-local +// variable and mock get and set. +#[cfg(not(test))] +pub fn set_protocol_version(protocol_version: ProtocolVersion) { + match PROTOCOL_VERSION.write() { + Ok(mut x) => { + *x = protocol_version; + log::debug!("Protocol version set to {}", protocol_version); + } + Err(e) => { + log::error!("Failed to set protocol version: {}", e); + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -118,4 +155,12 @@ mod tests { // addresses serialized as Bech32 will fail assert_eq!(get_environment(), Environment::Mainnet); } + + #[test] + fn default_protocol_version() { + // If this default changes before the transition to V2 is complete, almost everything will + // break because data structures change schema and, serialization changes and hash + // derivation breaks too + assert_eq!(get_protocol_version(), ProtocolVersion::V1_6); + } } diff --git a/data_structures/src/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..622d9c920 --- /dev/null +++ b/data_structures/src/proto/versioning.rs @@ -0,0 +1,523 @@ +use failure::{Error, Fail}; +use protobuf::Message as _; +use std::fmt; +use std::fmt::Formatter; + +use crate::proto::schema::witnet::SuperBlock; +use crate::{ + chain::Hash, + proto::{ + schema::witnet::{ + Block, Block_BlockHeader, Block_BlockHeader_BlockMerkleRoots, Block_BlockTransactions, + LegacyBlock, LegacyBlock_LegacyBlockHeader, + LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots, + LegacyBlock_LegacyBlockTransactions, LegacyMessage, LegacyMessage_LegacyCommand, + LegacyMessage_LegacyCommand_oneof_kind, Message_Command, Message_Command_oneof_kind, + }, + ProtobufConvert, + }, + transaction::MemoizedHashable, + types::Message, +}; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ProtocolVersion { + /// The original Witnet protocol. + V1_6, + /// The transitional protocol based on 1.x but with staking enabled. + V1_7, + /// The final Witnet 2.0 protocol. + V2_0, +} + +impl fmt::Display for ProtocolVersion { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let s = match self { + ProtocolVersion::V1_6 => "v1.6 (legacy)", + ProtocolVersion::V1_7 => "v1.7 (transitional)", + ProtocolVersion::V2_0 => "v2.0 (final)", + }; + + f.write_str(s) + } +} + +pub trait Versioned: ProtobufConvert { + type LegacyType: protobuf::Message; + + /// Turn a protobuf-compatible data structure into a versioned form of itself. + /// + /// For truly versionable data structures, this method should be implemented manually. For other + /// data structures, the trait's own blanket implementation should be fine. + fn to_versioned_pb( + &self, + _version: ProtocolVersion, + ) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + Ok(Box::new(self.to_pb())) + } + /// Turn a protobuf-compaitble data structures into its serialized protobuf bytes. + /// This blanket implementation should normally not be overriden. + fn to_versioned_pb_bytes(&self, version: ProtocolVersion) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + Ok(self.to_versioned_pb(version)?.write_to_bytes().unwrap()) + } + + /// Constructs an instance of this data structure based on a protobuf instance of its legacy + /// schema. + fn from_versioned_pb(legacy: Self::LegacyType) -> Result + where + Self: From, + { + Ok(Self::from(legacy)) + } + + /// Tries to deserialize a data structure from its regular protobuf schema, and if it fails, it + /// retries with its legacy schema. + fn from_versioned_pb_bytes(bytes: &[u8]) -> Result + where + ::ProtoStruct: protobuf::Message, + Self: From, + { + let mut current = Self::ProtoStruct::new(); + let direct_attempt = current + .merge_from_bytes(bytes) + .map_err(|e| Error::from_boxed_compat(Box::new(e.compat()))) + .and_then(|_| Self::from_pb(current)); + + if direct_attempt.is_ok() { + direct_attempt + } else { + let mut legacy = Self::LegacyType::new(); + legacy.merge_from_bytes(bytes)?; + + Ok(Self::from(legacy)) + } + } +} + +impl Versioned for crate::chain::BlockMerkleRoots { + type LegacyType = LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots; + + fn to_versioned_pb( + &self, + version: ProtocolVersion, + ) -> Result, Error> { + use ProtocolVersion::*; + + let mut pb = self.to_pb(); + + let versioned: Box = match version { + // Legacy merkle roots need to get rearranged + V1_6 => Box::new(Self::LegacyType::from(pb)), + // Transition merkle roots need no transformation + V1_7 => Box::new(pb), + // Final merkle roots need to drop the mint hash + V2_0 => { + pb.set_mint_hash(Default::default()); + + Box::new(pb) + } + }; + + Ok(versioned) + } +} + +impl Versioned for crate::chain::BlockHeader { + type LegacyType = LegacyBlock_LegacyBlockHeader; + + fn to_versioned_pb( + &self, + version: ProtocolVersion, + ) -> Result, Error> { + use ProtocolVersion::*; + + let pb = self.to_pb(); + + let versioned: Box = match version { + // Legacy block headers need to be rearranged + V1_6 => Box::new(Self::LegacyType::from(pb)), + // All other block headers need no transformation + V1_7 | V2_0 => Box::new(pb), + }; + + Ok(versioned) + } +} + +impl Versioned for crate::chain::SuperBlock { + type LegacyType = SuperBlock; + + fn to_versioned_pb_bytes(&self, _version: ProtocolVersion) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + Ok(self.hashable_bytes()) + } +} + +impl Versioned for crate::chain::Block { + type LegacyType = LegacyBlock; + + fn to_versioned_pb( + &self, + _version: ProtocolVersion, + ) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + Ok(Box::new(Self::LegacyType::from(self.to_pb()))) + } +} + +impl Versioned for Message { + type LegacyType = LegacyMessage; + + fn to_versioned_pb(&self, version: ProtocolVersion) -> Result, Error> + where + ::ProtoStruct: protobuf::Message, + { + use ProtocolVersion::*; + + let pb = self.to_pb(); + + let versioned: Box = match version { + V1_6 => Box::new(Self::LegacyType::from(pb)), + V1_7 | V2_0 => Box::new(pb), + }; + + Ok(versioned) + } +} + +pub trait AutoVersioned: ProtobufConvert {} + +impl AutoVersioned for crate::chain::BlockHeader {} +impl AutoVersioned for crate::chain::SuperBlock {} + +pub trait VersionedHashable { + fn versioned_hash(&self, version: ProtocolVersion) -> Hash; +} + +impl VersionedHashable for T +where + T: AutoVersioned + Versioned, + ::ProtoStruct: protobuf::Message, +{ + fn versioned_hash(&self, version: ProtocolVersion) -> Hash { + witnet_crypto::hash::calculate_sha256(&self.to_versioned_pb_bytes(version).unwrap()).into() + } +} + +impl VersionedHashable for crate::chain::Block { + fn versioned_hash(&self, version: ProtocolVersion) -> Hash { + self.block_header.versioned_hash(version) + } +} + +impl From + for LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots +{ + fn from(header: Block_BlockHeader_BlockMerkleRoots) -> Self { + let mut legacy = LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots::new(); + legacy.set_mint_hash(header.get_mint_hash().clone()); + legacy.vt_hash_merkle_root = header.vt_hash_merkle_root; + legacy.dr_hash_merkle_root = header.dr_hash_merkle_root; + legacy.commit_hash_merkle_root = header.commit_hash_merkle_root; + legacy.reveal_hash_merkle_root = header.reveal_hash_merkle_root; + legacy.tally_hash_merkle_root = header.tally_hash_merkle_root; + + legacy + } +} + +impl From + for Block_BlockHeader_BlockMerkleRoots +{ + fn from( + LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots { + mint_hash, + vt_hash_merkle_root, + dr_hash_merkle_root, + commit_hash_merkle_root, + reveal_hash_merkle_root, + tally_hash_merkle_root, + .. + }: LegacyBlock_LegacyBlockHeader_LegacyBlockMerkleRoots, + ) -> Self { + let mut header = Block_BlockHeader_BlockMerkleRoots::new(); + header.mint_hash = mint_hash; + header.vt_hash_merkle_root = vt_hash_merkle_root; + header.dr_hash_merkle_root = dr_hash_merkle_root; + header.commit_hash_merkle_root = commit_hash_merkle_root; + header.reveal_hash_merkle_root = reveal_hash_merkle_root; + header.tally_hash_merkle_root = tally_hash_merkle_root; + header.set_stake_hash_merkle_root(Hash::default().to_pb()); + header.set_unstake_hash_merkle_root(Hash::default().to_pb()); + + header + } +} + +impl From for LegacyBlock_LegacyBlockHeader { + fn from( + Block_BlockHeader { + signals, + beacon, + merkle_roots, + proof, + bn256_public_key, + .. + }: Block_BlockHeader, + ) -> Self { + let mut legacy = LegacyBlock_LegacyBlockHeader::new(); + legacy.signals = signals; + legacy.beacon = beacon; + legacy.merkle_roots = merkle_roots.map(Into::into); + legacy.proof = proof; + legacy.bn256_public_key = bn256_public_key; + + legacy + } +} + +impl From for Block_BlockHeader { + fn from( + LegacyBlock_LegacyBlockHeader { + signals, + beacon, + merkle_roots, + proof, + bn256_public_key, + .. + }: LegacyBlock_LegacyBlockHeader, + ) -> Self { + let mut header = Block_BlockHeader::new(); + header.signals = signals; + header.beacon = beacon; + header.merkle_roots = merkle_roots.map(Into::into); + header.proof = proof; + header.bn256_public_key = bn256_public_key; + + header + } +} + +impl From for LegacyBlock_LegacyBlockTransactions { + fn from( + Block_BlockTransactions { + mint, + value_transfer_txns, + data_request_txns, + commit_txns, + reveal_txns, + tally_txns, + .. + }: Block_BlockTransactions, + ) -> Self { + let mut legacy = LegacyBlock_LegacyBlockTransactions::new(); + legacy.mint = mint; + legacy.value_transfer_txns = value_transfer_txns; + legacy.data_request_txns = data_request_txns; + legacy.commit_txns = commit_txns; + legacy.reveal_txns = reveal_txns; + legacy.tally_txns = tally_txns; + + legacy + } +} + +impl From for Block_BlockTransactions { + fn from( + LegacyBlock_LegacyBlockTransactions { + mint, + value_transfer_txns, + data_request_txns, + commit_txns, + reveal_txns, + tally_txns, + .. + }: LegacyBlock_LegacyBlockTransactions, + ) -> Self { + let mut txns = Block_BlockTransactions::new(); + txns.mint = mint; + txns.value_transfer_txns = value_transfer_txns; + txns.data_request_txns = data_request_txns; + txns.commit_txns = commit_txns; + txns.reveal_txns = reveal_txns; + txns.tally_txns = tally_txns; + txns.stake_txns = vec![].into(); + txns.unstake_txns = vec![].into(); + + txns + } +} + +impl From for LegacyBlock { + fn from( + Block { + block_header, + block_sig, + txns, + .. + }: Block, + ) -> Self { + let mut legacy = LegacyBlock::new(); + legacy.block_header = block_header.map(Into::into); + legacy.block_sig = block_sig; + legacy.txns = txns.map(Into::into); + + legacy + } +} + +impl From for Block { + fn from( + LegacyBlock { + block_header, + block_sig, + txns, + .. + }: LegacyBlock, + ) -> Self { + let mut block = Block::new(); + block.block_header = block_header.map(Into::into); + block.block_sig = block_sig; + block.txns = txns.map(Into::into); + + block + } +} + +impl From for LegacyMessage_LegacyCommand_oneof_kind { + fn from(value: Message_Command_oneof_kind) -> Self { + match value { + Message_Command_oneof_kind::Version(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Version(x) + } + Message_Command_oneof_kind::Verack(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Verack(x) + } + Message_Command_oneof_kind::GetPeers(x) => { + LegacyMessage_LegacyCommand_oneof_kind::GetPeers(x) + } + Message_Command_oneof_kind::Peers(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Peers(x) + } + Message_Command_oneof_kind::Block(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Block(x.into()) + } + Message_Command_oneof_kind::InventoryAnnouncement(x) => { + LegacyMessage_LegacyCommand_oneof_kind::InventoryAnnouncement(x) + } + Message_Command_oneof_kind::InventoryRequest(x) => { + LegacyMessage_LegacyCommand_oneof_kind::InventoryRequest(x) + } + Message_Command_oneof_kind::LastBeacon(x) => { + LegacyMessage_LegacyCommand_oneof_kind::LastBeacon(x) + } + Message_Command_oneof_kind::Transaction(x) => { + LegacyMessage_LegacyCommand_oneof_kind::Transaction(x) + } + Message_Command_oneof_kind::SuperBlockVote(x) => { + LegacyMessage_LegacyCommand_oneof_kind::SuperBlockVote(x) + } + Message_Command_oneof_kind::SuperBlock(x) => { + LegacyMessage_LegacyCommand_oneof_kind::SuperBlock(x) + } + } + } +} + +impl From for Message_Command_oneof_kind { + fn from(legacy: LegacyMessage_LegacyCommand_oneof_kind) -> Self { + match legacy { + LegacyMessage_LegacyCommand_oneof_kind::Version(x) => { + Message_Command_oneof_kind::Version(x) + } + LegacyMessage_LegacyCommand_oneof_kind::Verack(x) => { + Message_Command_oneof_kind::Verack(x) + } + LegacyMessage_LegacyCommand_oneof_kind::GetPeers(x) => { + Message_Command_oneof_kind::GetPeers(x) + } + LegacyMessage_LegacyCommand_oneof_kind::Peers(x) => { + Message_Command_oneof_kind::Peers(x) + } + LegacyMessage_LegacyCommand_oneof_kind::Block(x) => { + Message_Command_oneof_kind::Block(x.into()) + } + LegacyMessage_LegacyCommand_oneof_kind::InventoryAnnouncement(x) => { + Message_Command_oneof_kind::InventoryAnnouncement(x) + } + LegacyMessage_LegacyCommand_oneof_kind::InventoryRequest(x) => { + Message_Command_oneof_kind::InventoryRequest(x) + } + LegacyMessage_LegacyCommand_oneof_kind::LastBeacon(x) => { + Message_Command_oneof_kind::LastBeacon(x) + } + LegacyMessage_LegacyCommand_oneof_kind::Transaction(x) => { + Message_Command_oneof_kind::Transaction(x) + } + LegacyMessage_LegacyCommand_oneof_kind::SuperBlockVote(x) => { + Message_Command_oneof_kind::SuperBlockVote(x) + } + LegacyMessage_LegacyCommand_oneof_kind::SuperBlock(x) => { + Message_Command_oneof_kind::SuperBlock(x) + } + } + } +} + +impl From for LegacyMessage_LegacyCommand { + fn from(Message_Command { kind, .. }: Message_Command) -> Self { + let mut legacy = LegacyMessage_LegacyCommand::new(); + legacy.kind = kind.map(Into::into); + + legacy + } +} + +impl From for Message_Command { + fn from(LegacyMessage_LegacyCommand { kind, .. }: LegacyMessage_LegacyCommand) -> Self { + let mut command = Message_Command::new(); + command.kind = kind.map(Into::into); + + command + } +} + +impl From for LegacyMessage { + fn from( + crate::proto::schema::witnet::Message { magic, kind, .. }: crate::proto::schema::witnet::Message, + ) -> Self { + let mut legacy = LegacyMessage::new(); + legacy.magic = magic; + legacy.kind = kind.map(Into::into); + + legacy + } +} + +impl From for crate::proto::schema::witnet::Message { + fn from(LegacyMessage { magic, kind, .. }: LegacyMessage) -> Self { + let mut message = crate::proto::schema::witnet::Message::new(); + message.magic = magic; + message.kind = kind.map(Into::into); + + message + } +} + +impl From for Message { + fn from(legacy: LegacyMessage) -> Self { + let pb = crate::proto::schema::witnet::Message::from(legacy); + + Message::from_pb(pb).unwrap() + } +} diff --git a/data_structures/src/staking/aux.rs b/data_structures/src/staking/aux.rs new file mode 100644 index 000000000..424158164 --- /dev/null +++ b/data_structures/src/staking/aux.rs @@ -0,0 +1,37 @@ +use std::rc::Rc; +use std::sync::RwLock; + +use super::prelude::*; + +/// Type alias for a reference-counted and read-write-locked instance of `Stake`. +pub type SyncStake = Rc>>; + +/// The resulting type for all the fallible functions in this module. +pub type Result = + std::result::Result>; + +/// Couples an amount of coins and an address together. This is to be used in `Stakes` as the index +/// of the `by_coins` index.. +#[derive(Eq, Ord, PartialEq, PartialOrd)] +pub struct CoinsAndAddress { + /// An amount of coins. + pub coins: Coins, + /// The address of a staker. + pub address: Address, +} + +/// Allows telling the `census` method in `Stakes` to source addresses from its internal `by_coins` +/// following different strategies. +#[repr(u8)] +#[derive(Clone, Copy, Debug)] +pub enum CensusStrategy { + /// Retrieve all addresses, ordered by decreasing power. + All = 0, + /// Retrieve every Nth address, ordered by decreasing power. + StepBy(usize) = 1, + /// Retrieve the most powerful N addresses, ordered by decreasing power. + Take(usize) = 2, + /// Retrieve a total of N addresses, evenly distributed from the index, ordered by decreasing + /// power. + Evenly(usize) = 3, +} diff --git a/data_structures/src/staking/constants.rs b/data_structures/src/staking/constants.rs new file mode 100644 index 000000000..d461b0560 --- /dev/null +++ b/data_structures/src/staking/constants.rs @@ -0,0 +1,2 @@ +/// A minimum stakeable amount needs to exist to prevent spamming of the tracker. +pub const MINIMUM_STAKEABLE_AMOUNT_WITS: u64 = 10_000; diff --git a/data_structures/src/staking/errors.rs b/data_structures/src/staking/errors.rs new file mode 100644 index 000000000..6169073f4 --- /dev/null +++ b/data_structures/src/staking/errors.rs @@ -0,0 +1,41 @@ +use std::sync::PoisonError; + +/// All errors related to the staking functionality. +#[derive(Debug, PartialEq)] +pub enum StakesError { + /// The amount of coins being staked or the amount that remains after unstaking is below the + /// minimum stakeable amount. + AmountIsBelowMinimum { + /// The number of coins being staked or remaining after staking. + amount: Coins, + /// The minimum stakeable amount. + minimum: Coins, + }, + /// Tried to query `Stakes` for information that belongs to the past. + EpochInThePast { + /// The Epoch being referred. + epoch: Epoch, + /// The latest Epoch. + latest: Epoch, + }, + /// An operation thrown an Epoch value that overflows. + EpochOverflow { + /// The computed Epoch value. + computed: u64, + /// The maximum Epoch. + maximum: Epoch, + }, + /// Tried to query `Stakes` for the address of a staker that is not registered in `Stakes`. + IdentityNotFound { + /// The unknown address. + identity: Address, + }, + /// Tried to obtain a lock on a write-locked piece of data that is already locked. + PoisonedLock, +} + +impl From> for StakesError { + fn from(_value: PoisonError) -> Self { + StakesError::PoisonedLock + } +} diff --git a/data_structures/src/staking/mod.rs b/data_structures/src/staking/mod.rs 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/simple.rs b/data_structures/src/staking/simple.rs new file mode 100644 index 000000000..e69de29bb diff --git a/data_structures/src/staking/stake.rs b/data_structures/src/staking/stake.rs new file mode 100644 index 000000000..38fff6ae4 --- /dev/null +++ b/data_structures/src/staking/stake.rs @@ -0,0 +1,122 @@ +use std::marker::PhantomData; + +use super::prelude::*; + +/// A data structure that keeps track of a staker's staked coins and the epochs for different +/// capabilities. +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub struct Stake +where + Address: Default, + Epoch: Default, +{ + /// An amount of staked coins. + pub coins: Coins, + /// The average epoch used to derive coin age for different capabilities. + pub epochs: CapabilityMap, + // These two phantom fields are here just for the sake of specifying generics. + phantom_address: PhantomData
, + phantom_power: PhantomData, +} + +impl Stake +where + Address: Default, + Coins: Copy + + From + + PartialOrd + + num_traits::Zero + + std::ops::Add + + std::ops::Sub + + std::ops::Mul + + std::ops::Mul, + Epoch: Copy + Default + num_traits::Saturating + std::ops::Sub, + Power: std::ops::Add + + std::ops::Div + + std::ops::Div, +{ + /// Increase the amount of coins staked by a certain staker. + /// + /// When adding stake: + /// - Amounts are added together. + /// - Epochs are weight-averaged, using the amounts as the weight. + /// + /// This type of averaging makes the entry equivalent to an unbounded record of all stake + /// additions and removals, without the overhead in memory and computation. + pub fn add_stake( + &mut self, + coins: Coins, + epoch: Epoch, + minimum_stakeable: Option, + ) -> Result { + // Make sure that the amount to be staked is equal or greater than the minimum + let minimum = minimum_stakeable.unwrap_or(Coins::from(MINIMUM_STAKEABLE_AMOUNT_WITS)); + if coins < minimum { + Err(StakesError::AmountIsBelowMinimum { + amount: coins, + minimum, + })?; + } + + let coins_before = self.coins; + let epoch_before = self.epochs.get(Capability::Mining); + + let product_before = coins_before * epoch_before; + let product_added = coins * epoch; + + let coins_after = coins_before + coins; + let epoch_after = (product_before + product_added) / coins_after; + + self.coins = coins_after; + self.epochs.update_all(epoch_after); + + Ok(coins_after) + } + + /// Construct a Stake entry from a number of coins and a capability map. This is only useful for + /// tests. + #[cfg(test)] + pub fn from_parts(coins: Coins, epochs: CapabilityMap) -> Self { + Self { + coins, + epochs, + phantom_address: Default::default(), + phantom_power: Default::default(), + } + } + + /// Derives the power of an identity in the network on a certain epoch from an entry. Most + /// normally, the epoch is the current epoch. + pub fn power(&self, capability: Capability, current_epoch: Epoch) -> Power { + self.coins * (current_epoch.saturating_sub(self.epochs.get(capability))) + } + + /// Remove a certain amount of staked coins. + pub fn remove_stake( + &mut self, + coins: Coins, + minimum_stakeable: Option, + ) -> Result { + let coins_after = self.coins.sub(coins); + + if coins_after > Coins::zero() { + let minimum = minimum_stakeable.unwrap_or(Coins::from(MINIMUM_STAKEABLE_AMOUNT_WITS)); + + if coins_after < minimum { + Err(StakesError::AmountIsBelowMinimum { + amount: coins_after, + minimum, + })?; + } + } + + self.coins = coins_after; + + Ok(self.coins) + } + + /// Set the epoch for a certain capability. Most normally, the epoch is the current epoch. + pub fn reset_age(&mut self, capability: Capability, current_epoch: Epoch) { + self.epochs.update(capability, current_epoch); + } +} diff --git a/data_structures/src/staking/stakes.rs b/data_structures/src/staking/stakes.rs new file mode 100644 index 000000000..13d0896d2 --- /dev/null +++ b/data_structures/src/staking/stakes.rs @@ -0,0 +1,466 @@ +use std::collections::btree_map::Entry; +use std::collections::BTreeMap; + +use itertools::Itertools; + +use super::prelude::*; + +/// The main data structure that provides the "stakes tracker" functionality. +/// +/// This structure holds indexes of stake entries. Because the entries themselves are reference +/// counted and write-locked, we can have as many indexes here as we need at a negligible cost. +#[derive(Default)] +pub struct Stakes +where + Address: Default, + Epoch: Default, +{ + /// A listing of all the stakers, indexed by their address. + by_address: BTreeMap>, + /// A listing of all the stakers, indexed by their coins and address. + /// + /// Because this uses a compound key to prevent duplicates, if we want to know which addresses + /// have staked a particular amount, we just need to run a range lookup on the tree. + by_coins: BTreeMap, SyncStake>, + /// The amount of coins that can be staked or can be left staked after unstaking. + minimum_stakeable: Option, +} + +impl Stakes +where + Address: Default, + Coins: Copy + + Default + + Ord + + From + + num_traits::Zero + + std::ops::Add + + std::ops::Sub + + std::ops::Mul + + std::ops::Mul, + Address: Clone + Ord + 'static, + Epoch: Copy + Default + num_traits::Saturating + std::ops::Sub, + Power: Copy + + Default + + Ord + + std::ops::Add + + std::ops::Div + + std::ops::Div + + 'static, +{ + /// Register a certain amount of additional stake for a certain address and epoch. + pub fn add_stake( + &mut self, + address: IA, + coins: Coins, + epoch: Epoch, + ) -> Result, Address, Coins, Epoch> + where + IA: Into
, + { + let address = address.into(); + let stake_arc = self.by_address.entry(address.clone()).or_default(); + + // Actually increase the number of coins + stake_arc + .write()? + .add_stake(coins, epoch, self.minimum_stakeable)?; + + // Update the position of the staker in the `by_coins` index + // If this staker was not indexed by coins, this will index it now + let key = CoinsAndAddress { + coins, + address: address.clone(), + }; + self.by_coins.remove(&key); + self.by_coins.insert(key, stake_arc.clone()); + + Ok(stake_arc.read()?.clone()) + } + + /// Obtain a list of stakers, conveniently ordered by one of several strategies. + /// + /// ## Strategies + /// + /// - `All`: retrieve all addresses, ordered by decreasing power. + /// - `StepBy`: retrieve every Nth address, ordered by decreasing power. + /// - `Take`: retrieve the most powerful N addresses, ordered by decreasing power. + /// - `Evenly`: retrieve a total of N addresses, evenly distributed from the index, ordered by + /// decreasing power. + pub fn census( + &self, + capability: Capability, + epoch: Epoch, + strategy: CensusStrategy, + ) -> Box> { + let iterator = self.rank(capability, epoch).map(|(address, _)| address); + + match strategy { + CensusStrategy::All => Box::new(iterator), + CensusStrategy::StepBy(step) => Box::new(iterator.step_by(step)), + CensusStrategy::Take(head) => Box::new(iterator.take(head)), + CensusStrategy::Evenly(count) => { + let collected = iterator.collect::>(); + let step = collected.len() / count; + + Box::new(collected.into_iter().step_by(step).take(count)) + } + } + } + + /// Tells what is the power of an identity in the network on a certain epoch. + pub fn query_power( + &self, + address: &Address, + capability: Capability, + epoch: Epoch, + ) -> Result { + Ok(self + .by_address + .get(address) + .ok_or(StakesError::IdentityNotFound { + identity: address.clone(), + })? + .read()? + .power(capability, epoch)) + } + + /// For a given capability, obtain the full list of stakers ordered by their power in that + /// capability. + pub fn rank( + &self, + capability: Capability, + current_epoch: Epoch, + ) -> impl Iterator + 'static { + self.by_coins + .iter() + .flat_map(move |(CoinsAndAddress { address, .. }, stake)| { + stake + .read() + .map(move |stake| (address.clone(), stake.power(capability, current_epoch))) + }) + .sorted_by_key(|(_, power)| *power) + .rev() + } + + /// Remove a certain amount of staked coins from a given identity at a given epoch. + pub fn remove_stake( + &mut self, + address: IA, + coins: Coins, + ) -> Result + where + IA: Into
, + { + let address = address.into(); + if let Entry::Occupied(mut by_address_entry) = self.by_address.entry(address.clone()) { + let (initial_coins, final_coins) = { + let mut stake = by_address_entry.get_mut().write()?; + + // Check the former amount of stake + let initial_coins = stake.coins; + + // Reduce the amount of stake + let final_coins = stake.remove_stake(coins, self.minimum_stakeable)?; + + (initial_coins, final_coins) + }; + + // No need to keep the entry if the stake has gone to zero + if final_coins.is_zero() { + by_address_entry.remove(); + self.by_coins.remove(&CoinsAndAddress { + coins: initial_coins, + address, + }); + } + + Ok(final_coins) + } else { + Err(StakesError::IdentityNotFound { identity: address }) + } + } + + /// Set the epoch for a certain address and capability. Most normally, the epoch is the current + /// epoch. + pub fn reset_age( + &mut self, + address: IA, + capability: Capability, + current_epoch: Epoch, + ) -> Result<(), Address, Coins, Epoch> + where + IA: Into
, + { + let address = address.into(); + let mut stake = self + .by_address + .get_mut(&address) + .ok_or(StakesError::IdentityNotFound { identity: address })? + .write()?; + stake.epochs.update(capability, current_epoch); + + Ok(()) + } + + /// Creates an instance of `Stakes` with a custom minimum stakeable amount. + pub fn with_minimum(minimum: Coins) -> Self { + Stakes { + minimum_stakeable: Some(minimum), + ..Default::default() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_stakes_initialization() { + let stakes = Stakes::::default(); + let ranking = stakes.rank(Capability::Mining, 0).collect::>(); + assert_eq!(ranking, Vec::default()); + } + + #[test] + fn test_add_stake() { + let mut stakes = Stakes::::with_minimum(5); + let alice = "Alice".into(); + let bob = "Bob".into(); + + // Let's check default power + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 0), + Err(StakesError::IdentityNotFound { + identity: alice.clone() + }) + ); + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 1_000), + Err(StakesError::IdentityNotFound { + identity: alice.clone() + }) + ); + + // Let's make Alice stake 100 Wit at epoch 100 + assert_eq!( + stakes.add_stake(&alice, 100, 100).unwrap(), + Stake::from_parts( + 100, + CapabilityMap { + mining: 100, + witnessing: 100 + } + ) + ); + + // Let's see how Alice's stake accrues power over time + assert_eq!(stakes.query_power(&alice, Capability::Mining, 99), Ok(0)); + assert_eq!(stakes.query_power(&alice, Capability::Mining, 100), Ok(0)); + assert_eq!(stakes.query_power(&alice, Capability::Mining, 101), Ok(100)); + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 200), + Ok(10_000) + ); + + // Let's make Alice stake 50 Wits at epoch 150 this time + assert_eq!( + stakes.add_stake(&alice, 50, 300).unwrap(), + Stake::from_parts( + 150, + CapabilityMap { + mining: 166, + witnessing: 166 + } + ) + ); + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 299), + Ok(19_950) + ); + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 300), + Ok(20_100) + ); + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 301), + Ok(20_250) + ); + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 400), + Ok(35_100) + ); + + // Now let's make Bob stake 500 Wits at epoch 1000 this time + assert_eq!( + stakes.add_stake(&bob, 500, 1_000).unwrap(), + Stake::from_parts( + 500, + CapabilityMap { + mining: 1_000, + witnessing: 1_000 + } + ) + ); + + // Before Bob stakes, Alice has all the power + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 999), + Ok(124950) + ); + assert_eq!(stakes.query_power(&bob, Capability::Mining, 999), Ok(0)); + + // New stakes don't change power in the same epoch + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 1_000), + Ok(125100) + ); + assert_eq!(stakes.query_power(&bob, Capability::Mining, 1_000), Ok(0)); + + // Shortly after, Bob's stake starts to gain power + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 1_001), + Ok(125250) + ); + assert_eq!(stakes.query_power(&bob, Capability::Mining, 1_001), Ok(500)); + + // After enough time, Bob overpowers Alice + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 2_000), + Ok(275_100) + ); + assert_eq!( + stakes.query_power(&bob, Capability::Mining, 2_000), + Ok(500_000) + ); + } + + #[test] + fn test_coin_age_resets() { + // First, lets create a setup with a few stakers + let mut stakes = Stakes::::with_minimum(5); + let alice = "Alice".into(); + let bob = "Bob".into(); + let charlie = "Charlie".into(); + + stakes.add_stake(&alice, 10, 0).unwrap(); + stakes.add_stake(&bob, 20, 20).unwrap(); + stakes.add_stake(&charlie, 30, 30).unwrap(); + + // Let's really start our test at epoch 100 + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 100), + Ok(1_000) + ); + assert_eq!(stakes.query_power(&bob, Capability::Mining, 100), Ok(1_600)); + assert_eq!( + stakes.query_power(&charlie, Capability::Mining, 100), + Ok(2_100) + ); + assert_eq!( + stakes.query_power(&alice, Capability::Witnessing, 100), + Ok(1_000) + ); + assert_eq!( + stakes.query_power(&bob, Capability::Witnessing, 100), + Ok(1_600) + ); + assert_eq!( + stakes.query_power(&charlie, Capability::Witnessing, 100), + Ok(2_100) + ); + assert_eq!( + stakes.rank(Capability::Mining, 100).collect::>(), + [ + (charlie.clone(), 2100), + (bob.clone(), 1600), + (alice.clone(), 1000) + ] + ); + assert_eq!( + stakes.rank(Capability::Witnessing, 100).collect::>(), + [ + (charlie.clone(), 2100), + (bob.clone(), 1600), + (alice.clone(), 1000) + ] + ); + + // Now let's slash Charlie's mining coin age right after + stakes.reset_age(&charlie, Capability::Mining, 101).unwrap(); + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 101), + Ok(1_010) + ); + assert_eq!(stakes.query_power(&bob, Capability::Mining, 101), Ok(1_620)); + assert_eq!(stakes.query_power(&charlie, Capability::Mining, 101), Ok(0)); + assert_eq!( + stakes.query_power(&alice, Capability::Witnessing, 101), + Ok(1_010) + ); + assert_eq!( + stakes.query_power(&bob, Capability::Witnessing, 101), + Ok(1_620) + ); + assert_eq!( + stakes.query_power(&charlie, Capability::Witnessing, 101), + Ok(2_130) + ); + assert_eq!( + stakes.rank(Capability::Mining, 101).collect::>(), + [ + (bob.clone(), 1_620), + (alice.clone(), 1_010), + (charlie.clone(), 0) + ] + ); + assert_eq!( + stakes.rank(Capability::Witnessing, 101).collect::>(), + [ + (charlie.clone(), 2_130), + (bob.clone(), 1_620), + (alice.clone(), 1_010) + ] + ); + + // Don't panic, Charlie! After enough time, you can take over again ;) + assert_eq!( + stakes.query_power(&alice, Capability::Mining, 300), + Ok(3_000) + ); + assert_eq!(stakes.query_power(&bob, Capability::Mining, 300), Ok(5_600)); + assert_eq!( + stakes.query_power(&charlie, Capability::Mining, 300), + Ok(5_970) + ); + assert_eq!( + stakes.query_power(&alice, Capability::Witnessing, 300), + Ok(3_000) + ); + assert_eq!( + stakes.query_power(&bob, Capability::Witnessing, 300), + Ok(5_600) + ); + assert_eq!( + stakes.query_power(&charlie, Capability::Witnessing, 300), + Ok(8_100) + ); + assert_eq!( + stakes.rank(Capability::Mining, 300).collect::>(), + [ + (charlie.clone(), 5_970), + (bob.clone(), 5_600), + (alice.clone(), 3_000) + ] + ); + assert_eq!( + stakes.rank(Capability::Witnessing, 300).collect::>(), + [ + (charlie.clone(), 8_100), + (bob.clone(), 5_600), + (alice.clone(), 3_000) + ] + ); + } +} diff --git a/data_structures/src/superblock.rs b/data_structures/src/superblock.rs index 7e3d30ad3..626661a55 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,15 @@ 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, +}; + /// Possible result of SuperBlockState::add_vote #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum AddSuperBlockVote { @@ -754,13 +755,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 +811,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 +823,7 @@ mod tests { default_hash, dr_merkle_root_1, 0, - block.hash(), + block.versioned_hash(ProtocolVersion::V1_6), default_hash, tally_merkle_root_1, ); @@ -855,6 +862,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 +879,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 +891,7 @@ mod tests { default_hash, expected_superblock_dr_root, 0, - block_2.hash(), + block_2.versioned_hash(ProtocolVersion::V1_6), default_hash, expected_superblock_tally_root, ); @@ -1108,7 +1119,7 @@ mod tests { genesis_hash, &alt_keys, None, - 1 + 1, ), expected_second_superblock ); diff --git a/data_structures/src/transaction.rs b/data_structures/src/transaction.rs index 4c98820e6..e300ddfc9 100644 --- a/data_structures/src/transaction.rs +++ b/data_structures/src/transaction.rs @@ -8,8 +8,9 @@ use witnet_crypto::{hash::calculate_sha256, merkle::FullMerkleTree}; use crate::{ chain::{ Block, Bn256PublicKey, DataRequestOutput, Epoch, Hash, Hashable, Input, KeyedSignature, - PublicKeyHash, ValueTransferOutput, + PublicKeyHash, StakeOutput, ValueTransferOutput, }, + error::TransactionError, proto::{schema::witnet, ProtobufConvert}, vrf::DataRequestEligibilityClaim, }; @@ -18,6 +19,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 +133,8 @@ pub enum Transaction { Reveal(RevealTransaction), Tally(TallyTransaction), Mint(MintTransaction), + Stake(StakeTransaction), + Unstake(UnstakeTransaction), } impl From for Transaction { @@ -168,6 +173,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 +266,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 +400,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 +411,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 +720,142 @@ 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 { + /// 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 operator: PublicKeyHash, + pub withdrawal: ValueTransferOutput, + + #[protobuf_convert(skip)] + #[serde(skip)] + hash: MemoHash, +} + +impl UnstakeTransactionBody { + /// Creates a new stake transaction body. + pub fn new(operator: PublicKeyHash, withdrawal: ValueTransferOutput) -> Self { + UnstakeTransactionBody { + operator, + withdrawal, + ..Default::default() + } + } + + pub fn value(&self) -> u64 { + self.withdrawal.value + } + + /// Stake transaction weight. It is calculated as: + /// + /// ```text + /// ST_weight = 153 + /// + /// ``` + pub fn weight(&self) -> u32 { + UNSTAKE_TRANSACTION_WEIGHT + } +} + impl MemoizedHashable for VTTransactionBody { fn hashable_bytes(&self) -> Vec { self.to_pb_bytes().unwrap() @@ -722,6 +895,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 +956,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 +976,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 +1177,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 +1198,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 e092879a9..3a33159e4 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, )) } @@ -570,6 +719,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/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..8de51b1e5 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_6).unwrap(); + assert_eq!(result, expected_buf); + + let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR_TRANSITION.to_vec(); + let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::V1_7).unwrap(); + assert_eq!(result, expected_buf); + + let expected_buf: Vec = EXAMPLE_BLOCK_VECTOR_FINAL.to_vec(); + let result: Vec = msg.to_versioned_pb_bytes(ProtocolVersion::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/handlers.rs b/node/src/actors/chain_manager/handlers.rs index 31c74f8d3..391073cff 100644 --- a/node/src/actors/chain_manager/handlers.rs +++ b/node/src/actors/chain_manager/handlers.rs @@ -9,14 +9,17 @@ use std::{ use actix::{prelude::*, ActorFutureExt, WrapFuture}; use futures::future::Either; -use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE; +use witnet_config::defaults::{ + PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT, + PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE, +}; use witnet_data_structures::{ chain::{ tapi::ActiveWips, Block, ChainState, CheckpointBeacon, DataRequestInfo, Epoch, Hash, Hashable, NodeStats, PublicKeyHash, SuperBlockVote, SupplyInfo, }, error::{ChainInfoError, TransactionError::DataRequestNotFound}, - transaction::{DRTransaction, Transaction, VTTransaction}, + transaction::{DRTransaction, StakeTransaction, Transaction, VTTransaction}, transaction_factory::{self, NodeBalance}, types::LastBeacon, utxo_pool::{get_utxo_info, UtxoInfo}, @@ -29,13 +32,14 @@ use crate::{ chain_manager::{handlers::BlockBatches::*, BlockCandidate}, messages::{ AddBlocks, AddCandidates, AddCommitReveal, AddSuperBlock, AddSuperBlockVote, - AddTransaction, Broadcast, BuildDrt, BuildVtt, EpochNotification, EstimatePriority, - GetBalance, GetBalanceTarget, GetBlocksEpochRange, GetDataRequestInfo, - GetHighestCheckpointBeacon, GetMemoryTransaction, GetMempool, GetMempoolResult, - GetNodeStats, GetReputation, GetReputationResult, GetSignalingInfo, GetState, - GetSuperBlockVotes, GetSupplyInfo, GetUtxoInfo, IsConfirmedBlock, PeersBeacons, - ReputationStats, Rewind, SendLastBeacon, SessionUnitResult, SetLastBeacon, - SetPeersLimits, SignalingInfo, SnapshotExport, SnapshotImport, TryMineBlock, + AddTransaction, Broadcast, BuildDrt, BuildStake, BuildVtt, EpochNotification, + EstimatePriority, GetBalance, GetBalanceTarget, GetBlocksEpochRange, + GetDataRequestInfo, GetHighestCheckpointBeacon, GetMemoryTransaction, GetMempool, + GetMempoolResult, GetNodeStats, GetReputation, GetReputationResult, GetSignalingInfo, + GetState, GetSuperBlockVotes, GetSupplyInfo, GetUtxoInfo, IsConfirmedBlock, + PeersBeacons, ReputationStats, Rewind, SendLastBeacon, SessionUnitResult, + SetLastBeacon, SetPeersLimits, SignalingInfo, SnapshotExport, SnapshotImport, + TryMineBlock, }, sessions_manager::SessionsManager, }, @@ -967,11 +971,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 +1007,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 +1292,70 @@ impl Handler for ChainManager { } } +impl Handler for ChainManager { + type Result = ResponseActFuture::Result>; + + fn handle(&mut self, msg: BuildStake, _ctx: &mut Self::Context) -> Self::Result { + if self.sm_state != StateMachine::Synced { + return Box::pin(actix::fut::err( + ChainManagerError::NotSynced { + current_state: self.sm_state, + } + .into(), + )); + } + let timestamp = u64::try_from(get_timestamp()).unwrap(); + match transaction_factory::build_st( + msg.stake_output, + msg.fee, + &mut self.chain_state.own_utxos, + self.own_pkh.unwrap(), + &self.chain_state.unspent_outputs_pool, + timestamp, + self.tx_pending_timeout, + &msg.utxo_strategy, + PSEUDO_CONSENSUS_CONSTANTS_POS_MAX_STAKE_BLOCK_WEIGHT, + msg.dry_run, + ) { + Err(e) => { + log::error!("Error when building stake transaction: {}", e); + Box::pin(actix::fut::err(e.into())) + } + Ok(st) => { + let fut = signature_mngr::sign_transaction(&st, st.inputs.len()) + .into_actor(self) + .then(move |s, act, _ctx| match s { + Ok(signatures) => { + let st = StakeTransaction::new(st, signatures); + + if msg.dry_run { + Either::Right(actix::fut::result(Ok(st))) + } else { + let transaction = Transaction::Stake(st.clone()); + Either::Left( + act.add_transaction( + AddTransaction { + transaction, + broadcast_flag: true, + }, + get_timestamp(), + ) + .map_ok(move |_, _, _| st), + ) + } + } + Err(e) => { + log::error!("Failed to sign stake transaction: {}", e); + Either::Right(actix::fut::result(Err(e))) + } + }); + + Box::pin(fut) + } + } + } +} + impl Handler for ChainManager { type Result = ResponseActFuture>; diff --git a/node/src/actors/chain_manager/mining.rs b/node/src/actors/chain_manager/mining.rs index 231bdfb9c..1263987b5 100644 --- a/node/src/actors/chain_manager/mining.rs +++ b/node/src/actors/chain_manager/mining.rs @@ -839,6 +839,10 @@ pub fn build_block( let mut value_transfer_txns = Vec::new(); let mut data_request_txns = Vec::new(); let mut tally_txns = Vec::new(); + // TODO: handle stake tx + let stake_txns = Vec::new(); + // TODO: handle unstake tx + let unstake_txns = Vec::new(); let min_vt_weight = VTTransactionBody::new(vec![Input::default()], vec![ValueTransferOutput::default()]) @@ -949,7 +953,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) { @@ -1000,6 +1004,8 @@ pub fn build_block( let commit_hash_merkle_root = merkle_tree_root(&commit_txns); let reveal_hash_merkle_root = merkle_tree_root(&reveal_txns); let tally_hash_merkle_root = merkle_tree_root(&tally_txns); + let stake_hash_merkle_root = merkle_tree_root(&stake_txns); + let unstake_hash_merkle_root = merkle_tree_root(&unstake_txns); let merkle_roots = BlockMerkleRoots { mint_hash: mint.hash(), vt_hash_merkle_root, @@ -1007,6 +1013,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 +1032,8 @@ pub fn build_block( commit_txns, reveal_txns, tally_txns, + stake_txns, + unstake_txns, }; (block_header, txns) @@ -1446,9 +1456,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![]); @@ -1542,9 +1552,9 @@ mod tests { let mut dr3 = dr1.clone(); dr3.commit_and_reveal_fee = 3; - let dr_body_one_output1 = DRTransactionBody::new(input.clone(), vec![], dr1); - let dr_body_one_output2 = DRTransactionBody::new(input.clone(), vec![], dr2); - let dr_body_one_output3 = DRTransactionBody::new(input, vec![], dr3); + let dr_body_one_output1 = DRTransactionBody::new(input.clone(), dr1, vec![]); + let dr_body_one_output2 = DRTransactionBody::new(input.clone(), dr2, vec![]); + let dr_body_one_output3 = DRTransactionBody::new(input, dr3, vec![]); // Build sample transactions let dr_tx1 = DRTransaction::new(dr_body_one_output1, vec![]); diff --git a/node/src/actors/chain_manager/mod.rs b/node/src/actors/chain_manager/mod.rs index 2d0544ca7..f2d6908de 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::{ @@ -725,8 +726,9 @@ impl ChainManager { || block.block_header.beacon.checkpoint == current_epoch + 1) { log::debug!( - "Ignoring received block #{} because its beacon is too old", - block.block_header.beacon.checkpoint + "Ignoring received block candidate because its beacon shows an old epoch ({}). The current epoch is {}.", + block.block_header.beacon.checkpoint, + current_epoch, ); return; diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index d7adf886a..1ce56b137 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -19,12 +19,11 @@ use itertools::Itertools; use jsonrpc_core::{BoxFuture, Error, Params, Value}; use jsonrpc_pubsub::{Subscriber, SubscriptionId}; use serde::{Deserialize, Serialize}; - -use witnet_crypto::key::KeyPath; +use witnet_crypto::{key::KeyPath, secp256k1::ecdsa::Signature}; use witnet_data_structures::{ chain::{ - tapi::ActiveWips, Block, DataRequestOutput, Epoch, Hash, Hashable, PublicKeyHash, RADType, - StateMachine, SyncStatus, + tapi::ActiveWips, Block, DataRequestOutput, Environment, Epoch, Hash, Hashable, + KeyedSignature, PublicKey, PublicKeyHash, RADType, StakeOutput, StateMachine, SyncStatus, }, transaction::Transaction, vrf::VrfMessage, @@ -37,13 +36,14 @@ use crate::{ inventory_manager::{InventoryManager, InventoryManagerError}, json_rpc::Subscriptions, messages::{ - AddCandidates, AddPeers, AddTransaction, BuildDrt, BuildVtt, ClearPeers, DropAllPeers, - EstimatePriority, GetBalance, GetBalanceTarget, GetBlocksEpochRange, - GetConsolidatedPeers, GetDataRequestInfo, GetEpoch, GetHighestCheckpointBeacon, - GetItemBlock, GetItemSuperblock, GetItemTransaction, GetKnownPeers, - GetMemoryTransaction, GetMempool, GetNodeStats, GetReputation, GetSignalingInfo, - GetState, GetSupplyInfo, GetUtxoInfo, InitializePeers, IsConfirmedBlock, Rewind, - SnapshotExport, SnapshotImport, + AddCandidates, AddPeers, AddTransaction, AuthorizationParams, AuthorizeStake, BuildDrt, + BuildStake, BuildStakeParams, BuildVtt, ClearPeers, DropAllPeers, EstimatePriority, + GetBalance, GetBalanceTarget, GetBlocksEpochRange, GetConsolidatedPeers, + GetDataRequestInfo, GetEpoch, GetHighestCheckpointBeacon, GetItemBlock, + GetItemSuperblock, GetItemTransaction, GetKnownPeers, GetMemoryTransaction, GetMempool, + GetNodeStats, GetReputation, GetSignalingInfo, GetState, GetSupplyInfo, GetUtxoInfo, + InitializePeers, IsConfirmedBlock, Rewind, SnapshotExport, SnapshotImport, + StakeAuthorization, }, peers_manager::PeersManager, sessions_manager::SessionsManager, @@ -266,6 +266,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 +1938,138 @@ pub async fn snapshot_import(params: Result) -> Jso // Write the response back (the path to the snapshot file) serde_json::to_value(response).map_err(internal_error_s) } +/// Build a stake transaction +pub async fn stake(params: Result) -> JsonRpcResult { + log::debug!("Creating stake transaction from JSON-RPC."); + + match params { + Ok(msg) => { + let withdrawer = match msg.withdrawer { + Some(withdrawer) => withdrawer, + None => { + let pk = signature_mngr::public_key() + .map(|res| { + res.map_err(internal_error) + .map(|pk| pk.pkh().bech32(Environment::Mainnet)) + }) + .await; + + pk.unwrap() + } + }; + + let authorization: AuthorizationParams = match msg.authorization { + Some(authorization) => authorization, + None => { + let mut data = [0u8; witnet_crypto::secp256k1::constants::MESSAGE_SIZE]; + data[0..20].clone_from_slice(withdrawer.as_ref()); + + let keyed_signature = signature_mngr::sign_data(data) + .map(|res| res.map_err(internal_error)) + .await + .unwrap(); + + AuthorizationParams { + authorization: hex::encode(keyed_signature.signature.to_bytes().unwrap()), + public_key: hex::encode(keyed_signature.public_key.to_bytes()), + } + } + }; + + let signature = Signature::from_str(&authorization.authorization).unwrap(); + let authorization = KeyedSignature { + signature: signature.into(), + // TODO: https://docs.rs/secp256k1/0.22.2/secp256k1/struct.Secp256k1.html#method.recover_ecdsa + // public_key: signature.recover_ecdsa(withdrawer, signature) + public_key: PublicKey::from_str(&authorization.public_key), + }; + + let build_stake = BuildStake { + dry_run: msg.dry_run, + fee: msg.fee, + utxo_strategy: msg.utxo_strategy, + stake_output: StakeOutput { + authorization, + value: msg.value, + }, + }; + + ChainManager::from_registry() + .send(build_stake) + .map(|res| match res { + Ok(Ok(hash)) => match serde_json::to_value(hash) { + Ok(x) => Ok(x), + Err(e) => { + let err = internal_error_s(e); + Err(err) + } + }, + Ok(Err(e)) => { + let err = internal_error_s(e); + Err(err) + } + Err(e) => { + let err = internal_error_s(e); + Err(err) + } + }) + .await + } + Err(err) => Err(err), + } +} + +/// Create a stake authorization for the given address. +/// +/// The output of this method is a required argument to call the Stake method. +/* test +{"jsonrpc": "2.0","method": "authorizeStake", "params": {"withdrawer":"wit1lkzl4a365fvrr604pwqzykxugpglkrp5ekj0k0"}, "id": "1"} +*/ +pub async fn authorize_stake(params: Result) -> JsonRpcResult { + print!("Inside authorize_stake"); + log::debug!("Creating an authorization stake from JSON-RPC."); + match params { + Ok(msg) => { + let mut withdrawer = msg.withdrawer; + if withdrawer.is_none() { + let pk = signature_mngr::public_key() + .map(|res| { + res.map_err(internal_error) + .map(|pk| pk.pkh().bech32(Environment::Mainnet)) + }) + .await; + withdrawer = Some(pk.unwrap()); + } + // FIXME: we could use directly the pk calculated above in one case + let pkh = + PublicKeyHash::from_bech32(Environment::Mainnet, &withdrawer.clone().unwrap()) + .unwrap(); + let mut data = [0u8; witnet_crypto::secp256k1::constants::MESSAGE_SIZE]; + data[0..20].clone_from_slice(pkh.as_ref()); + + signature_mngr::sign_data(data) + .map(|res| { + res.map_err(internal_error).and_then(|ks| { + let a = StakeAuthorization { + withdrawer: withdrawer.unwrap(), + signature: hex::encode(ks.signature.to_bytes().unwrap()), + public_key: hex::encode(ks.public_key.to_bytes()), + }; + + match serde_json::to_value(a) { + Ok(value) => Ok(value), + Err(e) => { + let err = internal_error_s(e); + Err(err) + } + } + }) + }) + .await + } + Err(err) => Err(err), + } +} #[cfg(test)] mod mock_actix { @@ -2124,7 +2273,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 +2336,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 +2375,7 @@ mod tests { all_methods_vec, vec![ "addPeers", + "authorizeStake", "chainExport", "chainImport", "clearPeers", @@ -2256,6 +2406,7 @@ mod tests { "sendValue", "sign", "signalingInfo", + "stake", "syncStatus", "tryRequest", "witnet_subscribe", @@ -2274,6 +2425,7 @@ mod tests { let expected_sensitive_methods = vec![ "addPeers", + "authorizeStake", "clearPeers", "createVRF", "getPkh", @@ -2286,6 +2438,7 @@ mod tests { "sendValue", "sign", "tryRequest", + "stake", ]; for method_name in expected_sensitive_methods { diff --git a/node/src/actors/messages.rs b/node/src/actors/messages.rs index d11776e2b..fa78be182 100644 --- a/node/src/actors/messages.rs +++ b/node/src/actors/messages.rs @@ -22,13 +22,14 @@ use witnet_data_structures::{ tapi::{ActiveWips, BitVotesCounter}, Block, CheckpointBeacon, DataRequestInfo, DataRequestOutput, Epoch, EpochConstants, Hash, InventoryEntry, InventoryItem, NodeStats, PointerToBlock, PublicKeyHash, - PublicKeyHashParseError, RADRequest, RADTally, Reputation, StateMachine, SuperBlock, - SuperBlockVote, SupplyInfo, ValueTransferOutput, + PublicKeyHashParseError, RADRequest, RADTally, Reputation, StakeOutput, StateMachine, + SuperBlock, SuperBlockVote, SupplyInfo, ValueTransferOutput, }, fee::{deserialize_fee_backwards_compatible, Fee}, radon_report::RadonReport, transaction::{ - CommitTransaction, DRTransaction, RevealTransaction, Transaction, VTTransaction, + CommitTransaction, DRTransaction, RevealTransaction, StakeTransaction, Transaction, + VTTransaction, }, transaction_factory::NodeBalance, types::LastBeacon, @@ -220,6 +221,89 @@ impl Message for BuildVtt { type Result = Result; } +/// Builds a `StakeTransaction` from a list of `ValueTransferOutput`s +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct BuildStake { + /// List of `ValueTransferOutput`s + pub stake_output: StakeOutput, + /// Fee + #[serde(default)] + pub fee: Fee, + /// Strategy to sort the unspent outputs pool + #[serde(default)] + pub utxo_strategy: UtxoSelectionStrategy, + /// Construct the transaction but do not broadcast it + #[serde(default)] + pub dry_run: bool, +} + +impl Message for BuildStake { + type Result = Result; +} + +/// Authorization formatted as strings +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct AuthorizationParams { + /// Authorization public key + pub public_key: String, + /// Authorization signature + pub authorization: String, +} + +/// Builds a `StakeTransaction` from a list of `ValueTransferOutput`s +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct BuildStakeParams { + /// Authorization signature and public key + #[serde(default)] + pub authorization: Option, + /// List of `ValueTransferOutput`s + #[serde(default)] + pub value: u64, + /// Withdrawer + #[serde(default)] + pub withdrawer: Option, + /// Fee + #[serde(default)] + pub fee: Fee, + /// Strategy to sort the unspent outputs pool + #[serde(default)] + pub utxo_strategy: UtxoSelectionStrategy, + /// Construct the transaction but do not broadcast it + #[serde(default)] + pub dry_run: bool, +} + +// impl Message for BuildStake { +// type Result = Result; +// } + +/// Builds an `AuthorizeStake` +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct AuthorizeStake { + /// Address that can withdraw the stake + #[serde(default)] + pub withdrawer: Option, +} + +impl Message for AuthorizeStake { + type Result = Result; +} + +/// Builds an `StakeAuthorization` +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] +pub struct StakeAuthorization { + /// Address that can withdraw the stake + pub withdrawer: String, + /// Signature of the withdrawer + pub signature: String, + /// Public key related with signature + pub public_key: String, +} + +impl Message for StakeAuthorization { + type Result = Result; +} + /// Builds a `DataRequestTransaction` from a `DataRequestOutput` #[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct BuildDrt { diff --git a/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/schemas/witnet/witnet.proto b/schemas/witnet/witnet.proto index 64b1b04e0..f110eda14 100644 --- a/schemas/witnet/witnet.proto +++ b/schemas/witnet/witnet.proto @@ -2,6 +2,28 @@ syntax = "proto3"; package witnet; +message LegacyMessage { + message LegacyCommand { + oneof kind { + Version Version = 1; + Verack Verack = 2; + GetPeers GetPeers = 3; + Peers Peers = 4; + LegacyBlock Block = 5; + InventoryAnnouncement InventoryAnnouncement = 6; + InventoryRequest InventoryRequest = 7; + LastBeacon LastBeacon = 8; + Transaction Transaction = 9; + SuperBlockVote SuperBlockVote = 10; + SuperBlock SuperBlock = 11; + } + } + + // uint32 is not a fixed-size 32 bit integer: it uses variable length encoding + uint32 magic = 1; + LegacyCommand kind = 2; +} + message Message { message Command { oneof kind { @@ -47,10 +69,42 @@ message Peers { repeated Address peers = 1; } -message Block { - message BlockEligibilityClaim { - VrfProof proof = 1; +message BlockEligibilityClaim { + VrfProof proof = 1; +} + +message LegacyBlock { + message LegacyBlockHeader { + message LegacyBlockMerkleRoots { + Hash mint_hash = 1; + Hash vt_hash_merkle_root = 2; + Hash dr_hash_merkle_root = 3; + Hash commit_hash_merkle_root = 4; + Hash reveal_hash_merkle_root = 5; + Hash tally_hash_merkle_root = 6; + } + + uint32 signals = 1; + CheckpointBeacon beacon = 2; + LegacyBlockMerkleRoots merkle_roots = 3; + BlockEligibilityClaim proof = 4; + Bn256PublicKey bn256_public_key = 5; } + message LegacyBlockTransactions { + MintTransaction mint = 1; + repeated VTTransaction value_transfer_txns = 2; + repeated DRTransaction data_request_txns = 3; + repeated CommitTransaction commit_txns = 4; + repeated RevealTransaction reveal_txns = 5; + repeated TallyTransaction tally_txns = 6; + } + + LegacyBlockHeader block_header = 1; + KeyedSignature block_sig = 2; + LegacyBlockTransactions txns = 3; +} + +message Block { message BlockHeader { message BlockMerkleRoots { Hash mint_hash = 1; @@ -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,33 @@ message MintTransaction { repeated ValueTransferOutput outputs = 2; } +message StakeOutput { + uint64 value = 1; + KeyedSignature authorization = 2; +} + +message StakeTransactionBody { + repeated Input inputs = 1; + StakeOutput output = 2; + optional ValueTransferOutput change = 3; +} + +message StakeTransaction { + StakeTransactionBody body = 1 ; + repeated KeyedSignature signatures = 2; +} + +message UnstakeTransactionBody { + PublicKeyHash operator = 1; + ValueTransferOutput withdrawal = 2; + ValueTransferOutput change = 3; +} + +message UnstakeTransaction { + UnstakeTransactionBody body = 1 ; + KeyedSignature signature = 2; +} + message Transaction { oneof kind { VTTransaction ValueTransfer = 1; @@ -237,6 +322,8 @@ message Transaction { RevealTransaction Reveal = 4; TallyTransaction Tally = 5; MintTransaction Mint = 6; + StakeTransaction Stake = 7; + UnstakeTransaction Unstake = 8; } } diff --git a/src/cli/node/json_rpc_client.rs b/src/cli/node/json_rpc_client.rs index c1a9ee6c9..2a8b6ae3f 100644 --- a/src/cli/node/json_rpc_client.rs +++ b/src/cli/node/json_rpc_client.rs @@ -1,3 +1,10 @@ +use ansi_term::Color::{Purple, Red, White, Yellow}; +use failure::{bail, Fail}; +use itertools::Itertools; +use num_format::{Locale, ToFormattedString}; +use prettytable::{row, Table}; +use qrcode::render::unicode; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ collections::HashMap, convert::TryFrom, @@ -8,13 +15,6 @@ use std::{ path::Path, str::FromStr, }; - -use ansi_term::Color::{Purple, Red, White, Yellow}; -use failure::{bail, Fail}; -use itertools::Itertools; -use num_format::{Locale, ToFormattedString}; -use prettytable::{row, Table}; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; use witnet_crypto::{ hash::calculate_sha256, @@ -30,7 +30,7 @@ use witnet_data_structures::{ }, fee::Fee, proto::ProtobufConvert, - transaction::{DRTransaction, Transaction, VTTransaction}, + transaction::{DRTransaction, StakeTransaction, Transaction, VTTransaction}, transaction_factory::NodeBalance, types::SequentialId, utxo_pool::{UtxoInfo, UtxoSelectionStrategy}, @@ -39,7 +39,10 @@ use witnet_data_structures::{ use witnet_node::actors::{ chain_manager::run_dr_locally, json_rpc::api::{AddrType, GetBlockChainParams, GetTransactionOutput, PeersResult}, - messages::{BuildDrt, BuildVtt, GetBalanceTarget, GetReputationResult, SignalingInfo}, + messages::{ + AuthorizationParams, AuthorizeStake, BuildDrt, BuildStakeParams, BuildVtt, + GetBalanceTarget, GetReputationResult, SignalingInfo, StakeAuthorization, + }, }; use witnet_rad::types::RadonTypes; use witnet_util::{files::create_private_file, timestamp::pretty_print}; @@ -855,6 +858,167 @@ pub fn send_dr( Ok(()) } +pub fn send_st( + addr: SocketAddr, + value: u64, + withdrawer: Option, + fee: Option, + sorted_bigger: Option, + dry_run: bool, +) -> Result<(), failure::Error> { + let mut stream = start_client(addr)?; + let mut id = SequentialId::initialize(1u8); + + let authorize_stake_params = AuthorizeStake { withdrawer }; + + let (stake_authorization, (_, _response)): (StakeAuthorization, _) = issue_method( + "authorizeStake", + Some(authorize_stake_params), + &mut stream, + id.next(), + )?; + + // Prepare for fee estimation if no fee value was specified + let (fee, estimate) = unwrap_fee_or_estimate_priority(fee, &mut stream, &mut id)?; + + let utxo_strategy = match sorted_bigger { + Some(true) => UtxoSelectionStrategy::BigFirst { from: None }, + Some(false) => UtxoSelectionStrategy::SmallFirst { from: None }, + None => UtxoSelectionStrategy::Random { from: None }, + }; + + let mut build_stake_params = BuildStakeParams { + authorization: Some(AuthorizationParams { + authorization: stake_authorization.signature, + public_key: stake_authorization.public_key, + }), + withdrawer: Some(stake_authorization.withdrawer), + value, + fee, + utxo_strategy, + dry_run, + }; + + // If no fee was specified, we first need to do a dry run for each of the priority tiers to + // find out the actual transaction weight (as different priorities will affect the number + // of inputs being used, and thus also the weight). + if let Some(PrioritiesEstimate { + vtt_stinky, + vtt_low, + vtt_medium, + vtt_high, + vtt_opulent, + .. + }) = estimate + { + let priorities = vec![ + (vtt_stinky, "Stinky"), + (vtt_low, "Low"), + (vtt_medium, "Medium"), + (vtt_high, "High"), + (vtt_opulent, "Opulent"), + ]; + let mut estimates = vec![]; + let mut fee; + + // Iterative algorithm for transaction weight discovery. It calculates the fees for this + // transaction assuming that it has the minimum weight, and then repeats the estimation + // using the actual weight of the latest created transaction, until the weight stabilizes + // or after 5 rounds. + for ( + PriorityEstimate { + priority, + time_to_block, + }, + label, + ) in priorities + { + // The minimum ST size is N*133+M*36+105` where `N` is the number of `inputs`, and `M` + // is 0 or 1 depending on whether a `change` output is used + let mut weight = 238u32; + let mut rounds = 0u8; + // Iterative algorithm for weight discovery + loop { + // Calculate fee for current priority and weight + fee = Fee::absolute_from_wit(priority.derive_fee_wit(weight)); + + // Create and dry run a Stake transaction using that fee + let dry_params = BuildStakeParams { + fee, + dry_run: true, + ..build_stake_params.clone() + }; + let (dry_st, ..): (StakeTransaction, _) = + issue_method("stake", Some(dry_params), &mut stream, id.next())?; + let dry_weight = dry_st.weight(); + + // We retry up to 5 times, or until the weight is stable + if rounds > 5 || dry_weight == weight { + break; + } + + weight = dry_weight; + rounds += 1; + } + + estimates.push((label, priority, fee, time_to_block)); + } + + // We are ready to compose the params for the actual transaction. + build_stake_params.fee = prompt_user_for_priority_selection(estimates)?; + } + + // Finally ask the node to create the transaction with the chosen fee. + let (_st, (request, response)): (StakeTransaction, _) = + issue_method("stake", Some(build_stake_params), &mut stream, id.next())?; + + // On dry run mode, print the request, otherwise, print the response. + // This is kept like this strictly for backwards compatibility. + // TODO: wouldn't it be better to always print the response or both? + if dry_run { + println!("{}", request); + } else { + println!("{}", response); + } + + Ok(()) +} + +pub fn authorize_st(addr: SocketAddr, withdrawer: Option) -> Result<(), failure::Error> { + if withdrawer.is_some() { + // validate withdrawer + PublicKeyHash::from_bech32(Environment::Mainnet, &withdrawer.clone().unwrap())?; + } + + let mut stream = start_client(addr)?; + let mut id = SequentialId::initialize(1u8); + + let params = AuthorizeStake { withdrawer }; + let (authorization, (_, response)): (StakeAuthorization, _) = + issue_method("authorizeStake", Some(params), &mut stream, id.next())?; + + println!("{}", response); + + let mut str = authorization.signature.clone(); + str.push(':'); + str.push_str(&authorization.public_key); + + let auth_qr = qrcode::QrCode::new(str).unwrap(); + let auth_ascii = auth_qr + .render::() + .quiet_zone(true) + .dark_color(unicode::Dense1x2::Light) + .light_color(unicode::Dense1x2::Dark) + .build(); + + println!( + "Authorization code:\n{}\nPublicKey code:\n{}\nQR code for myWitWallet:\n{}", + authorization.signature, authorization.public_key, auth_ascii + ); + + Ok(()) +} + pub fn master_key_export( addr: SocketAddr, write_to_path: Option<&Path>, @@ -1849,7 +2013,6 @@ where id.unwrap_or(1) ); let response = send_request(stream, &request)?; - parse_response::(&response).map(|output| (output, (request, response))) } diff --git a/src/cli/node/with_node.rs b/src/cli/node/with_node.rs index 41321f29c..408ef70a0 100644 --- a/src/cli/node/with_node.rs +++ b/src/cli/node/with_node.rs @@ -269,6 +269,23 @@ pub fn exec_cmd( Command::Rewind { node, epoch } => rpc::rewind(node.unwrap_or(default_jsonrpc), epoch), Command::SignalingInfo { node } => rpc::signaling_info(node.unwrap_or(default_jsonrpc)), Command::Priority { node, json } => rpc::priority(node.unwrap_or(default_jsonrpc), json), + Command::Stake { + node, + value, + withdrawer, + fee, + dry_run, + } => rpc::send_st( + node.unwrap_or(default_jsonrpc), + value, + withdrawer, + fee.map(Fee::absolute_from_nanowits), + None, + dry_run, + ), + Command::AuthorizeStake { node, withdrawer } => { + rpc::authorize_st(node.unwrap_or(default_jsonrpc), withdrawer) + } } } @@ -730,6 +747,35 @@ pub enum Command { #[structopt(long = "json", help = "Show output in JSON format")] json: bool, }, + #[structopt(name = "stake", about = "Create a stake transaction")] + Stake { + /// Socket address of the Witnet node to query + #[structopt(short = "n", long = "node")] + node: Option, + /// Value + #[structopt(long = "value")] + value: u64, + /// Withdrawer + #[structopt(long = "withdrawer")] + // make it also optional in jsonrcp + // see get balance in main + withdrawer: Option, + /// Fee + #[structopt(long = "fee")] + fee: Option, + /// Print the request that would be sent to the node and exit without doing anything + #[structopt(long = "dry-run")] + dry_run: bool, + }, + #[structopt(name = "authorizeStake", about = "Create an stake authorization")] + AuthorizeStake { + /// Socket address of the Witnet node to query + #[structopt(short = "n", long = "node")] + node: Option, + /// Withdrawer address + #[structopt(long = "withdrawer")] + withdrawer: Option, + }, } #[derive(Debug, StructOpt)] 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..1c43c1971 100644 --- a/validations/src/tests/mod.rs +++ b/validations/src/tests/mod.rs @@ -6,7 +6,10 @@ use std::{ use itertools::Itertools; -use witnet_config::defaults::PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO; +use witnet_config::defaults::{ + PSEUDO_CONSENSUS_CONSTANTS_POS_MIN_STAKE_NANOWITS, + PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO, +}; use witnet_crypto::{ secp256k1::{PublicKey as Secp256k1_PublicKey, SecretKey as Secp256k1_SecretKey}, signature::sign, @@ -47,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]); @@ -3011,7 +3016,7 @@ fn test_commit_with_dr_and_utxo_set( collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3071,7 +3076,7 @@ fn test_commit_difficult_proof() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3158,7 +3163,7 @@ fn test_commit_with_collateral( collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3321,7 +3326,7 @@ fn commitment_no_signature() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3418,7 +3423,7 @@ fn commitment_invalid_proof() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_epoch = 0; @@ -3485,7 +3490,7 @@ fn commitment_dr_in_reveal_stage() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3856,7 +3861,7 @@ fn commitment_collateral_zero_is_minimum() { collateral: 0, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -3945,7 +3950,7 @@ fn commitment_timelock() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -4042,7 +4047,7 @@ fn dr_pool_with_dr_in_reveal_stage() -> (DataRequestPool, Hash) { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_pointer = dr_transaction.hash(); @@ -4161,7 +4166,7 @@ fn reveal_dr_in_commit_stage() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_pointer = dr_transaction.hash(); @@ -4292,7 +4297,7 @@ fn reveal_valid_commitment() { ..DataRequestOutput::default() }; let dr_transaction = DRTransaction { - body: DRTransactionBody::new(vec![], vec![], dr_output), + body: DRTransactionBody::new(vec![], dr_output, vec![]), signatures: vec![KeyedSignature::default()], }; let dr_pointer = dr_transaction.hash(); @@ -4577,7 +4582,7 @@ fn dr_pool_with_dr_in_tally_all_errors( // Create DRTransaction let epoch = 0; let dr_transaction = DRTransaction { - body: DRTransactionBody::new(vec![], vec![], dr_output.clone()), + body: DRTransactionBody::new(vec![], dr_output.clone(), vec![]), signatures: vec![KeyedSignature { signature: Default::default(), public_key: dr_public_key.clone(), @@ -4690,7 +4695,7 @@ fn dr_pool_with_dr_in_tally_stage_generic( // Create DRTransaction let epoch = 0; let dr_transaction = DRTransaction { - body: DRTransactionBody::new(vec![], vec![], dr_output.clone()), + body: DRTransactionBody::new(vec![], dr_output.clone(), vec![]), signatures: vec![KeyedSignature { signature: Default::default(), public_key: dr_public_key.clone(), @@ -4907,7 +4912,7 @@ fn tally_dr_not_tally_stage() { data_request: example_data_request(), collateral: DEFAULT_COLLATERAL, }; - let dr_transaction_body = DRTransactionBody::new(vec![], vec![], dr_output.clone()); + let dr_transaction_body = DRTransactionBody::new(vec![], dr_output.clone(), vec![]); let dr_transaction_signature = sign_tx(PRIV_KEY_2, &dr_transaction_body); let dr_transaction = DRTransaction::new(dr_transaction_body, vec![dr_transaction_signature]); let dr_pointer = dr_transaction.hash(); @@ -5205,7 +5210,7 @@ fn generic_tally_test_inner( // Create DRTransaction let epoch = 0; let dr_transaction = DRTransaction { - body: DRTransactionBody::new(vec![], vec![], dr_output), + body: DRTransactionBody::new(vec![], dr_output, vec![]), signatures: vec![KeyedSignature { signature: Default::default(), public_key: dr_public_key.clone(), @@ -8448,6 +8453,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 +9204,7 @@ fn block_duplicated_commits() { data_request: example_data_request(), collateral: DEFAULT_COLLATERAL, }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -9191,7 +9297,7 @@ fn block_duplicated_reveals() { data_request: example_data_request(), collateral: DEFAULT_COLLATERAL, }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -9360,7 +9466,7 @@ fn block_before_and_after_hard_fork() { data_request: example_data_request_before_wip19(), collateral: DEFAULT_COLLATERAL, }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro.clone()); + let dr_body = DRTransactionBody::new(vec![], dro.clone(), vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_epoch = 0; @@ -9375,7 +9481,7 @@ fn block_before_and_after_hard_fork() { }; let utxo_set = build_utxo_set_with_mint(vec![vto], None, vec![]); let vti = Input::new(utxo_set.iter().next().unwrap().0); - let dr_tx_body = DRTransactionBody::new(vec![vti], vec![], dro); + let dr_tx_body = DRTransactionBody::new(vec![vti], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -9923,7 +10029,7 @@ fn block_add_drt() { }; let output1_pointer = ONE_WIT_OUTPUT.parse().unwrap(); let dr_tx_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![vto0], dr_output); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dr_output, vec![vto0]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_transaction = DRTransaction::new(dr_tx_body, vec![drs]); @@ -9959,7 +10065,7 @@ fn block_add_2_drt_same_input() { }; let output1_pointer = ONE_WIT_OUTPUT.parse().unwrap(); let dr_tx_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![vto0], dr_output); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dr_output, vec![vto0]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_tx1 = DRTransaction::new(dr_tx_body, vec![drs]); @@ -9979,7 +10085,7 @@ fn block_add_2_drt_same_input() { }; let output1_pointer = ONE_WIT_OUTPUT.parse().unwrap(); let dr_tx_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![vto0], dr_output); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dr_output, vec![vto0]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_tx2 = DRTransaction::new(dr_tx_body, vec![drs]); @@ -10020,7 +10126,7 @@ fn block_add_1_drt_and_1_vtt_same_input() { }; let output1_pointer = ONE_WIT_OUTPUT.parse().unwrap(); let dr_tx_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![vto0], dr_output); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dr_output, vec![vto0]); let drs = sign_tx(PRIV_KEY_1, &dr_tx_body); let dr_tx = DRTransaction::new(dr_tx_body, vec![drs]); @@ -10506,7 +10612,7 @@ fn validate_commit_transactions_included_in_utxo_diff() { collateral: DEFAULT_COLLATERAL, ..DataRequestOutput::default() }; - let dr_body = DRTransactionBody::new(vec![], vec![], dro); + let dr_body = DRTransactionBody::new(vec![], dro, vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_transaction = DRTransaction::new(dr_body, vec![drs]); let dr_hash = dr_transaction.hash(); @@ -10898,12 +11004,12 @@ fn validate_dr_weight_overflow() { let dr_value = dro.checked_total_value().unwrap(); let dr_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![], dro.clone()); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dro.clone(), vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_tx = DRTransaction::new(dr_body, vec![drs]); assert_eq!(dr_tx.weight(), 1589); - let dr_body2 = DRTransactionBody::new(vec![Input::new(output2_pointer)], vec![], dro); + let dr_body2 = DRTransactionBody::new(vec![Input::new(output2_pointer)], dro, vec![]); let drs2 = sign_tx(PRIV_KEY_1, &dr_body2); let dr_tx2 = DRTransaction::new(dr_body2, vec![drs2]); assert_eq!(dr_tx2.weight(), 1589); @@ -10940,7 +11046,7 @@ fn validate_dr_weight_overflow_126_witnesses() { let dr_value = dro.checked_total_value().unwrap(); let dr_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![], dro.clone()); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dro.clone(), vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_tx = DRTransaction::new(dr_body, vec![drs]); @@ -10979,12 +11085,12 @@ fn validate_dr_weight_valid() { let dr_value = dro.checked_total_value().unwrap(); let dr_body = - DRTransactionBody::new(vec![Input::new(output1_pointer)], vec![], dro.clone()); + DRTransactionBody::new(vec![Input::new(output1_pointer)], dro.clone(), vec![]); let drs = sign_tx(PRIV_KEY_1, &dr_body); let dr_tx = DRTransaction::new(dr_body, vec![drs]); assert_eq!(dr_tx.weight(), 1589); - let dr_body2 = DRTransactionBody::new(vec![Input::new(output2_pointer)], vec![], dro); + let dr_body2 = DRTransactionBody::new(vec![Input::new(output2_pointer)], dro, vec![]); let drs2 = sign_tx(PRIV_KEY_1, &dr_body2); let dr_tx2 = DRTransaction::new(dr_body2, vec![drs2]); assert_eq!(dr_tx2.weight(), 1589); diff --git a/validations/src/validations.rs b/validations/src/validations.rs index cc7c90ca3..1c678e89f 100644 --- a/validations/src/validations.rs +++ b/validations/src/validations.rs @@ -6,6 +6,7 @@ use std::{ }; use itertools::Itertools; + use witnet_config::defaults::{ PSEUDO_CONSENSUS_CONSTANTS_WIP0022_REWARD_COLLATERAL_RATIO, PSEUDO_CONSENSUS_CONSTANTS_WIP0027_COLLATERAL_AGE, @@ -21,17 +22,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, 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, @@ -49,6 +52,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 = 10_000_000_000_000; +const MAX_UNSTAKE_BLOCK_WEIGHT: u32 = 5_000; +const UNSTAKING_DELAY_SECONDS: u32 = 1_209_600; + /// Returns the fee of a value transfer transaction. /// /// The fee is the difference between the outputs and the inputs @@ -95,6 +104,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) @@ -339,8 +385,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(), @@ -1089,6 +1133,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, @@ -1109,6 +1280,8 @@ pub fn validate_block_signature( let signature = keyed_signature.signature.clone().try_into()?; let public_key = keyed_signature.public_key.clone().try_into()?; + // TODO: take into account block epoch to decide protocol version (with regards to data + // structures and hashing) let Hash::SHA256(message) = block.hash(); add_secp_block_signature_to_verify(signatures_to_verify, &public_key, &message, &signature); @@ -1387,7 +1560,7 @@ pub fn validate_block_transactions( mut visitor: Option<&mut dyn Visitor>, ) -> Result { let epoch = block.block_header.beacon.checkpoint; - let is_genesis = block.hash() == consensus_constants.genesis_hash; + let is_genesis = block.is_genesis(&consensus_constants.genesis_hash); let mut utxo_diff = UtxoDiff::new(utxo_set, block_number); // Init total fee @@ -1677,6 +1850,106 @@ pub fn validate_block_transactions( ); } + // TODO skip all staking logic if protocol version is legacy + + // validate stake transactions in a block + let mut st_mt = ProgressiveMerkleTree::sha256(); + let mut st_weight: u32 = 0; + + // Check if the block contains more than one stake tx from the same operator + let duplicate = block + .txns + .stake_txns + .iter() + .map(|stake_tx| &stake_tx.body.output.authorization.public_key) + .duplicates() + .next(); + + if let Some(duplicate) = duplicate { + return Err(BlockError::RepeatedStakeOperator { + pkh: duplicate.pkh(), + } + .into()); + } + + for transaction in &block.txns.stake_txns { + let (inputs, _output, fee, weight, change) = validate_stake_transaction( + transaction, + &utxo_diff, + epoch, + epoch_constants, + signatures_to_verify, + )?; + + total_fee += fee; + + // Update st weight + let acc_weight = st_weight.saturating_add(weight); + if acc_weight > MAX_STAKE_BLOCK_WEIGHT { + return Err(BlockError::TotalStakeWeightLimitExceeded { + weight: acc_weight, + max_weight: MAX_STAKE_BLOCK_WEIGHT, + } + .into()); + } + st_weight = acc_weight; + + let outputs = change.iter().collect_vec(); + update_utxo_diff(&mut utxo_diff, inputs, outputs, transaction.hash()); + + // Add new hash to merkle tree + st_mt.push(transaction.hash().into()); + + // TODO: Move validations to a visitor + // // Execute visitor + // if let Some(visitor) = &mut visitor { + // let transaction = Transaction::ValueTransfer(transaction.clone()); + // visitor.visit(&(transaction, fee, weight)); + // } + } + + let mut ut_mt = ProgressiveMerkleTree::sha256(); + let mut ut_weight: u32 = 0; + + for transaction in &block.txns.unstake_txns { + // TODO: get tx, default to compile + let st_tx = StakeTransaction::default(); + let (fee, weight) = + validate_unstake_transaction(transaction, &st_tx, &utxo_diff, epoch, epoch_constants)?; + + total_fee += fee; + + // Update ut weight + let acc_weight = ut_weight.saturating_add(weight); + if acc_weight > MAX_UNSTAKE_BLOCK_WEIGHT { + return Err(BlockError::TotalUnstakeWeightLimitExceeded { + weight: acc_weight, + max_weight: MAX_UNSTAKE_BLOCK_WEIGHT, + } + .into()); + } + ut_weight = acc_weight; + + // Add new hash to merkle tree + let txn_hash = transaction.hash(); + let Hash::SHA256(sha) = txn_hash; + ut_mt.push(Sha256(sha)); + + // TODO: Move validations to a visitor + // // Execute visitor + // if let Some(visitor) = &mut visitor { + // let transaction = Transaction::ValueTransfer(transaction.clone()); + // visitor.visit(&(transaction, fee, weight)); + // } + } + + // Nullify roots for legacy protocol version + // TODO skip all staking logic if protocol version is legacy + let (st_root, ut_root) = match get_protocol_version() { + ProtocolVersion::V1_6 => Default::default(), + _ => (Hash::from(st_mt.root()), Hash::from(ut_mt.root())), + }; + // Validate Merkle Root let merkle_roots = BlockMerkleRoots { mint_hash: block.txns.mint.hash(), @@ -1685,6 +1958,8 @@ pub fn validate_block_transactions( commit_hash_merkle_root: Hash::from(co_hash_merkle_root), reveal_hash_merkle_root: Hash::from(re_hash_merkle_root), tally_hash_merkle_root: Hash::from(ta_hash_merkle_root), + stake_hash_merkle_root: st_root, + unstake_hash_merkle_root: ut_root, }; if merkle_roots != block.block_header.merkle_roots { @@ -1851,6 +2126,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()), } } @@ -2123,6 +2406,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/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 6e401c586..3a3a5af52 100644 --- a/wallet/src/repository/wallet/mod.rs +++ b/wallet/src/repository/wallet/mod.rs @@ -1144,7 +1144,7 @@ where .map(Input::new) .collect_vec(); - let body = DRTransactionBody::new(pointers_as_inputs.clone(), outputs, request); + let body = DRTransactionBody::new(pointers_as_inputs.clone(), request, outputs); let sign_data = body.hash(); let signatures = self.create_signatures_from_inputs(pointers_as_inputs, sign_data, &mut state); @@ -1490,6 +1490,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(); diff --git a/wallet/src/types.rs b/wallet/src/types.rs index 84c9a9513..3f8e73a29 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -22,7 +22,8 @@ use witnet_data_structures::{ fee::Fee, transaction::{ CommitTransaction, DRTransaction, DRTransactionBody, MintTransaction, RevealTransaction, - TallyTransaction, Transaction, VTTransaction, VTTransactionBody, + StakeTransaction, TallyTransaction, Transaction, UnstakeTransaction, VTTransaction, + VTTransactionBody, }, utxo_pool::UtxoSelectionStrategy, }; @@ -322,6 +323,8 @@ pub enum TransactionHelper { Reveal(RevealTransaction), Tally(TallyTransaction), Mint(MintTransaction), + Stake(StakeTransaction), + Unstake(UnstakeTransaction), } impl From for TransactionHelper { @@ -337,6 +340,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 +361,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 +424,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) } }