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/exit at reward cycle contract #3035

Closed
wants to merge 29 commits into from
Closed

Conversation

pavitthrap
Copy link
Contributor

@pavitthrap pavitthrap commented Feb 4, 2022

This PR implements vote to exit network contract & related logic. It is built off of the ideas in this discussion: #2845

Overview of protocol

  • Stackers vote for a reward cycle at which the node should next exit.
  • For a vote to gain consensus in a particular cycle:
    • PoX must be active
    • 50% of stackers must have voted in the exit contract
    • Voting is aggregated as follows:
      • Before an exit height is chosen, a vote for RC N is interpreted as voting to exit at RC N or higher. If there is an equal number of votes for RC 10, 11, and 20, RC 11 will be selected as the exit height (those who voted for RC 10 will have their votes counted towards RC 11 in this example).
      • After some exit height H is chosen, only votes for block heights higher than H will be counted as valid in the election.
  • Once there is consensus for some reward cycle, the cycle after consensus was reached is a "veto" cycle.
  • Miners call the veto function to signal that they want to reject a particular proposal. 80% of the blocks in the veto reward cycle must contain a veto for the veto to pass.

Feedback

  • In process_ready_blocks and process_exit_reward_cycle, I currently do nothing if the node is unable to find the exit cycle info for a particular Stacks block. This should only happen for the first Stacks block. What should this behavior look like?

