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

Calculate and validate founders reward addresses #1170

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4a1a952
calculate founders reward address
oxarbitrage Oct 16, 2020
d2e9ccf
make founder rewards tests work in the testnet
oxarbitrage Oct 17, 2020
5cc6bd1
validate founders reward address
oxarbitrage Oct 20, 2020
d3b14ee
fix FounderAddressChangeInterval
oxarbitrage Oct 20, 2020
a9b0220
add and use `founders_reward_active()`
oxarbitrage Oct 20, 2020
b1e3b50
remove redundant check
oxarbitrage Oct 21, 2020
ddbc6d2
remove done TODOs, change error msg
oxarbitrage Oct 21, 2020
f4b5862
clean founders_reward_address()
oxarbitrage Oct 21, 2020
a90eb62
fixes after rebase
oxarbitrage Oct 22, 2020
4da927f
check for halving or canopy in founders_reward_active()
oxarbitrage Oct 22, 2020
7ff52bd
remove test_founders_address_ceiling()
oxarbitrage Oct 23, 2020
ec60d02
improve docs
oxarbitrage Oct 23, 2020
291bf4f
split founders reward validation error
oxarbitrage Oct 23, 2020
0b0a959
trigger the 3 errors in test
oxarbitrage Oct 23, 2020
98fee67
change canopy.pdf to protocol.pdf
oxarbitrage Oct 28, 2020
0c516ce
create and use check_script_form() for address validation
oxarbitrage Nov 7, 2020
77eea90
clippy
oxarbitrage Nov 7, 2020
976497c
refactor check_script_form()
oxarbitrage Nov 7, 2020
1f9f847
change check_script_form() docs
oxarbitrage Nov 7, 2020
616da09
change founders address test
oxarbitrage Nov 12, 2020
1b87ab9
fix founders address test
oxarbitrage Nov 16, 2020
dbc22f4
uncomment ignore
oxarbitrage Nov 16, 2020
ee76364
add init to founders reward address tests
oxarbitrage Nov 16, 2020
6c62321
remove unused import
oxarbitrage Feb 28, 2021
6dde02c
fix clippy
oxarbitrage Feb 28, 2021
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion zebra-chain/src/transparent.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
//! Transparent-related (Bitcoin-inherited) functionality.
#![allow(clippy::unit_arg)]

mod address;
pub mod address;
mod keys;
mod script;
mod serialize;

pub use address::Address;
pub use address::OpCode;
pub use script::Script;

#[cfg(any(test, feature = "proptest-impl"))]
Expand Down
15 changes: 15 additions & 0 deletions zebra-chain/src/transparent/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ pub enum Address {
},
}

/// Minimal subset of script opcodes.
/// Ported from https://github.com/zcash/librustzcash/blob/master/zcash_primitives/src/legacy.rs#L10
pub enum OpCode {
// stack ops
Dup = 0x76,

// bit logic
Equal = 0x87,
EqualVerify = 0x88,

// crypto
Hash160 = 0xa9,
CheckSig = 0xac,
}

