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

Feat/btc stx exchange rate #3142

Merged
merged 44 commits into from
Jun 8, 2022
Merged

Feat/btc stx exchange rate #3142

merged 44 commits into from
Jun 8, 2022

Conversation

jcnelson
Copy link
Member

This PR addresses #3043 by adding the following new fields to get-block-info? in Stacks 2.1:

  • block-reward: returns the total number of uSTX awarded to the miner once the block reward matures.

  • miner-spend-total: returns the total number of satoshis spent by all Stacks miners trying to win this block.

  • miner-spend-winner: returns the number of satoshis spent by the winning miner on this block.

The block-reward value is only (some ..) after the queried block receives MINER_REWARD_MATURITY + 1 confirmations. This is because the block reward can be slashed within the first MINER_REWARD_MATURITY blocks by a PoisonMicroblock transaction, should the miner publish two or more conflicting microblock streams during their tenure. Therefore, the system cannot reliably report the block reward until it is no longer possible to publish a PoisonMicroblock transaction.

The Stacks chainstate is not backwards-compatible with this PR. Specifically:

  • In order for this PR to faithfully report the block-reward, I had to make it so that the system stores the MinerReward structs that mature once a block is appended to the chainstate. The actual number of tokens credited to the miner isn't recorded anywhere; we just directly pay to the miner's account when processing matured rewards.

  • In order to get miner-spend-total to work, I had to fix [chainstate] burnchain_sortition_burn calculation is for the previous, not current sortition #2492 to store the right sortition burn for a Stacks block. While I don't believe this is a consensus-breaking change, since we don't appear to use the value anywhere, the fact that this value is different than what we have now means that we're gonna need to recalculate it.

I could go back and add schema migration code for these two things, but I'm hesitant to do so, since the time taken to write, test, and review the migration code will likely exceed the time to just boot up a 2.1 node from genesis.

… spent by the winning block, burnchain tokens spent by all miners in a block, and the number of uSTX awarded to the miner in a block once they mature
…tokens spent by miners and get the block reward
…y2 for querying the btc and stx exchange rate
…he matured block rewards are for each block. This data gets fed into `get-block-info?`. We store them on maturity, not on creation, so we can account for PoisonMicroblocks getting processed before the maturity window
…ich *shouldn't* affect consensus, since we don't use it anywhere, *but we will need to test it nevertheless!*). Also, plumb through matured miner rewards from block-processing so we can store them
…ge is *not* backwards-compatible! Stacks 2.1 nodes will need to boot from genesis if this change is merged.
…ds per block. N.b. the number of STX awarded isn't tested yet due to #3140, but will be fixed once #3140 is addressed
@jcnelson
Copy link
Member Author