Future steps

  • Need to deploy the exit contract, and once I do so, I must "refer" to the contract differently within the code (issue Update contract identifier for exit contract #3034).
  • Need to think about whether miners will have difficulty getting their veto transactions in the current blocks. Might need to prioritize miner veto transactions?
  • Update SIP-015 draft to mention the contract

Testing

  • new tests in contract tests, chains coordinator tests, and added a neon_integrations test

@gregorycoppola
Copy link
Contributor

Thanks for important change.

What about these test failures? Are they flakes or real?

https://github.com/stacks-network/stacks-blockchain/runs/5072277005?check_suite_focus=true

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.

Lots of good code! Still reading. Had some interim comments.

src/burnchains/mod.rs Outdated Show resolved Hide resolved
src/burnchains/mod.rs Show resolved Hide resolved
@@ -320,6 +321,41 @@ pub struct PoxConstants {
_shadow: PhantomData<()>,
}

#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
Copy link
Contributor

Choose a reason for hiding this comment

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

can comment explain which part of the code will use this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

use what? The struct or these derived functions?

src/chainstate/coordinator/mod.rs Show resolved Hide resolved
src/chainstate/coordinator/mod.rs Outdated Show resolved Hide resolved
testnet/stacks-node/src/tests/neon_integrations.rs Outdated Show resolved Hide resolved
testnet/stacks-node/src/tests/neon_integrations.rs Outdated Show resolved Hide resolved
testnet/stacks-node/src/tests/neon_integrations.rs Outdated Show resolved Hide resolved
self.execute(sql, args)?;

if let Some(exit_proposal) = exit_at_reward_cycle_info.curr_exit_proposal {
let sql =
Copy link
Member

Choose a reason for hiding this comment

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

Should this verify that exactly one row was set? Is it possible that this UPDATE touches zero rows?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The logic right before this inserts a row with that block_id, and it should fail if the insert failed?

Do you think an assertion is still necessary here?

if stacks_block_height_in_cycle == 2 {
// We want to process the exit-at-rc contract reward cycle for the reward cycle the
// current block's parent's parent belongs to since the height (in number of Stacks
// blocks) of the current block in its reward cycle is 2.
Copy link
Member

Choose a reason for hiding this comment

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

It's not clear to me why this is? Why not process the votes in the first block of a reward cycle, or even the last block?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aaron felt it was a better idea to stall exit contract processing until after the first block of a reward cycle (just to avoid doing more computation in the first block of a reward cycle).

I don't do it in the last block of the reward cycle since that would invalidate the last veto.

Copy link
Member

Choose a reason for hiding this comment

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

Aaron felt it was a better idea to stall exit contract processing until after the first block of a reward cycle (just to avoid doing more computation in the first block of a reward cycle).

I'm not sure the trade-off is worth it here, though (cc @kantai). If you process the exit-at-rc logic at Stacks block height mod 2, then you have to introduce and test corner cases where you have a reward cycle with only one block. If we can instead make the PoX reward set calculation faster (something that's useful on its own anyway, and something that we could reasonably do), then the concern about doing it in the first reward cycle block goes away, and no corner cases are needed. This is all consensus-critical code, so if we can avoid less code even if it means a modest performance hit (or a need to spend some cycles optimizing code), then I think it's worth it. But would love to hear a strong counter-argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's discuss this in the architecture meeting

Copy link
Member

Choose a reason for hiding this comment

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

I can't come this week :/ Please let me know what y'all talk about

{
// This is an edge case in which the previous reward cycle contains only a single Stacks block.
// It is time to process the exit-at-rc contract for the reward cycle the current block's
// parent's parent belongs to.
Copy link
Member

Choose a reason for hiding this comment

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

If there's a reward cycle that contains only a single Stacks block, it might simplify things to simply declare that a vote tally will not occur. In such conditions, the network is already well and truly dead anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The processing occurs for the parent cycle (which could be any length) so not sure if we should introduce that rule.

(define-constant MINIMUM_RC_BUFFER_FROM_PRESENT u6)

;; Data vars
(define-data-var pox-reward-cycle-length uint POX_REWARD_CYCLE_LENGTH)
Copy link
Member

Choose a reason for hiding this comment

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

Instead of duplicating this here, would it be better if the contract simply pulled this information from .pox via get-pox-info?

Copy link
Member

Choose a reason for hiding this comment

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

This also applies to first-burnchain-block-height below.

Copy link
Member

Choose a reason for hiding this comment

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

It would also be more efficient to store all of these vars as a single tuple data variable, since then reading it would incur only one MARF read. The initial values for these variables could be instantiated directly from contract-call?'s to .pox.

Copy link
Contributor Author

@pavitthrap pavitthrap Mar 15, 2022

Choose a reason for hiding this comment

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

I avoided using get-pox-info since that function is defined as define-read-only (and is not callable).
I made them data vars to mimic the pattern in pox.clar - but I can change them to a tuple.

I think it could make sense to leave them separate since they are few in number, I would need a double get to get the value I require, and because I use them in different functions.

Copy link
Member

Choose a reason for hiding this comment

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

read-only methods are also callable by other contracts, so the initial values for these variables should be settable by doing something like (contract-call? .pox get-pox-info)

Copy link
Member

Choose a reason for hiding this comment

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

If these are settable via the pox get-pox-info call, then you should be able to switch them to constants (rather than data vars), which would simplify some of the contract code.

let mut total_votes = 0;
let invalid_reward_cycles: HashSet<u64> =
HashSet::from_iter(invalid_reward_cycles.into_iter());
let is_mainnet = self.burnchain.is_mainnet();
Copy link
Member

Choose a reason for hiding this comment

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

This method is broken, but we can't fix it because it's consensus-critical. Burnchain::is_mainnet() always returns false, since it compares the Stacks network ID to the Bitcoin network ID (oops).

Copy link
Member

@jcnelson jcnelson Apr 13, 2022

Choose a reason for hiding this comment

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

Can you add a docstring to Burnchain::is_mainnet() to say something to the effect of "BROKEN! DO NOT USE IN NEW PLACES!"?

Copy link
Contributor Author

@pavitthrap pavitthrap Apr 13, 2022

Choose a reason for hiding this comment

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

Should I make a new is_mainnet_function (like is_mainnet_v2) and use that instead? In that function I would compare the Stacks network ID against BITCOIN_NETWORK_ID_MAINNET (aka BITCOIN_MAINNET).

Copy link
Member

Choose a reason for hiding this comment

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

It doesn't look like this value is ever used in this method -- I think just remove this variable.

}
}
} else {
warn!(
Copy link
Member

Choose a reason for hiding this comment

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

This is bound to happen at least once, right? When this PR goes live on existing chainstate, no block is going to have any reward cycle exit info.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes - let's discuss this in the arch meeting

@jcnelson
Copy link
Member

Todo from architecture meeting:

  • Add to testnet checklist to instantiate this contract on testnet, and make sure the address is correct in the code

  • Consider deleting Burnchain::is_mainnet() (in a separate PR)

  • Need a good solution for deducing the exit reward cycle when starting a node from existing chainstate in the middle of a vote (in a separate PR)

@kantai kantai self-requested a review May 6, 2022 15:27
.burnchain
.exit_contract_constants
.rejection_confirmation_percent;
let is_mainnet = self.burnchain.is_mainnet();
Copy link
Member

Choose a reason for hiding this comment

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

It doesn't look like is_mainnet is ever used in this method -- can be removed.

@kantai
Copy link
Member

kantai commented May 6, 2022

This looks good to me, I think the only todos are a few superficial changes I noted, and then a larger change to make it so that the contract isn't a boot contract in testnet. That will require changing some of the tests, but that should be doable by just:

  1. Adding a contract publish transaction to the tests from some address that has funds in the tests.
  2. Removing the contract from the testnet burnchain config.

I don't think we need to have a contract address for the testnet burnchain config in this PR: a contract can be launched to testnet after this PR merges, and a subsequent PR would set that contract in the config.

.expect("BUG: failed to construct simple tuple"),
),
)
.expect("BUG: Failed querying rc-proposal-rejections")
Copy link
Member

Choose a reason for hiding this comment

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

Hmm. What happens if this is evaluated before the contract is launched? Does it panic?

@jcnelson
Copy link
Member

jcnelson commented Feb 7, 2023

We'll make the call on the future of this PR at the next architecture meeting.

@jcnelson jcnelson closed this Feb 7, 2023
@blockstack-devops
Copy link
Contributor

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@stacks-network stacks-network locked as resolved and limited conversation to collaborators Nov 14, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
frozen PRs that are on hold locked
Projects
Status: Status: ✅ Done
Development

Successfully merging this pull request may close these issues.

5 participants