impl fmt::Debug for Address {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut debug_struct = f.debug_struct("TransparentAddress");
Expand Down
1 change: 1 addition & 0 deletions zebra-consensus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ bls12_381 = "0.3.1"
futures = "0.3.13"
futures-util = "0.3.6"
metrics = "0.13.0-alpha.8"
num-integer = "0.1.44"
thiserror = "1.0.24"
tokio = { version = "0.3.6", features = ["time", "sync", "stream", "tracing"] }
tower = { version = "0.4", features = ["timeout", "util", "buffer"] }
Expand Down
28 changes: 18 additions & 10 deletions zebra-consensus/src/block/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use chrono::{DateTime, Utc};

use zebra_chain::{
block::{Block, Hash, Header, Height},
parameters::{Network, NetworkUpgrade},
parameters::Network,
transaction,
work::{difficulty::ExpandedDifficulty, equihash},
};
Expand Down Expand Up @@ -101,9 +101,6 @@ pub fn subsidy_is_valid(block: &Block, network: Network) -> Result<(), BlockErro
let coinbase = block.transactions.get(0).ok_or(SubsidyError::NoCoinbase)?;

let halving_div = subsidy::general::halving_divisor(height, network);
let canopy_activation_height = NetworkUpgrade::Canopy
.activation_height(network)
.expect("Canopy activation height is known");

// TODO: the sum of the coinbase transaction outputs must be less than or equal to the block subsidy plus transaction fees

Expand All @@ -115,18 +112,29 @@ pub fn subsidy_is_valid(block: &Block, network: Network) -> Result<(), BlockErro
)
} else if halving_div.count_ones() != 1 {
unreachable!("invalid halving divisor: the halving divisor must be a non-zero power of two")
} else if height < canopy_activation_height {
} else if subsidy::founders_reward::founders_reward_active(height, network) {
// Founders rewards are paid up to Canopy activation, on both mainnet and testnet
let founders_reward = subsidy::founders_reward::founders_reward(height, network)
.expect("invalid Amount: founders reward should be valid");
let matching_values = subsidy::general::find_output_with_amount(coinbase, founders_reward);

// TODO: the exact founders reward value must be sent as a single output to the correct address
if !matching_values.is_empty() {
Ok(())
} else {
Err(SubsidyError::FoundersRewardNotFound)?
let founders_reward_address =
subsidy::founders_reward::founders_reward_address(height, network)
.expect("we should have an address");
let matching_address =
subsidy::founders_reward::find_output_with_address(coinbase, founders_reward_address);

if matching_values.is_empty() {
return Err(SubsidyError::FoundersRewardAmountNotFound)?;
}
if matching_address.is_empty() {
return Err(SubsidyError::FoundersRewardAddressNotFound)?;
}
if matching_values != matching_address {
return Err(SubsidyError::FoundersRewardDifferentOutput)?;
}
Comment on lines +133 to 135
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be a set intersection test, not an equality test.

It is ok to:

  • send the correct value to a different address
  • send other values to the correct address
  • send multiple correct values to the correct address

As long as at least one correct value is sent to the correct address.

Please also add tests for all these cases.

As an alternative, we could checkpoint on Canopy, and delete this code. (But we'll probably want to re-use this code in the Funding Streams implementation, so we might as well fix it now.)


Ok(())
} else if halving_div < 4 {
// 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
Expand Down
184 changes: 179 additions & 5 deletions zebra-consensus/src/block/subsidy/founders_reward.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,40 @@
//!
//! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies

use num_integer::div_ceil;

use std::convert::TryFrom;
use std::str::FromStr;

use zebra_chain::{
amount::{Amount, Error, NonNegative},
block::Height,
parameters::Network,
parameters::{Network, NetworkUpgrade::*},
serialization::ZcashSerialize,
transaction::Transaction,
transparent::{Address, OpCode, Output, Script},
};

use crate::block::subsidy::general::{block_subsidy, halving_divisor};
use crate::parameters::subsidy::FOUNDERS_FRACTION_DIVISOR;
use crate::parameters::subsidy::*;

/// Returns `true` if we are in the founders reward period of the blockchain.
pub fn founders_reward_active(height: Height, network: Network) -> bool {
let canopy_activation_height = Canopy
.activation_height(network)
.expect("Canopy activation height is known");

// The Zcash Specification and ZIPs explain the end of the founders reward in different ways,
// because some were written before the set of Canopy network upgrade ZIPs was decided.
// These are the canonical checks recommended by `zcashd` developers.
height < canopy_activation_height && halving_divisor(height, network) == 1
}

/// `FoundersReward(height)` as described in [protocol specification §7.7][7.7]
///
/// [7.7]: 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 {
if founders_reward_active(height, network) {
// this calculation is exact, because the block subsidy is divisible by
// the FOUNDERS_FRACTION_DIVISOR until long after the first halving
block_subsidy(height, network)? / FOUNDERS_FRACTION_DIVISOR
Expand All @@ -26,16 +44,110 @@ pub fn founders_reward(height: Height, network: Network) -> Result<Amount<NonNeg
}
}

/// Function `FounderAddressChangeInterval` as specified in [protocol specification §7.8][7.8]
///
/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#foundersreward
pub fn founders_address_change_interval() -> Height {
let interval = div_ceil(
SLOW_START_SHIFT.0 + PRE_BLOSSOM_HALVING_INTERVAL.0,
FOUNDERS_ADDRESS_COUNT,
);
Height(interval)
}

/// Get the founders reward t-address for the specified block height as described in [protocol specification §7.8][7.8]
///
/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#foundersreward
pub fn founders_reward_address(height: Height, network: Network) -> Result<Address, Error> {
let blossom_height = Blossom
.activation_height(network)
.expect("blossom activation height should be available");

if !founders_reward_active(height, network) {
panic!("founders reward address lookup on invalid block: block is after canopy activation");
}

let mut adjusted_height = height;
if height >= blossom_height {
adjusted_height = Height(
blossom_height.0
+ ((height.0 - blossom_height.0) / (BLOSSOM_POW_TARGET_SPACING_RATIO as u32)),
);
}
Comment on lines +71 to +76
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like we do post-blossom height adjustments in a few different parts of the code.

Is there a way to abstract them into a new function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created a follow up ticket for this: #1311


let address_index = (adjusted_height.0 / founders_address_change_interval().0) as usize;
let addresses = match network {
Network::Mainnet => FOUNDERS_REWARD_ADDRESSES_MAINNET,
Network::Testnet => FOUNDERS_REWARD_ADDRESSES_TESTNET,
};
let address: Address =
Address::from_str(addresses[address_index]).expect("we should get a taddress here");

Ok(address)
}
/// 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.8][7.8]
///
/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#foundersreward.
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, 0x14_u8);
address_hash.insert(0, OpCode::Hash160 as u8);
address_hash.insert(address_hash.len(), OpCode::Equal as u8);
if lock_script.0.len() == address_hash.len() && lock_script == Script(address_hash) {
return true;
}
false
}

