Skip to content

Commit

Permalink
Validate funding stream addresses (#3040)
Browse files Browse the repository at this point in the history
* validate funding stream addresses

* simplify a bit funder stream address check

* add integer division code comment

* document constant

* replace some unwraps

* fix some doc comments

* check at least one output has calculated address and amount

* create a convinient storage for funding stream addresses

* replace some unwraps

* docs: change `7.7` protocol sections to `7.8`

* change errors text

* change function name

* refactor `FundingStreamReceiver::receivers()`

* refactor FUNDING_STREAM_ADDRESSES

Co-authored-by: Janito Vaqueiro Ferreira Filho <[email protected]>

* remove a `clone()`

Co-authored-by: Janito Vaqueiro Ferreira Filho <[email protected]>

* fix consensus rule check

* use a constant for testnet first halving height

Co-authored-by: teor <[email protected]>

Co-authored-by: Janito Vaqueiro Ferreira Filho <[email protected]>
Co-authored-by: teor <[email protected]>
  • Loading branch information
3 people authored Nov 12, 2021
1 parent d6f3b3d commit d321e8f
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 55 deletions.
30 changes: 15 additions & 15 deletions zebra-consensus/src/block/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use chrono::{DateTime, Utc};
use std::collections::HashSet;

use zebra_chain::{
amount::{Amount, NonNegative},
block::{Block, Hash, Header, Height},
parameters::{Network, NetworkUpgrade},
transaction,
Expand Down Expand Up @@ -133,28 +132,29 @@ pub fn subsidy_is_valid(block: &Block, network: Network) -> Result<(), BlockErro
// Funding streams are paid from Canopy activation to the second halving
// Note: Canopy activation is at the first halving on mainnet, but not on testnet
// ZIP-1014 only applies to mainnet, ZIP-214 contains the specific rules for testnet

// funding stream amount values
let funding_streams = subsidy::funding_streams::funding_stream_values(height, network)
.expect("We always expect a funding stream hashmap response even if empty");

let funding_stream_amounts: HashSet<Amount<NonNegative>> = funding_streams
.iter()
.map(|(_receiver, amount)| *amount)
.collect();
let output_amounts = subsidy::general::output_amounts(coinbase);

// Consensus rule:[Canopy onward] The coinbase transaction at block height `height`
// MUST contain at least one output per funding stream `fs` active at `height`,
// that pays `fs.Value(height)` zatoshi in the prescribed way to the stream's
// recipient address represented by `fs.AddressList[fs.AddressIndex(height)]

// TODO: We are only checking each fundign stream reward is present in the
// coinbase transaction outputs but not the recipient addresses.
if funding_stream_amounts.is_subset(&output_amounts) {
Ok(())
} else {
Err(SubsidyError::FundingStreamNotFound)?
for (receiver, expected_amount) in funding_streams {
let address =
subsidy::funding_streams::funding_stream_address(height, network, receiver);

let has_expected_output =
subsidy::funding_streams::filter_outputs_by_address(coinbase, address)
.iter()
.map(zebra_chain::transparent::Output::value)
.any(|value| value == expected_amount);

if !has_expected_output {
Err(SubsidyError::FundingStreamNotFound)?;
}
}
Ok(())
} else {
// Future halving, with no founders reward or funding streams
Ok(())
Expand Down
4 changes: 2 additions & 2 deletions zebra-consensus/src/block/subsidy.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Validate coinbase transaction rewards as described in [§7.7][7.7]
//! Validate coinbase transaction rewards as described in [§7.8][7.8]
//!
//! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
//! [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
/// Founders' Reward functions apply for blocks before Canopy.
pub mod founders_reward;
Expand Down
8 changes: 4 additions & 4 deletions zebra-consensus/src/block/subsidy/founders_reward.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Founders' Reward calculations. - [§7.7][7.7]
//! Founders' Reward calculations. - [§7.8][7.8]
//!
//! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
//! [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
use std::convert::TryFrom;

Expand All @@ -13,9 +13,9 @@ use zebra_chain::{
use crate::block::subsidy::general::{block_subsidy, halving_divisor};
use crate::parameters::subsidy::FOUNDERS_FRACTION_DIVISOR;

/// `FoundersReward(height)` as described in [protocol specification §7.7][7.7]
/// `FoundersReward(height)` as described in [protocol specification §7.8][7.8]
///
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
pub fn founders_reward(height: Height, network: Network) -> Result<Amount<NonNegative>, Error> {
if halving_divisor(height, network) == 1 {
// this calculation is exact, because the block subsidy is divisible by
Expand Down
130 changes: 118 additions & 12 deletions zebra-consensus/src/block/subsidy/funding_streams.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
//! Funding Streams calculations. - [§7.7][7.7]
//! Funding Streams calculations. - [§7.8][7.8]
//!
//! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
//! [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
use zebra_chain::{
amount::{Amount, Error, NonNegative},
block::Height,
parameters::{Network, NetworkUpgrade::*},
serialization::ZcashSerialize,
transaction::Transaction,
transparent::{Address, Output, Script},
};

use crate::{
block::subsidy::general::block_subsidy,
parameters::subsidy::{
FundingStreamReceiver, FUNDING_STREAM_HEIGHT_RANGES, FUNDING_STREAM_RECEIVER_DENOMINATOR,
FUNDING_STREAM_RECEIVER_NUMERATORS,
},
};
use crate::{block::subsidy::general::block_subsidy, parameters::subsidy::*};

use std::{collections::HashMap, str::FromStr};

#[cfg(test)]
mod tests;

/// Returns the `fs.Value(height)` for each stream receiver
/// as described in [protocol specification §7.7][7.7]
/// as described in [protocol specification §7.8][7.8]
///
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
use std::collections::HashMap;
/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
pub fn funding_stream_values(
height: Height,
network: Network,
Expand All @@ -50,3 +48,111 @@ pub fn funding_stream_values(
}
Ok(results)
}

/// Returns the minumum height after the first halving
/// as described in [protocol specification §7.10][7.10]
///
/// [7.10]: https://zips.z.cash/protocol/protocol.pdf#fundingstreams
fn height_for_first_halving(network: Network) -> Height {
// First halving on Mainnet is at Canopy
// while in Testnet is at block constant height of `1_116_000`
// https://zips.z.cash/protocol/protocol.pdf#zip214fundingstreams
match network {
Network::Mainnet => Canopy
.activation_height(network)
.expect("canopy activation height should be available"),
Network::Testnet => FIRST_HALVING_TESTNET,
}
}

/// Returns the address change period
/// as described in [protocol specification §7.10][7.10]
///
/// [7.10]: https://zips.z.cash/protocol/protocol.pdf#fundingstreams
fn funding_stream_address_period(height: Height, network: Network) -> u32 {
// - Spec equation: `address_period = floor((height - height_for_halving(1) - post_blossom_halving_interval)/funding_stream_address_change_interval)`:
// https://zips.z.cash/protocol/protocol.pdf#fundingstreams
// - In Rust, "integer division rounds towards zero":
// https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#arithmetic-and-logical-binary-operators
// This is the same as `floor()`, because these numbers are all positive.
(height.0 + (POST_BLOSSOM_HALVING_INTERVAL.0) - (height_for_first_halving(network).0))
/ (FUNDING_STREAM_ADDRESS_CHANGE_INTERVAL.0)
}

/// Returns the position in the address slice for each funding stream
/// as described in [protocol specification §7.10][7.10]
///
/// [7.10]: https://zips.z.cash/protocol/protocol.pdf#fundingstreams
fn funding_stream_address_index(height: Height, network: Network) -> usize {
let num_addresses = match network {
Network::Mainnet => FUNDING_STREAMS_NUM_ADDRESSES_MAINNET,
Network::Testnet => FUNDING_STREAMS_NUM_ADDRESSES_TESTNET,
};

let index = 1u32
.checked_add(funding_stream_address_period(height, network))
.expect("no overflow should happen in this sum")
.checked_sub(funding_stream_address_period(
FUNDING_STREAM_HEIGHT_RANGES.get(&network).unwrap().start,
network,
))
.expect("no overflow should happen in this sub") as usize;

assert!(index > 0 && index <= num_addresses);
// spec formula will output an index starting at 1 but
// Zebra indices for addresses start at zero, return converted.
index - 1
}

/// Return the address corresponding to given height, network and funding stream receiver.
pub fn funding_stream_address(
height: Height,
network: Network,
receiver: FundingStreamReceiver,
) -> Address {
let index = funding_stream_address_index(height, network);
let address = &FUNDING_STREAM_ADDRESSES
.get(&network)
.expect("there is always another hash map as value for a given valid network")
.get(&receiver)
.expect("in the inner hash map there is always a vector of strings with addresses")[index];
Address::from_str(address).expect("Address should deserialize")
}

/// Given a founders reward address, create a script and check if it is the same
/// as the given lock_script as described in [protocol specification §7.10][7.10]
///
/// [7.10]: https://zips.z.cash/protocol/protocol.pdf#fundingstreams.
pub fn check_script_form(lock_script: &Script, address: Address) -> bool {
let mut address_hash = address
.zcash_serialize_to_vec()
.expect("we should get address bytes here");

address_hash = address_hash[2..22].to_vec();
address_hash.insert(0, OpCode::Push20Bytes as u8);
address_hash.insert(0, OpCode::Hash160 as u8);
address_hash.insert(address_hash.len(), OpCode::Equal as u8);
if lock_script.as_raw_bytes().len() == address_hash.len()
&& *lock_script == Script::new(&address_hash)
{
return true;
}
false
}

/// Returns a list of outputs in `Transaction`, which have a script address equal to `Address`.
pub fn filter_outputs_by_address(transaction: &Transaction, address: Address) -> Vec<Output> {
transaction
.outputs()
.iter()
.filter(|o| check_script_form(&o.lock_script, address))
.cloned()
.collect()
}

/// Script opcodes needed to compare the `lock_script` with the funding stream reward address.
pub enum OpCode {
Equal = 0x87,
Hash160 = 0xa9,
Push20Bytes = 0x14,
}
23 changes: 12 additions & 11 deletions zebra-consensus/src/block/subsidy/general.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Block and Miner subsidies, halvings and target spacing modifiers. - [§7.7][7.7]
//! Block and Miner subsidies, halvings and target spacing modifiers. - [§7.8][7.8]
//!
//! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
//! [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
use std::{collections::HashSet, convert::TryFrom};

Expand All @@ -16,9 +16,9 @@ use crate::parameters::subsidy::*;

/// The divisor used for halvings.
///
/// `1 << Halving(height)`, as described in [protocol specification §7.7][7.7]
/// `1 << Halving(height)`, as described in [protocol specification §7.8][7.8]
///
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
pub fn halving_divisor(height: Height, network: Network) -> u64 {
let blossom_height = Blossom
.activation_height(network)
Expand All @@ -43,9 +43,9 @@ pub fn halving_divisor(height: Height, network: Network) -> u64 {
}
}

/// `BlockSubsidy(height)` as described in [protocol specification §7.7][7.7]
/// `BlockSubsidy(height)` as described in [protocol specification §7.8][7.8]
///
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
pub fn block_subsidy(height: Height, network: Network) -> Result<Amount<NonNegative>, Error> {
let blossom_height = Blossom
.activation_height(network)
Expand All @@ -69,9 +69,9 @@ pub fn block_subsidy(height: Height, network: Network) -> Result<Amount<NonNegat
}
}

/// `MinerSubsidy(height)` as described in [protocol specification §7.7][7.7]
/// `MinerSubsidy(height)` as described in [protocol specification §7.8][7.8]
///
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
///
/// `non_miner_reward` is the founders reward or funding stream value.
/// If all the rewards for a block go to the miner, use `None`.
Expand Down Expand Up @@ -103,6 +103,7 @@ pub fn find_output_with_amount(
}

/// Returns all output amounts in `Transaction`.
#[allow(dead_code)]
pub fn output_amounts(transaction: &Transaction) -> HashSet<Amount<NonNegative>> {
transaction
.outputs()
Expand Down Expand Up @@ -131,7 +132,7 @@ mod test {
let blossom_height = Blossom.activation_height(network).unwrap();
let first_halving_height = match network {
Network::Mainnet => Canopy.activation_height(network).unwrap(),
// Based on "7.7 Calculation of Block Subsidy and Founders' Reward"
// Based on "7.8 Calculation of Block Subsidy and Founders' Reward"
Network::Testnet => Height(1_116_000),
};

Expand Down Expand Up @@ -218,7 +219,7 @@ mod test {
let blossom_height = Blossom.activation_height(network).unwrap();
let first_halving_height = match network {
Network::Mainnet => Canopy.activation_height(network).unwrap(),
// Based on "7.7 Calculation of Block Subsidy and Founders' Reward"
// Based on "7.8 Calculation of Block Subsidy and Founders' Reward"
Network::Testnet => Height(1_116_000),
};

Expand All @@ -244,7 +245,7 @@ mod test {
);

// After the 2nd halving, the block subsidy is reduced to 1.5625 ZEC
// See "7.7 Calculation of Block Subsidy and Founders' Reward"
// See "7.8 Calculation of Block Subsidy and Founders' Reward"
assert_eq!(
Amount::try_from(156_250_000),
block_subsidy(
Expand Down
2 changes: 1 addition & 1 deletion zebra-consensus/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub enum SubsidyError {
#[error("founders reward output not found")]
FoundersRewardNotFound,

#[error("funding stream output not found")]
#[error("funding stream expected output not found")]
FundingStreamNotFound,
}

Expand Down
Loading

0 comments on commit d321e8f

Please sign in to comment.