Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate funding stream addresses #3040

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions zebra-consensus/src/block/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ use zebra_chain::{
work::{difficulty::ExpandedDifficulty, equihash},
};

use crate::{error::*, parameters::SLOW_START_INTERVAL};
use crate::{
error::*,
parameters::{
subsidy::FundingStreamReceiver, FUNDING_STREAM_RECEIVERS_NUMBER, SLOW_START_INTERVAL,
},
};

use super::subsidy;

Expand Down Expand Up @@ -134,6 +139,7 @@ pub fn subsidy_is_valid(block: &Block, network: Network) -> Result<(), BlockErro
// 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");

Expand All @@ -143,17 +149,30 @@ pub fn subsidy_is_valid(block: &Block, network: Network) -> Result<(), BlockErro
.collect();
let output_amounts = subsidy::general::output_amounts(coinbase);

// funding stream addresses
let mut found_outputs = HashSet::<FundingStreamReceiver>::new();
for receiver in FundingStreamReceiver::receivers() {
let address =
subsidy::funding_streams::funding_stream_address(height, network, receiver);

let outputs = subsidy::funding_streams::find_output_with_address(coinbase, address);
if !outputs.is_empty() {
found_outputs.insert(receiver);
}
}

// 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(())
if found_outputs.len() == FUNDING_STREAM_RECEIVERS_NUMBER {
Ok(())
} else {
Err(SubsidyError::FundingStreamAddressNotFound)?
}
} else {
Err(SubsidyError::FundingStreamNotFound)?
Err(SubsidyError::FundingStreamValueNotFound)?
}
} else {
// Future halving, with no founders reward or funding streams
Expand Down
133 changes: 121 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,114 @@ 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_halving(network: Network) -> Height {
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
// First halving on Mainnet is at Canopy
// while in Testnet is at block 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 => Height(1_116_000),
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
}
}

/// 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 {
(height.0 + (POST_BLOSSOM_HALVING_INTERVAL.0) - (height_for_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_N_ADDRESSES_MAINNET,
Network::Testnet => FUNDING_STREAMS_N_ADDRESSES_TESTNET,
};

let index = 1u32
.checked_add(funding_stream_address_period(height, network))
.unwrap()
.checked_sub(funding_stream_address_period(
FUNDING_STREAM_HEIGHT_RANGES.get(&network).unwrap().start,
network,
))
.unwrap() as usize;
assert!(index > 0 && index <= num_addresses);

index
}

/// Return the address corresponding to this height for this funding stream receiver.
pub fn funding_stream_address(
height: Height,
network: Network,
receiver: FundingStreamReceiver,
) -> Address {
let index = funding_stream_address_index(height, network) - 1;

let address = match receiver {
FundingStreamReceiver::Ecc => match network {
Network::Mainnet => FUNDING_STREAM_ECC_ADDRESSES_MAINNET[index].to_string(),
Network::Testnet => FUNDING_STREAM_ECC_ADDRESSES_TESTNET[index].to_string(),
},
FundingStreamReceiver::ZcashFoundation => match network {
Network::Mainnet => FUNDING_STREAM_ZF_ADDRESSES_MAINNET[index].to_string(),
Network::Testnet => FUNDING_STREAM_ZF_ADDRESSES_TESTNET[index].to_string(),
},
FundingStreamReceiver::MajorGrants => match network {
Network::Mainnet => FUNDING_STREAM_MG_ADDRESSES_MAINNET[index].to_string(),
Network::Testnet => FUNDING_STREAM_MG_ADDRESSES_TESTNET[index].to_string(),
},
};
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);
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
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 find_output_with_address(transaction: &Transaction, address: Address) -> Vec<Output> {
transaction
.outputs()
.iter()
.filter(|o| check_script_form(o.lock_script.clone(), 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,
}
4 changes: 2 additions & 2 deletions zebra-consensus/src/block/subsidy/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
///
/// [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 Down
2 changes: 1 addition & 1 deletion zebra-consensus/src/block/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ fn funding_stream_validation_failure() -> Result<(), Report> {
// Validate it
let result = check::subsidy_is_valid(&block, network).unwrap_err();
let expected = BlockError::Transaction(TransactionError::Subsidy(
SubsidyError::FundingStreamNotFound,
SubsidyError::FundingStreamValueNotFound,
));
assert_eq!(expected, result);

Expand Down
7 changes: 5 additions & 2 deletions zebra-consensus/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ pub enum SubsidyError {
#[error("founders reward output not found")]
FoundersRewardNotFound,

#[error("funding stream output not found")]
FundingStreamNotFound,
#[error("funding stream output with value not found")]
FundingStreamValueNotFound,

#[error("funding stream output with address not found")]
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
FundingStreamAddressNotFound,
}

#[derive(Error, Clone, Debug, PartialEq, Eq)]
Expand Down
Loading