/// Returns a list of outputs in `Transaction`, which have a script address equal to `String`.
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()
}

#[cfg(test)]
mod test {
use super::*;
use color_eyre::Report;
use zebra_chain::parameters::NetworkUpgrade::*;

#[test]
fn test_founders_reward_active() -> Result<(), Report> {
founders_reward_active_for_network(Network::Mainnet)?;
founders_reward_active_for_network(Network::Testnet)?;

Ok(())
}

fn founders_reward_active_for_network(network: Network) -> Result<(), Report> {
let blossom_height = Blossom.activation_height(network).unwrap();
let canopy_height = Canopy.activation_height(network).unwrap();

assert_eq!(founders_reward_active(blossom_height, network), true);
assert_eq!(founders_reward_active(canopy_height, network), false);

Ok(())
}

#[test]
fn test_founders_reward() -> Result<(), Report> {
zebra_test::init();

let network = Network::Mainnet;
founders_reward_for_network(Network::Mainnet)?;
founders_reward_for_network(Network::Testnet)?;

Ok(())
}

fn founders_reward_for_network(network: Network) -> Result<(), Report> {
let blossom_height = Blossom.activation_height(network).unwrap();
let canopy_height = Canopy.activation_height(network).unwrap();

Expand All @@ -60,4 +172,66 @@ mod test {

Ok(())
}

#[test]
// TODO: Ignored because we loop through all the founders reward period block by block.
#[ignore]
fn test_founders_address() -> Result<(), Report> {
zebra_test::init();

founders_address_for_network(Network::Mainnet)?;
founders_address_for_network(Network::Testnet)?;

Ok(())
}

fn founders_address_for_network(network: Network) -> Result<(), Report> {
// Test if all the founders addresses are paid in the entire period
// where rewards are active.
let mut populated_addresses: Vec<Address> = Vec::new();
let mut hardcoded_addresses = FOUNDERS_REWARD_ADDRESSES_MAINNET.to_vec();

if network == Network::Testnet {
// The last two addresses are not used, because Canopy activated before
// the halving on testnet.
hardcoded_addresses = FOUNDERS_REWARD_ADDRESSES_TESTNET[0..46].to_vec();
}

// We test the entire founders reward period block by block, this takes some time.
let mut increment = SLOW_START_SHIFT.0;
while founders_reward_active(Height(increment), network) {
let founder_address = founders_reward_address(Height(increment), network)?;
if !populated_addresses.contains(&founder_address) {
populated_addresses.push(founder_address);
}
increment += 1;
}

assert_eq!(populated_addresses.len(), hardcoded_addresses.len());

for (index, addr) in populated_addresses.into_iter().enumerate() {
assert_eq!(
addr,
Address::from_str(hardcoded_addresses[index as usize]).expect("an address")
);
}

Ok(())
}

#[test]
fn test_founders_address_count() -> Result<(), Report> {
zebra_test::init();

assert_eq!(
FOUNDERS_REWARD_ADDRESSES_MAINNET.len() as u32,
FOUNDERS_ADDRESS_COUNT
);
assert_eq!(
FOUNDERS_REWARD_ADDRESSES_TESTNET.len() as u32,
FOUNDERS_ADDRESS_COUNT
);

Ok(())
}
}
Loading