Merge pull request #3370 from stacks-network/feat/3346

Feat/3346: epoch marker for LeaderBlockCommitOp
This commit is contained in:
Jude Nelson
2022-11-02 13:23:42 +00:00
committed by GitHub
7 changed files with 353 additions and 50 deletions

View File

@@ -86,6 +86,7 @@ jobs:
- tests::epoch_21::transition_adds_pay_to_contract
- tests::epoch_21::transition_adds_get_pox_addr_recipients
- tests::epoch_21::transition_removes_pox_sunset
- tests::epoch_21::transition_empty_blocks
steps:
- uses: actions/checkout@v2
- name: Download docker image

View File

@@ -1074,6 +1074,9 @@ pub mod test {
if epoch.epoch_id >= StacksEpochId::Epoch2_05 {
txop.memo = vec![STACKS_EPOCH_2_05_MARKER];
}
if epoch.epoch_id >= StacksEpochId::Epoch21 {
txop.memo = vec![STACKS_EPOCH_2_1_MARKER];
}
self.txs
.push(BlockstackOperationType::LeaderBlockCommit(txop.clone()));

View File

@@ -658,6 +658,56 @@ impl LeaderBlockCommitOp {
self.check_single_burn_output()
}
/// Check the epoch marker in a block-commit to make sure it matches the right epoch.
/// Valid in Stacks 2.05+
fn check_epoch_commit_marker(&self, marker: u8) -> Result<(), op_error> {
if self.memo.len() < 1 {
debug!(
"Invalid block commit";
"reason" => "no epoch marker byte given",
);
return Err(op_error::BlockCommitBadEpoch);
}
if self.memo[0] < marker {
debug!(
"Invalid block commit";
"reason" => "invalid epoch marker byte",
"marker_byte" => self.memo[0],
"expected_marker_byte" => marker,
);
return Err(op_error::BlockCommitBadEpoch);
}
Ok(())
}
/// Check the epoch marker in the block commit, given the epoch we're in
fn check_epoch_commit(&self, epoch_id: StacksEpochId) -> Result<(), op_error> {
match epoch_id {
StacksEpochId::Epoch10 => {
panic!("FATAL: processed block-commit pre-Stacks 2.0");
}
StacksEpochId::Epoch20 => {
// no-op, but log for helping node operators watch for old nodes
if self.memo.len() < 1 {
debug!(
"Soon-to-be-invalid block commit";
"reason" => "no epoch marker byte given",
);
} else if self.memo[0] < STACKS_EPOCH_2_05_MARKER {
debug!(
"Soon-to-be-invalid block commit";
"reason" => "invalid epoch marker byte",
"marker_byte" => self.memo[0],
);
}
Ok(())
}
StacksEpochId::Epoch2_05 => self.check_epoch_commit_marker(STACKS_EPOCH_2_05_MARKER),
StacksEpochId::Epoch21 => self.check_epoch_commit_marker(STACKS_EPOCH_2_1_MARKER),
}
}
pub fn check(
&self,
burnchain: &Burnchain,
@@ -860,46 +910,7 @@ impl LeaderBlockCommitOp {
self.block_height
));
match epoch.epoch_id {
StacksEpochId::Epoch10 => {
panic!("FATAL: processed block-commit pre-Stacks 2.0");
}
StacksEpochId::Epoch20 => {
// no-op, but log for helping node operators watch for old nodes
if self.memo.len() < 1 {
debug!(
"Soon-to-be-invalid block commit";
"reason" => "no epoch marker byte given",
);
} else if self.memo[0] < STACKS_EPOCH_2_05_MARKER {
debug!(
"Soon-to-be-invalid block commit";
"reason" => "invalid epoch marker byte",
"marker_byte" => self.memo[0],
"expected_marker_byte" => STACKS_EPOCH_2_05_MARKER
);
}
}
// Note: 2.05 and 2.1 share the same marker.
StacksEpochId::Epoch2_05 | StacksEpochId::Epoch21 => {
if self.memo.len() < 1 {
debug!(
"Invalid block commit";
"reason" => "no epoch marker byte given",
);
return Err(op_error::BlockCommitBadEpoch);
}
if self.memo[0] < STACKS_EPOCH_2_05_MARKER {
debug!(
"Invalid block commit";
"reason" => "invalid epoch marker byte",
"marker_byte" => self.memo[0],
"expected_marker_byte" => STACKS_EPOCH_2_05_MARKER
);
return Err(op_error::BlockCommitBadEpoch);
}
}
}
self.check_epoch_commit(epoch.epoch_id)?;
// good to go!
Ok(())
@@ -924,7 +935,7 @@ mod tests {
use crate::chainstate::stacks::StacksPublicKey;
use crate::core::{
StacksEpoch, StacksEpochId, PEER_VERSION_EPOCH_1_0, PEER_VERSION_EPOCH_2_0,
PEER_VERSION_EPOCH_2_05, STACKS_EPOCH_MAX,
PEER_VERSION_EPOCH_2_05, PEER_VERSION_EPOCH_2_1, STACKS_EPOCH_MAX,
};
use stacks_common::address::AddressHashMode;
use stacks_common::deps_common::bitcoin::blockdata::transaction::Transaction;
@@ -2900,7 +2911,7 @@ mod tests {
}
#[test]
fn test_epoch_marker_2_05() {
fn test_epoch_marker() {
let first_block_height = 121;
let first_burn_hash = BurnchainHeaderHash::from_hex(
"0000000000000000000000000000000000000000000000000000000000000001",
@@ -2923,6 +2934,7 @@ mod tests {
};
let epoch_2_05_start = 125;
let epoch_2_1_start = 130;
let mut rng = rand::thread_rng();
let mut buf = [0u8; 32];
@@ -2955,10 +2967,17 @@ mod tests {
StacksEpoch {
epoch_id: StacksEpochId::Epoch2_05,
start_height: epoch_2_05_start,
end_height: STACKS_EPOCH_MAX,
end_height: epoch_2_1_start,
block_limit: ExecutionCost::max_value(),
network_epoch: PEER_VERSION_EPOCH_2_05,
},
StacksEpoch {
epoch_id: StacksEpochId::Epoch21,
start_height: epoch_2_1_start,
end_height: STACKS_EPOCH_MAX,
block_limit: ExecutionCost::max_value(),
network_epoch: PEER_VERSION_EPOCH_2_1,
},
],
PoxConstants::test_default(),
true,
@@ -3126,6 +3145,122 @@ mod tests {
burn_header_hash: BurnchainHeaderHash([0x00; 32]), // to be filled in
};
let block_commit_post_2_1_valid = LeaderBlockCommitOp {
sunset_burn: 0,
block_header_hash: BlockHeaderHash([0x03; 32]),
new_seed: VRFSeed([0x04; 32]),
parent_block_ptr: 0,
parent_vtxindex: 0,
key_block_ptr: leader_key.block_height as u32,
key_vtxindex: leader_key.vtxindex as u16,
memo: vec![STACKS_EPOCH_2_1_MARKER],
commit_outs: vec![],
burn_fee: 12345,
input: (Txid([0; 32]), 0),
apparent_sender: BurnchainSigner {
public_keys: vec![StacksPublicKey::from_hex(
"024d8cdaef508d665dd9dd50ca7e9fbd9e7984ec8bfac8f02dea9f02a9232af1d7",
)
.unwrap()],
num_sigs: 1,
hash_mode: AddressHashMode::SerializeP2PKH,
},
txid: Txid([0x03; 32]),
vtxindex: 444,
block_height: epoch_2_1_start,
burn_parent_modulus: ((epoch_2_1_start - 1) % BURN_BLOCK_MINED_AT_MODULUS) as u8,
burn_header_hash: BurnchainHeaderHash([0x00; 32]), // to be filled in
};
let block_commit_post_2_1_valid_bigger_epoch = LeaderBlockCommitOp {
sunset_burn: 0,
block_header_hash: BlockHeaderHash([0x03; 32]),
new_seed: VRFSeed([0x04; 32]),
parent_block_ptr: 0,
parent_vtxindex: 0,
key_block_ptr: leader_key.block_height as u32,
key_vtxindex: leader_key.vtxindex as u16,
memo: vec![STACKS_EPOCH_2_1_MARKER + 1],
commit_outs: vec![],
burn_fee: 12345,
input: (Txid([0; 32]), 0),
apparent_sender: BurnchainSigner {
public_keys: vec![StacksPublicKey::from_hex(
"024d8cdaef508d665dd9dd50ca7e9fbd9e7984ec8bfac8f02dea9f02a9232af1d7",
)
.unwrap()],
num_sigs: 1,
hash_mode: AddressHashMode::SerializeP2PKH,
},
txid: Txid([0x13; 32]),
vtxindex: 444,
block_height: epoch_2_1_start,
burn_parent_modulus: ((epoch_2_1_start - 1) % BURN_BLOCK_MINED_AT_MODULUS) as u8,
burn_header_hash: BurnchainHeaderHash([0x00; 32]), // to be filled in
};
let block_commit_post_2_1_invalid_bad_memo = LeaderBlockCommitOp {
sunset_burn: 0,
block_header_hash: BlockHeaderHash([0x04; 32]),
new_seed: VRFSeed([0x05; 32]),
parent_block_ptr: 0,
parent_vtxindex: 0,
key_block_ptr: leader_key.block_height as u32,
key_vtxindex: leader_key.vtxindex as u16,
memo: vec![STACKS_EPOCH_2_1_MARKER - 1],
commit_outs: vec![],
burn_fee: 12345,
input: (Txid([0; 32]), 0),
apparent_sender: BurnchainSigner {
public_keys: vec![StacksPublicKey::from_hex(
"02b20f7d690afa0464d7eb17bdd86820261fb1acfdf489b2442a205a693da231ac",
)
.unwrap()],
num_sigs: 1,
hash_mode: AddressHashMode::SerializeP2PKH,
},
txid: Txid([0x04; 32]),
vtxindex: 445,
block_height: epoch_2_1_start,
burn_parent_modulus: ((epoch_2_1_start - 1) % BURN_BLOCK_MINED_AT_MODULUS) as u8,
burn_header_hash: BurnchainHeaderHash([0x00; 32]), // to be filled in
};
let block_commit_post_2_1_invalid_no_memo = LeaderBlockCommitOp {
sunset_burn: 0,
block_header_hash: BlockHeaderHash([0x05; 32]),
new_seed: VRFSeed([0x06; 32]),
parent_block_ptr: 0,
parent_vtxindex: 0,
key_block_ptr: leader_key.block_height as u32,
key_vtxindex: leader_key.vtxindex as u16,
memo: vec![],
commit_outs: vec![],
burn_fee: 12345,
input: (Txid([0; 32]), 0),
apparent_sender: BurnchainSigner {
public_keys: vec![StacksPublicKey::from_hex(
"02e371309f1c25abc5f00353d74632c6f5b95eb80e1e1edb9ba53e14b0d47bc0de",
)
.unwrap()],
num_sigs: 1,
hash_mode: AddressHashMode::SerializeP2PKH,
},
txid: Txid([0x05; 32]),
vtxindex: 446,
block_height: epoch_2_1_start,
burn_parent_modulus: ((epoch_2_1_start - 1) % BURN_BLOCK_MINED_AT_MODULUS) as u8,
burn_header_hash: BurnchainHeaderHash([0x00; 32]), // to be filled in
};
let all_leader_key_ops = vec![leader_key];
let all_block_commit_ops = vec![
@@ -3134,10 +3269,14 @@ mod tests {
(block_commit_post_2_05_valid_bigger_epoch, true),
(block_commit_post_2_05_invalid_bad_memo, false),
(block_commit_post_2_05_invalid_no_memo, false),
(block_commit_post_2_1_valid, true),
(block_commit_post_2_1_valid_bigger_epoch, true),
(block_commit_post_2_1_invalid_bad_memo, false),
(block_commit_post_2_1_invalid_no_memo, false),
];
let mut sn = SortitionDB::get_first_block_snapshot(db.conn()).unwrap();
for i in sn.block_height..(epoch_2_05_start + 2) {
for i in sn.block_height..(epoch_2_1_start + 2) {
eprintln!("Block {}", i);
let mut byte_pattern = [0u8; 32];
byte_pattern[24..32].copy_from_slice(&i.to_be_bytes());

View File

@@ -569,7 +569,7 @@ fn make_genesis_block_with_recipients(
},
key_block_ptr: 1, // all registers happen in block height 1
key_vtxindex: (1 + key_index) as u16,
memo: vec![STACKS_EPOCH_2_05_MARKER],
memo: vec![STACKS_EPOCH_2_1_MARKER],
new_seed: VRFSeed::from_proof(&proof),
commit_outs,
@@ -791,7 +791,7 @@ fn make_stacks_block_with_input(
},
key_block_ptr: 1, // all registers happen in block height 1
key_vtxindex: (1 + key_index) as u16,
memo: vec![STACKS_EPOCH_2_05_MARKER],
memo: vec![STACKS_EPOCH_2_1_MARKER],
new_seed: VRFSeed::from_proof(&proof),
commit_outs,

View File

@@ -176,7 +176,7 @@ use stacks::chainstate::stacks::{
use stacks::codec::StacksMessageCodec;
use stacks::core::mempool::MemPoolDB;
use stacks::core::FIRST_BURNCHAIN_CONSENSUS_HASH;
use stacks::core::STACKS_EPOCH_2_05_MARKER;
use stacks::core::STACKS_EPOCH_2_1_MARKER;
use stacks::cost_estimates::metrics::CostMetric;
use stacks::cost_estimates::metrics::UnitMetric;
use stacks::cost_estimates::UnitEstimator;
@@ -1285,7 +1285,7 @@ impl BlockMinerThread {
apparent_sender: sender,
key_block_ptr: key.block_height as u32,
key_vtxindex: key.op_vtxindex as u16,
memo: vec![STACKS_EPOCH_2_05_MARKER],
memo: vec![STACKS_EPOCH_2_1_MARKER],
new_seed: vrf_seed,
parent_block_ptr,
parent_vtxindex,

View File

@@ -18,7 +18,7 @@ use stacks::chainstate::stacks::{
};
use stacks::chainstate::{burn::db::sortdb::SortitionDB, stacks::db::StacksEpochReceipt};
use stacks::core::mempool::MemPoolDB;
use stacks::core::STACKS_EPOCH_2_05_MARKER;
use stacks::core::STACKS_EPOCH_2_1_MARKER;
use stacks::cost_estimates::metrics::UnitMetric;
use stacks::cost_estimates::UnitEstimator;
use stacks::net::atlas::AttachmentInstance;
@@ -1030,7 +1030,7 @@ impl Node {
apparent_sender: self.keychain.get_burnchain_signer(),
key_block_ptr: key.block_height as u32,
key_vtxindex: key.op_vtxindex as u16,
memo: vec![STACKS_EPOCH_2_05_MARKER],
memo: vec![STACKS_EPOCH_2_1_MARKER],
new_seed: vrf_seed,
parent_block_ptr,
parent_vtxindex,

View File

@@ -23,8 +23,10 @@ use stacks::core;
use stacks::chainstate::burn::db::sortdb::SortitionDB;
use stacks::chainstate::burn::distribution::BurnSamplePoint;
use stacks::chainstate::burn::operations::leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS;
use stacks::chainstate::burn::operations::leader_block_commit::OUTPUTS_PER_COMMIT;
use stacks::chainstate::burn::operations::BlockstackOperationType;
use stacks::chainstate::burn::operations::LeaderBlockCommitOp;
use stacks::chainstate::burn::operations::PreStxOp;
use stacks::chainstate::burn::operations::TransferStxOp;
@@ -39,7 +41,9 @@ use crate::stacks_common::address::AddressHashMode;
use crate::stacks_common::types::Address;
use crate::stacks_common::util::hash::{bytes_to_hex, hex_bytes};
use stacks_common::types::chainstate::BlockHeaderHash;
use stacks_common::types::chainstate::BurnchainHeaderHash;
use stacks_common::types::chainstate::VRFSeed;
use stacks_common::util::hash::Hash160;
use stacks_common::util::secp256k1::Secp256k1PublicKey;
@@ -1557,3 +1561,159 @@ fn transition_removes_pox_sunset() {
test_observer::clear();
channel.stop_chains_coordinator();
}
#[test]
#[ignore]
fn transition_empty_blocks() {
// very simple test to verify that the miner will keep making valid (empty) blocks after the
// transition. Really tests that the block-commits are well-formed before and after the epoch
// transition.
if env::var("BITCOIND_TEST") != Ok("1".into()) {
return;
}
let epoch_2_05 = 210;
let epoch_2_1 = 215;
let (mut conf, miner_account) = neon_integration_test_conf();
let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec();
epochs[1].end_height = epoch_2_05;
epochs[2].start_height = epoch_2_05;
epochs[2].end_height = epoch_2_1;
epochs[3].start_height = epoch_2_1;
conf.node.mine_microblocks = false;
conf.burnchain.max_rbf = 1000000;
conf.miner.first_attempt_time_ms = 5_000;
conf.miner.subsequent_attempt_time_ms = 10_000;
conf.node.wait_time_for_blocks = 0;
conf.burnchain.epochs = Some(epochs);
let keychain = Keychain::default(conf.node.seed.clone());
let mut btcd_controller = BitcoinCoreController::new(conf.clone());
btcd_controller
.start_bitcoind()
.map_err(|_e| ())
.expect("Failed starting bitcoind");
let mut btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None);
let http_origin = format!("http://{}", &conf.node.rpc_bind);
btc_regtest_controller.bootstrap_chain(201);
eprintln!("Chain bootstrapped...");
let mut run_loop = neon::RunLoop::new(conf.clone());
let blocks_processed = run_loop.get_blocks_processed_arc();
let channel = run_loop.get_coordinator_channel().unwrap();
thread::spawn(move || run_loop.start(None, 0));
// give the run loop some time to start up!
wait_for_runloop(&blocks_processed);
// first block wakes up the run loop
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
// first block will hold our VRF registration
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
let tip_info = get_chain_info(&conf);
let key_block_ptr = tip_info.burn_block_height as u32;
let key_vtxindex = 1; // nothing else here but the coinbase
// second block will be the first mined Stacks block
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
let burnchain = Burnchain::regtest(&conf.get_burn_db_path());
let mut bitcoin_controller = BitcoinRegtestController::new_dummy(conf.clone());
// these should all succeed across the epoch boundary
for _i in 0..15 {
// also, make *huge* block-commits with invalid marker bytes once we reach the new
// epoch, and verify that it fails.
let tip_info = get_chain_info(&conf);
// this block is the epoch transition?
let (chainstate, _) = StacksChainState::open(
false,
conf.burnchain.chain_id,
&conf.get_chainstate_path_str(),
None,
)
.unwrap();
let res = StacksChainState::block_crosses_epoch_boundary(
&chainstate.db(),
&tip_info.stacks_tip_consensus_hash,
&tip_info.stacks_tip,
)
.unwrap();
debug!(
"Epoch transition at {} ({}/{}) height {}: {}",
&StacksBlockHeader::make_index_block_hash(
&tip_info.stacks_tip_consensus_hash,
&tip_info.stacks_tip
),
&tip_info.stacks_tip_consensus_hash,
&tip_info.stacks_tip,
tip_info.burn_block_height,
res
);
if tip_info.burn_block_height == epoch_2_05 || tip_info.burn_block_height == epoch_2_1 {
assert!(res);
} else {
assert!(!res);
}
if tip_info.burn_block_height + 1 >= epoch_2_1 {
let burn_fee_cap = 100000000; // 1 BTC
let commit_outs = if !burnchain.is_in_prepare_phase(tip_info.burn_block_height + 1) {
vec![
PoxAddress::standard_burn_address(conf.is_mainnet()),
PoxAddress::standard_burn_address(conf.is_mainnet()),
]
} else {
vec![PoxAddress::standard_burn_address(conf.is_mainnet())]
};
// let's commit
let burn_parent_modulus =
(tip_info.burn_block_height % BURN_BLOCK_MINED_AT_MODULUS) as u8;
let op = BlockstackOperationType::LeaderBlockCommit(LeaderBlockCommitOp {
sunset_burn: 0,
block_header_hash: BlockHeaderHash([0xff; 32]),
burn_fee: burn_fee_cap,
input: (Txid([0; 32]), 0),
apparent_sender: keychain.get_burnchain_signer(),
key_block_ptr,
key_vtxindex,
memo: vec![0], // bad epoch marker
new_seed: VRFSeed([0x11; 32]),
parent_block_ptr: 0,
parent_vtxindex: 0,
// to be filled in
vtxindex: 0,
txid: Txid([0u8; 32]),
block_height: 0,
burn_header_hash: BurnchainHeaderHash::zero(),
burn_parent_modulus,
commit_outs,
});
let mut op_signer = keychain.generate_op_signer();
let res = bitcoin_controller.submit_operation(op, &mut op_signer, 1);
assert!(res, "Failed to submit block-commit");
}
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
}
let account = get_account(&http_origin, &miner_account);
assert_eq!(account.nonce, 16);
channel.stop_chains_coordinator();
}