feat: add tenure height to block_headers table

Handle the `tenure-height` keyword in Clarity3, and using the same logic
for `block-height` in Clarity 1 or 2.
This commit is contained in:
Brice Dobry
2024-05-02 23:23:19 -04:00
parent 32111d20ab
commit 134afd3696
14 changed files with 219 additions and 12 deletions

View File

@@ -98,6 +98,7 @@ pub trait HeadersDB {
fn get_burnchain_tokens_spent_for_block(&self, id_bhh: &StacksBlockId) -> Option<u128>;
fn get_burnchain_tokens_spent_for_winning_block(&self, id_bhh: &StacksBlockId) -> Option<u128>;
fn get_tokens_earned_for_block(&self, id_bhh: &StacksBlockId) -> Option<u128>;
fn get_tenure_height_for_block(&self, id_bhh: &StacksBlockId) -> Option<u32>;
}
pub trait BurnStateDB {
@@ -183,6 +184,9 @@ impl HeadersDB for &dyn HeadersDB {
fn get_tokens_earned_for_block(&self, id_bhh: &StacksBlockId) -> Option<u128> {
(*self).get_tokens_earned_for_block(id_bhh)
}
fn get_tenure_height_for_block(&self, id_bhh: &StacksBlockId) -> Option<u32> {
(*self).get_tenure_height_for_block(id_bhh)
}
}
impl BurnStateDB for &dyn BurnStateDB {
@@ -334,6 +338,9 @@ impl HeadersDB for NullHeadersDB {
fn get_tokens_earned_for_block(&self, _id_bhh: &StacksBlockId) -> Option<u128> {
None
}
fn get_tenure_height_for_block(&self, _id_bhh: &StacksBlockId) -> Option<u32> {
None
}
}
#[allow(clippy::panic)]
@@ -1142,6 +1149,18 @@ impl<'a> ClarityDatabase<'a> {
pub fn set_stx_btc_ops_processed(&mut self, processed: u64) -> Result<()> {
self.put_data("vm_pox::stx_btc_ops::processed_blocks", &processed)
}
pub fn get_current_tenure_height(&mut self) -> Result<u32> {
let cur_stacks_height = self.store.get_current_block_height();
let last_mined_bhh = match self.get_index_block_header_hash(cur_stacks_height) {
Ok(x) => x,
Err(_) => return Ok(1),
};
// FIXME: This only works on a previous block, but not for a block that is currently being built.
self.headers_db
.get_tenure_height_for_block(&last_mined_bhh)
.ok_or_else(|| InterpreterError::Expect("Failed to get block data.".into()).into())
}
}
// poison-microblock

View File

@@ -2760,6 +2760,10 @@ mod test {
fn get_tokens_earned_for_block(&self, id_bhh: &StacksBlockId) -> Option<u128> {
Some(12000)
}
fn get_tenure_height_for_block(&self, id_bhh: &StacksBlockId) -> Option<u32> {
Some(100)
}
}
struct DocBurnStateDB {}

View File

@@ -196,6 +196,11 @@ impl HeadersDB for UnitTestHeaderDB {
// if the block is defined at all, then return a constant
self.get_burn_block_height_for_block(id_bhh).map(|_| 3000)
}
fn get_tenure_height_for_block(&self, id_bhh: &StacksBlockId) -> Option<u32> {
// if the block is defined at all, then return a constant
self.get_burn_block_height_for_block(id_bhh).map(|_| 100)
}
}
impl BurnStateDB for UnitTestBurnStateDB {

View File

@@ -145,3 +145,61 @@ fn test_stacks_block_height(
assert_eq!(Ok(Value::UInt(1)), eval_result);
}
}
#[apply(test_clarity_versions)]
fn test_tenure_height(
version: ClarityVersion,
epoch: StacksEpochId,
mut tl_env_factory: TopLevelMemoryEnvironmentGenerator,
) {
let contract = "(define-read-only (test-func) tenure-height)";
let mut placeholder_context =
ContractContext::new(QualifiedContractIdentifier::transient(), version);
let mut owned_env = tl_env_factory.get_env(epoch);
let contract_identifier = QualifiedContractIdentifier::local("test-contract").unwrap();
let mut exprs = parse(&contract_identifier, &contract, version, epoch).unwrap();
let mut marf = MemoryBackingStore::new();
let mut db = marf.as_analysis_db();
let analysis = db.execute(|db| {
type_check_version(&contract_identifier, &mut exprs, db, true, epoch, version)
});
if version < ClarityVersion::Clarity3 {
let err = analysis.unwrap_err();
assert_eq!(
CheckErrors::UndefinedVariable("tenure-height".to_string()),
err.err
);
} else {
assert!(analysis.is_ok());
}
// Initialize the contract
// Note that we're ignoring the analysis failure here so that we can test
// the runtime behavior. In Clarity 3, if this case somehow gets past the
// analysis, it should fail at runtime.
let result = owned_env.initialize_versioned_contract(
contract_identifier.clone(),
version,
contract,
None,
ASTRules::PrecheckSize,
);
let mut env = owned_env.get_exec_environment(None, None, &mut placeholder_context);
// Call the function
let eval_result = env.eval_read_only(&contract_identifier, "(test-func)");
// In Clarity 3, this should trigger a runtime error
if version < ClarityVersion::Clarity3 {
let err = eval_result.unwrap_err();
assert_eq!(
Error::Unchecked(CheckErrors::UndefinedVariable("tenure-height".to_string(),)),
err
);
} else {
assert_eq!(Ok(Value::UInt(1)), eval_result);
}
}

View File

@@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
use stacks_common::types::StacksEpochId;
use super::errors::InterpreterError;
use crate::vm::contexts::{Environment, LocalContext};
use crate::vm::costs::cost_functions::ClarityCostFunction;
@@ -97,10 +99,19 @@ pub fn lookup_reserved_variable(
Ok(Some(sponsor))
}
NativeVariables::BlockHeight => {
// FIXME: this needs to be updated to epoch 3.0 vs epoch 2.x
runtime_cost(ClarityCostFunction::FetchVar, env, 1)?;
let block_height = env.global_context.database.get_current_block_height();
Ok(Some(Value::UInt(block_height as u128)))
// In epoch 2.x, the `block-height` keyword returns the block height, but
// beginning in epoch 3, it returns the tenure height instead in order to
// maintain a similar pace to the block height advancement pre-Nakamoto. This
// keyword is removed in Clarity 3 to avoid confusion and replaced with
// `stacks-block-height` and `tenure-height`.
if env.global_context.epoch_id < StacksEpochId::Epoch30 {
let block_height = env.global_context.database.get_current_block_height();
Ok(Some(Value::UInt(block_height as u128)))
} else {
let tenure_height = env.global_context.database.get_current_tenure_height()?;
Ok(Some(Value::UInt(tenure_height as u128)))
}
}
NativeVariables::BurnBlockHeight => {
runtime_cost(ClarityCostFunction::FetchVar, env, 1)?;
@@ -137,12 +148,8 @@ pub fn lookup_reserved_variable(
}
NativeVariables::TenureHeight => {
runtime_cost(ClarityCostFunction::FetchVar, env, 1)?;
// FIXME: this is a placeholder and needs to be implemented correctly
let burn_block_height = env
.global_context
.database
.get_current_burnchain_block_height()?;
Ok(Some(Value::UInt(u128::from(burn_block_height))))
let tenure_height = env.global_context.database.get_current_tenure_height()?;
Ok(Some(Value::UInt(tenure_height as u128)))
}
}
} else {

View File

@@ -216,6 +216,16 @@ lazy_static! {
UPDATE db_config SET version = "4";
"#.into(),
];
pub static ref NAKAMOTO_CHAINSTATE_SCHEMA_2: Vec<String> = vec![
r#"
-- Add a `tenure_height` column to the block_headers table.
ALTER TABLE block_headers ADD COLUMN tenure_height INTEGER;
"#.into(),
r#"
UPDATE db_config SET version = "5";
"#.into(),
];
}
/// Matured miner reward schedules
@@ -2342,6 +2352,22 @@ impl NakamotoChainState {
let new_block_hash = new_tip.block_hash();
let index_block_hash = new_tip.block_id();
let parent_header_info =
NakamotoChainState::get_block_header(headers_tx.deref(), &new_tip.parent_block_id)
.and_then(|x| x.ok_or(ChainstateError::NoSuchBlockError))?;
let tenure_height = if let Some(th) = parent_header_info.tenure_height {
if new_tenure {
th + 1
} else {
th
}
} else {
// This can only be the case if the parent is an epoch 2.x block that was stored
// before the tenure height was added to the header info. In that case, the tenure
// height is the parent's block height + 1.
parent_header_info.stacks_block_height + 1
};
// store each indexed field
test_debug!("Headers index_put_begin {parent_hash}-{index_block_hash}");
let root_hash =
@@ -2353,6 +2379,7 @@ impl NakamotoChainState {
microblock_tail: None,
index_root: root_hash,
stacks_block_height: new_tip.chain_length,
tenure_height: Some(tenure_height),
consensus_hash: new_tip.consensus_hash.clone(),
burn_header_hash: new_burn_header_hash.clone(),
burn_header_height: new_burnchain_height,

View File

@@ -668,6 +668,7 @@ pub fn test_load_store_update_nakamoto_blocks() {
anchored_header: StacksBlockHeaderTypes::Epoch2(epoch2_header.clone()),
microblock_tail: None,
stacks_block_height: epoch2_header.total_work.work,
tenure_height: None,
index_root: TrieHash([0x56; 32]),
consensus_hash: epoch2_consensus_hash.clone(),
burn_header_hash: BurnchainHeaderHash([0x77; 32]),
@@ -768,6 +769,7 @@ pub fn test_load_store_update_nakamoto_blocks() {
anchored_header: StacksBlockHeaderTypes::Nakamoto(nakamoto_header.clone()),
microblock_tail: None,
stacks_block_height: nakamoto_header.chain_length,
tenure_height: Some(nakamoto_header.chain_length),
index_root: TrieHash([0x67; 32]),
consensus_hash: nakamoto_header.consensus_hash.clone(),
burn_header_hash: BurnchainHeaderHash([0x88; 32]),
@@ -812,6 +814,7 @@ pub fn test_load_store_update_nakamoto_blocks() {
anchored_header: StacksBlockHeaderTypes::Nakamoto(nakamoto_header_2.clone()),
microblock_tail: None,
stacks_block_height: nakamoto_header_2.chain_length,
tenure_height: Some(nakamoto_header.chain_length), // same as the first block in tenure
index_root: TrieHash([0x67; 32]),
consensus_hash: nakamoto_header_2.consensus_hash.clone(),
burn_header_hash: BurnchainHeaderHash([0x88; 32]),
@@ -851,6 +854,7 @@ pub fn test_load_store_update_nakamoto_blocks() {
anchored_header: StacksBlockHeaderTypes::Nakamoto(nakamoto_header_3.clone()),
microblock_tail: None,
stacks_block_height: nakamoto_header_2.chain_length,
tenure_height: Some(nakamoto_header.chain_length),
index_root: TrieHash([0x67; 32]),
consensus_hash: nakamoto_header_2.consensus_hash.clone(),
burn_header_hash: BurnchainHeaderHash([0x88; 32]),

View File

@@ -559,6 +559,20 @@ impl HeadersDB for TestSimHeadersDB {
// if the block is defined at all, then return a constant
self.get_burn_block_height_for_block(id_bhh).map(|_| 3000)
}
fn get_tenure_height_for_block(&self, id_bhh: &StacksBlockId) -> Option<u32> {
if *id_bhh == *FIRST_INDEX_BLOCK_HASH {
Some(0)
} else {
let input_height = test_sim_hash_to_height(&id_bhh.0)?;
if input_height > self.height {
eprintln!("{} > {}", input_height, self.height);
None
} else {
Some(input_height as u32)
}
}
}
}
#[test]

View File

@@ -125,6 +125,11 @@ impl StacksChainState {
let consensus_hash = &tip_info.consensus_hash;
let burn_header_hash = &tip_info.burn_header_hash;
let block_height = tip_info.stacks_block_height;
let tenure_height = if let Some(th) = tip_info.tenure_height {
th
} else {
tip_info.stacks_block_height
};
let burn_header_height = tip_info.burn_header_height;
let burn_header_timestamp = tip_info.burn_header_timestamp;
@@ -162,6 +167,7 @@ impl StacksChainState {
&block_size_str,
parent_id,
&u64_to_sql(affirmation_weight)?,
&u64_to_sql(tenure_height)?,
];
tx.execute("INSERT INTO block_headers \
@@ -186,8 +192,9 @@ impl StacksChainState {
cost,
block_size,
parent_block_id,
affirmation_weight) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22)", args)
affirmation_weight,
tenure_height) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23)", args)
.map_err(|e| Error::DBError(db_error::SqliteError(e)))?;
Ok(())

View File

@@ -53,7 +53,7 @@ use crate::chainstate::burn::operations::{
use crate::chainstate::burn::{ConsensusHash, ConsensusHashExtensions};
use crate::chainstate::nakamoto::{
HeaderTypeNames, NakamotoBlock, NakamotoBlockHeader, NakamotoChainState,
NakamotoStagingBlocksConn, NAKAMOTO_CHAINSTATE_SCHEMA_1,
NakamotoStagingBlocksConn, NAKAMOTO_CHAINSTATE_SCHEMA_1, NAKAMOTO_CHAINSTATE_SCHEMA_2,
};
use crate::chainstate::stacks::address::StacksAddressExtensions;
use crate::chainstate::stacks::boot::*;
@@ -184,6 +184,8 @@ pub struct StacksHeaderInfo {
pub microblock_tail: Option<StacksMicroblockHeader>,
/// Height of this Stacks block
pub stacks_block_height: u64,
/// Tenure height of this Stacks block
pub tenure_height: Option<u64>,
/// MARF root hash of the headers DB (not consensus critical)
pub index_root: TrieHash,
/// consensus hash of the burnchain block in which this miner was selected to produce this block
@@ -365,6 +367,7 @@ impl StacksHeaderInfo {
anchored_header: StacksBlockHeader::genesis_block_header().into(),
microblock_tail: None,
stacks_block_height: 0,
tenure_height: Some(0),
index_root: TrieHash([0u8; 32]),
burn_header_hash: burnchain_params.first_block_hash.clone(),
burn_header_height: burnchain_params.first_block_height as u32,
@@ -384,6 +387,7 @@ impl StacksHeaderInfo {
anchored_header: StacksBlockHeader::genesis_block_header().into(),
microblock_tail: None,
stacks_block_height: 0,
tenure_height: Some(0),
index_root: root_hash,
burn_header_hash: first_burnchain_block_hash.clone(),
burn_header_height: first_burnchain_block_height,
@@ -426,6 +430,7 @@ impl FromRow<DBConfig> for DBConfig {
impl FromRow<StacksHeaderInfo> for StacksHeaderInfo {
fn from_row<'a>(row: &'a Row) -> Result<StacksHeaderInfo, db_error> {
let block_height: u64 = u64::from_column(row, "block_height")?;
let tenure_height: Option<u64> = u64::from_column(row, "tenure_height")?;
let index_root = TrieHash::from_column(row, "index_root")?;
let consensus_hash = ConsensusHash::from_column(row, "consensus_hash")?;
let burn_header_hash = BurnchainHeaderHash::from_column(row, "burn_header_hash")?;
@@ -454,6 +459,7 @@ impl FromRow<StacksHeaderInfo> for StacksHeaderInfo {
anchored_header: stacks_header,
microblock_tail: None,
stacks_block_height: block_height,
tenure_height,
index_root,
consensus_hash,
burn_header_hash,
@@ -1079,6 +1085,13 @@ impl StacksChainState {
tx.execute_batch(cmd)?;
}
}
"4" => {
// migrate to clarity 3
info!("Migrating chainstate schema from version 4 to 5: Clarity 3 support");
for cmd in NAKAMOTO_CHAINSTATE_SCHEMA_2.iter() {
tx.execute_batch(cmd)?;
}
}
_ => {
error!(
"Invalid chain state database: expected version = {}, got {}",
@@ -2584,11 +2597,26 @@ impl StacksChainState {
&index_block_hash,
);
let parent_header_info = StacksChainState::get_stacks_block_header_info_by_consensus_hash(
headers_tx.deref(),
parent_consensus_hash,
)
.and_then(|x| x.ok_or(Error::NoSuchBlockError))?;
let tenure_height = if let Some(th) = parent_header_info.tenure_height {
th + 1
} else {
// This can only be the case if the parent is an epoch 2.x block that was stored
// before the tenure height was added to the header info. In that case, the tenure
// height is the parent's block height + 1.
parent_header_info.stacks_block_height + 1
};
let new_tip_info = StacksHeaderInfo {
anchored_header: new_tip.clone().into(),
microblock_tail: microblock_tail_opt,
index_root: root_hash,
stacks_block_height: new_tip.total_work.work,
tenure_height: Some(tenure_height),
consensus_hash: new_consensus_hash.clone(),
burn_header_hash: new_burn_header_hash.clone(),
burn_header_height: new_burnchain_height,

View File

@@ -1487,6 +1487,7 @@ impl StacksBlockBuilder {
anchored_header: StacksBlockHeader::genesis_block_header().into(),
microblock_tail: None,
stacks_block_height: 0,
tenure_height: Some(0),
index_root: TrieHash([0u8; 32]),
consensus_hash: genesis_consensus_hash.clone(),
burn_header_hash: genesis_burn_header_hash.clone(),

View File

@@ -730,6 +730,11 @@ impl HeadersDB for CLIHeadersDB {
// if the block is defined at all, then return a constant
get_cli_block_height(&self.conn(), id_bhh).map(|_| 3000)
}
fn get_tenure_height_for_block(&self, id_bhh: &StacksBlockId) -> Option<u32> {
// if the block is defined at all, then return a constant
get_cli_block_height(&self.conn(), id_bhh).map(|_| 100)
}
}
fn get_eval_input(invoked_by: &str, args: &[String]) -> EvalInput {

View File

@@ -107,6 +107,15 @@ impl<'a> HeadersDB for HeadersDBConn<'a> {
fn get_tokens_earned_for_block(&self, id_bhh: &StacksBlockId) -> Option<u128> {
get_matured_reward(self.0, id_bhh).map(|x| x.total().into())
}
fn get_tenure_height_for_block(&self, id_bhh: &StacksBlockId) -> Option<u32> {
get_stacks_header_column(self.0, id_bhh, "tenure_height", |r| {
u64::from_row(r)
.expect("FATAL: malformed tenure_height")
.try_into()
.expect("FATAL: blockchain too long")
})
}
}
impl<'a> HeadersDB for ChainstateTx<'a> {
@@ -184,6 +193,15 @@ impl<'a> HeadersDB for ChainstateTx<'a> {
fn get_tokens_earned_for_block(&self, id_bhh: &StacksBlockId) -> Option<u128> {
get_matured_reward(self.deref().deref(), id_bhh).map(|x| x.total().into())
}
fn get_tenure_height_for_block(&self, id_bhh: &StacksBlockId) -> Option<u32> {
get_stacks_header_column(self.deref().deref(), id_bhh, "tenure_height", |r| {
u64::from_row(r)
.expect("FATAL: malformed tenure_height")
.try_into()
.expect("FATAL: blockchain too long")
})
}
}
impl HeadersDB for MARF<StacksBlockId> {
@@ -261,6 +279,15 @@ impl HeadersDB for MARF<StacksBlockId> {
fn get_tokens_earned_for_block(&self, id_bhh: &StacksBlockId) -> Option<u128> {
get_matured_reward(self.sqlite_conn(), id_bhh).map(|x| x.total().into())
}
fn get_tenure_height_for_block(&self, id_bhh: &StacksBlockId) -> Option<u32> {
get_stacks_header_column(self.sqlite_conn(), id_bhh, "tenure_height", |r| {
u64::from_row(r)
.expect("FATAL: malformed tenure_height")
.try_into()
.expect("FATAL: blockchain too long")
})
}
}
fn get_stacks_header_column<F, R>(

View File

@@ -122,6 +122,7 @@ pub fn make_block(
microblock_tail: None,
index_root: TrieHash::from_empty_data(),
stacks_block_height: block_height,
tenure_height: Some(block_height),
consensus_hash: block_consensus.clone(),
burn_header_hash: BurnchainHeaderHash([0; 32]),
burn_header_height: burn_height as u32,