In writing this code, I discovered a consensus bug (#3140) in how we calculate microblock transaction fees. I'm going to send another PR on top of this one that fixes it. I documented where this bug occurs in the comments in this PR.

@codecov
Copy link

codecov bot commented May 19, 2022

Codecov Report

Merging #3142 (00ca64b) into next (43d8fb5) will increase coverage by 0.19%.
The diff coverage is 92.15%.

@@            Coverage Diff             @@
##             next    #3142      +/-   ##
==========================================
+ Coverage   84.00%   84.19%   +0.19%     
==========================================
  Files         268      268              
  Lines      210798   212573    +1775     
==========================================
+ Hits       177077   178981    +1904     
+ Misses      33721    33592     -129     
Impacted Files Coverage Δ
clarity/src/vm/docs/mod.rs 86.30% <0.00%> (-1.99%) ⬇️
clarity/src/vm/test_util/mod.rs 56.42% <0.00%> (-5.30%) ⬇️
src/chainstate/stacks/mod.rs 75.03% <ø> (ø)
src/clarity_cli.rs 75.90% <0.00%> (-0.54%) ⬇️
stacks-common/src/libcommon.rs 100.00% <ø> (ø)
src/clarity_vm/database/mod.rs 83.94% <56.81%> (-4.69%) ⬇️
src/chainstate/stacks/db/accounts.rs 90.03% <77.25%> (-2.21%) ⬇️
src/chainstate/stacks/db/mod.rs 87.26% <81.25%> (-0.13%) ⬇️
clarity/src/vm/database/clarity_db.rs 89.98% <81.53%> (-0.43%) ⬇️
src/chainstate/stacks/db/blocks.rs 89.53% <90.00%> (+0.01%) ⬆️
... and 46 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1c7db10...00ca64b. Read the comment docs.

…nt's block's evaluated epoch is 2.1, we start using the child block's reported streamed tx fees to determine the parent's reward. We need to use the parent block's evaluated epoch, since a stream cannot cross an epoch boundary.
@jcnelson jcnelson mentioned this pull request May 19, 2022
…fee behavior, the new fee behavior (by way of the new get-block-info? fields), and the transition gating
…om two or more forks that share the same ancestor block whose reward is about to mature
Copy link
Contributor

@gregorycoppola gregorycoppola left a comment

Choose a reason for hiding this comment

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

Thanks for the change.. It's making sense to me but had a few questions.

@@ -474,6 +563,198 @@ impl StacksChainState {
Ok(())
}

/// Store a matured miner reward for subsequent query in Clarity, without doing any validation
fn inner_insert_matured_miner_payment<'a>(
Copy link
Contributor

Choose a reason for hiding this comment

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

inner_insert_matured_miner_reward is more in keeping with the naming convention, i think

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm deliberately distinguishing between rewards (which are to be paid) and payments (which are already paid).

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if this distinction is readily understandable. Maybe add a comment with this intended meaning.

Alternatively, you could use rewards / past rewards or payments / future payments

Copy link
Member

@kantai kantai Jun 1, 2022

Choose a reason for hiding this comment

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

I agree with the above, payment vs. reward isn't a readily understandable distinction here -- isn't matured already indicating that the reward has been paid? (Also, the code seems to ignore this distinction between "payment" and "reward", as the 4th argument is called reward and not payment).

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

src/clarity_vm/tests/contracts.rs Outdated Show resolved Hide resolved
src/clarity_vm/tests/contracts.rs Outdated Show resolved Hide resolved
src/chainstate/stacks/miner.rs Outdated Show resolved Hide resolved
src/clarity_vm/tests/contracts.rs Outdated Show resolved Hide resolved
BlockInfoProperty::lookup_by_name(block_info_prop_str).ok_or(CheckError::new(
CheckErrors::NoSuchBlockInfoProperty(block_info_prop_str.to_string()),
))?;
BlockInfoProperty::lookup_by_name_at_version(block_info_prop_str, &checker.clarity_version)
Copy link
Contributor

Choose a reason for hiding this comment

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

regarding this:

I could go back and add schema migration code for these two things, but I'm hesitant to do so, since the time taken to write, test, and review the migration code will likely exceed the time to just boot up a 2.1 node from genesis.

Are you comparing the time of the miners to your time? I mean it's fine to make this tradeoff, but not for the reason that the users' time is 1:1 substitutable with yours.

Copy link
Member Author

@jcnelson jcnelson May 22, 2022

Choose a reason for hiding this comment

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

It takes less than 30 hours to boot from genesis in the latest release. This is something you could kick off on a Friday and expect to be done on a Monday.

More generally, I think we want to get folks into the habit of spawning a node from genesis on each hard fork. It would:

  • reduce the codebase's technical debt by giving us a chance to remove the last epoch's schema migrations

  • reduce the testing burden of hard forks by only requiring us to verify that the new node can boot from genesis, as opposed to booting from all of the previously-supported epochs' chainstates.

  • de-risk the chance that a bug in the ever-growing sequence of schema migrations leads to a chain split

  • enable us to be able to make significant, principled, and even green-field changes to the database schemas without worrying about compatibility, thereby reducing development, review, and testing time for deploying them

I think it's fair to consider each new epoch of Stacks to be a wholly new blockchain, whose genesis state just happens to be derived from the last epoch's chainstate. In fact, I'd be supportive of shipping each hard fork with all of the blocks from the last epochs as a database, so they don't even need to be downloaded on boot-up (just re-processed and re-validated against the Bitcoin blockchain). This would certainly speed up the boot-up process.

@@ -252,10 +266,25 @@ fn test_get_block_info() {
&format!("{}", type_check_helper(&good_test).unwrap())
);
}
for (good_test_v210, expected_v210) in good.iter().zip(expected.iter()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to also iterate over good_v10 & expected_v10 in the clarity v2 context.

Also maybe want to run normal good/bad tests with the function type_check_helper_v1.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

@@ -272,6 +285,18 @@ impl HeadersDB for NullHeadersDB {
fn get_miner_address(&self, _id_bhh: &StacksBlockId) -> Option<StacksAddress> {
None
}
fn get_burnchain_tokens_spent_for_block(&self, _id_bhh: &StacksBlockId) -> Option<u128> {
None
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious as to why we are returning None in most of the methods in this trait implementation here.
More specifically, is this a place we could have better trait design? For example, if a method takes in a type that must implement the BurnStateDB trait, would we instead want to break that down into separate traits / use supertraits?

Copy link
Member Author

Choose a reason for hiding this comment

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

This implementation is meant to represent an empty database, so there is no state to return. If you need a unit test that relies on specific state returned for specific blocks, you'd just add a new implementation. We do this for HeadersDB already in several places.

Stacks block at the given Stacks chain height.

`id-header-hash`: This property returns a `(buff 32)` value containing the _index block hash_ of a Stacks block. This hash is globally unique, and is derived
from the block hash and the history of accepted PoX operations. This is also the block hash value you would pass into `(at-block)`.
Copy link
Contributor

Choose a reason for hiding this comment

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

"the history of accepted PoX operations" - I think this phrasing could be clearer to make this explanation more accessible

Copy link
Member Author

Choose a reason for hiding this comment

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

The alternative is to just say consensus_hash, which is at least as hard to explain (since it is derived from the history of PoX operations).

@@ -474,6 +563,198 @@ impl StacksChainState {
Ok(())
}

/// Store a matured miner reward for subsequent query in Clarity, without doing any validation
fn inner_insert_matured_miner_payment<'a>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if this distinction is readily understandable. Maybe add a comment with this intended meaning.

Alternatively, you could use rewards / past rewards or payments / future payments

Comment on lines 2164 to 2165
mature_miner_payouts: Option<(MinerReward, Vec<MinerReward>, MinerReward)>, // (miner, [users], parent)
mature_rewards_info: Option<MinerRewardInfo>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Could make this a struct -
would eliminate the panic here: let reward_info = mature_rewards_info.expect("FATAL: have mature payouts but no rewards info");

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines 10151 to 10152
#[test]
fn test_get_block_info_v210() {
Copy link
Member

Choose a reason for hiding this comment

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

Between this PR and #3144, there's almost 2,000 new lines of tests in this file, and these tests are really only sort of related to the miner module. Only about 2,000 lines of this 10k+ line file are implementation. Both of these things strongly suggest to me that the tests components of this file should be split out (i.e., the fact that tests are being added to this file without any changes otherwise in this module, and the fact that this file is now huge and almost entirely composed of tests). It doesn't have to happen in this PR, but I definitely believe it needs to happen.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, I'll open an issue.

Comment on lines 610 to 613
&format!("{}", &reward.coinbase),
&format!("{}", &reward.tx_fees_anchored),
&format!("{}", &reward.tx_fees_streamed_confirmed),
&format!("{}", &reward.tx_fees_streamed_produced),
Copy link
Member

Choose a reason for hiding this comment

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

x.to_string() is always the same as format!("{}", x)

Suggested change
&format!("{}", &reward.coinbase),
&format!("{}", &reward.tx_fees_anchored),
&format!("{}", &reward.tx_fees_streamed_confirmed),
&format!("{}", &reward.tx_fees_streamed_produced),
&reward.coinbase.to_string(),
&reward.tx_fees_anchored.to_string(),
&reward.tx_fees_streamed_confirmed.to_string(),
&reward.tx_fees_streamed_produced.to_string(),

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines +747 to +750
== &StacksBlockHeader::make_index_block_hash(
&FIRST_BURNCHAIN_CONSENSUS_HASH,
&FIRST_STACKS_BLOCK_HASH,
)
Copy link
Member

Choose a reason for hiding this comment

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

StacksBlockId::new is a more succinct way to create StacksBlockIds, and it conveys the type more clearly. I don't feel very strongly about actually enforcing this, but it's probably better practice to stick to one in our codebase.

Copy link
Member Author

Choose a reason for hiding this comment

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

I kinda feel like we should just remove all instances of StacksBlockHeader::make_index_block_hash() in one fell swoop in a separate PR. There's lots of them. What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I'm fine with that, but I'd prefer not to add new ones in the mean time

Copy link
Member

@kantai kantai left a comment

Choose a reason for hiding this comment

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

This all looks good to me, just some superficial comments.

@gregorycoppola
Copy link
Contributor

LGTM, but the other reviewers have left comments so I'll let them approve.

@jcnelson
Copy link
Member Author

jcnelson commented Jun 6, 2022

I'm ready for another pass @kantai @gregorycoppola @pavitthrap.

Copy link
Member

@kantai kantai left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@gregorycoppola gregorycoppola left a comment

Choose a reason for hiding this comment

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

Assuming tests pass

@jcnelson jcnelson merged commit 875eb49 into next Jun 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants