-
Notifications
You must be signed in to change notification settings - Fork 659
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
Conversation
…ock info properties
… 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.
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 Report
@@ 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
Continue to review full report at Codecov.
|
…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.
…eward's parent block's evaluated epoch
…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
There was a problem hiding this 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.
src/chainstate/stacks/db/accounts.rs
Outdated
@@ -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>( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
…confirm that we have 0 tx fees from them stored in the MinerReward structs
…s-network/stacks-blockchain into feat/btc-stx-exchange-rate
@@ -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()) { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)`. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
src/chainstate/stacks/db/accounts.rs
Outdated
@@ -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>( |
There was a problem hiding this comment.
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
src/chainstate/stacks/db/mod.rs
Outdated
mature_miner_payouts: Option<(MinerReward, Vec<MinerReward>, MinerReward)>, // (miner, [users], parent) | ||
mature_rewards_info: Option<MinerRewardInfo>, |
There was a problem hiding this comment.
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");
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
…ratuitous .expect()
…lock fee reporting (not a consensus bug since user burn supports are disabled)
src/chainstate/stacks/miner.rs
Outdated
#[test] | ||
fn test_get_block_info_v210() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/chainstate/stacks/db/accounts.rs
Outdated
&format!("{}", &reward.coinbase), | ||
&format!("{}", &reward.tx_fees_anchored), | ||
&format!("{}", &reward.tx_fees_streamed_confirmed), | ||
&format!("{}", &reward.tx_fees_streamed_produced), |
There was a problem hiding this comment.
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)
&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(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
== &StacksBlockHeader::make_index_block_hash( | ||
&FIRST_BURNCHAIN_CONSENSUS_HASH, | ||
&FIRST_STACKS_BLOCK_HASH, | ||
) |
There was a problem hiding this comment.
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 StacksBlockId
s, 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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this 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.
LGTM, but the other reviewers have left comments so I'll let them approve. |
I'm ready for another pass @kantai @gregorycoppola @pavitthrap. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming tests pass
Fix/microblock fees
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 receivesMINER_REWARD_MATURITY + 1
confirmations. This is because the block reward can be slashed within the firstMINER_REWARD_MATURITY
blocks by aPoisonMicroblock
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 aPoisonMicroblock
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 theMinerReward
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.