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/get pox addrs #3245

Merged
merged 65 commits into from
Aug 22, 2022
Merged

Feat/get pox addrs #3245

merged 65 commits into from
Aug 22, 2022

Conversation

jcnelson
Copy link
Member

@jcnelson jcnelson commented Aug 9, 2022

This PR implements #3155 by adding get-burn-block-info? pox-addrs <burn-height> to Clarity. It returns an optional tuple with two items:

  • addrs: The list of up to two PoX addresses (type (list 2 { hashbytes: (buff 20), version: (buff 1) }))
  • payout: The number of burnchain tokens (i.e. satoshis) paid to each PoX address across all block-commits (type uint).

It returns (some ..) when the sortition for burn-height is in the transaction's Stacks block's fork, and none if not.

Burn addresses are not reported. This method will return an empty addrs list during the reward phase if the BTC are to be burnt, and will return an empty addrs list during the prepare phase. EDIT: Burn addresses are reported -- up to two during the reward phase for unclaimed PoX reward slots, and one during the prepare phase.

The payout value is per TX output. During the reward phase, there are two outputs whose values are equal to half of the node's burn_fee_cap. This is true even if there are no more PoX reward addresses to pay. During the prepare phase, there is one output, and its value is equal to the node's burn_fee_cap. So for example if the network is spending 10,000 sats per block-commit, then payout will be 5,000 during the reward phase and 10,000 during the prepare phase.

In addition to implementing this feature, this PR replaces the use of StacksAddress to represent Bitcoin transaction outputs with a similar PoxAddress structure. This structure captures all of the fields in a pox-addr argument to stack-stx or delegate-stack-stx, which is not recoverable from just the Bitcoin chain state. It is these values that are passed back to the caller from get-burn-block-info? pox-addrs.

The addition of PoxAddress is not only necessary to represent the full PoX address data, but also lays the groundwork for implementing segwit (and taproot) addresses. These addresses cannot be represented in a StacksAddress, but because PoxAddress is an enum, we can later add type variants that represent any scriptPubKey we want.

jcnelson added 30 commits August 9, 2022 14:38
… the .pox or .pox-2 contracts, and add a trait method for getting the PoX addresses and payouts sent in a particular burn block height on the calling Stacks fork
…method for pulling the requisite information out of the BurnStateDB impl and converts it into a Clarity value)
…hain recipients; also, make prepare phase calculation a static method
…sses for this sortition as well as the per-output payout (which will then be reported via BurnStateDB implementations by querying the MARF)
…tacksAddress to represent PoX reward addresses
… other tests to use PoxAddress instead of StacksAddress)
…ithout uncommitted state from the underlying storage connection
…torage transaction state with only an immutable reference (required to make it possible to query the sortition DB MARF within a BurnStateDB impl)
Comment on lines 74 to 79
pub enum PoxAddress {
/// represents { version: (buff u1), hashbytes: (buff 20) }.
/// The address hash mode is optional because if we decode a legacy bitcoin address, we won't
/// be able to determine the hash mode since we can't distinguish segwit-p2sh from p2sh
Standard(StacksAddress, Option<AddressHashMode>),
}
Copy link
Member

Choose a reason for hiding this comment

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

I think this struct should be in the chainstate module, rather than in the Clarity module. I know there's some leakage of PoX into the Clarity module (mostly in the function signatures in the database), but most of these methods seem more important to the chainstate than it is to Clarity -- all Clarity really needs is the tuples:

/// Returns the paid out PoX addresses (as Clarity tuples of type `{ hashbytes: (buff 1), version: (buff 20) }` )
pub fn get_pox_payout_addrs_for_burnchain_height(
        &mut self,
        burnchain_block_height: u32,
    ) -> Option<(Vec<TupleData>, u128)> {

Whereas the chainstate module actually uses most of the methods of this struct.

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 can do that

Copy link
Member Author

Choose a reason for hiding this comment

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

Done: 49dea6e

…ve BurnStateDB::get_pox_payout_addrs() return Option<Vec<TupleData>, u128> instead of Option<Vec<PoxAddress>, u128>. This makes more sense since the chainstate module depends on PoxAddress far more so than Clarity. So, have the BurnStateDB return the tuple representation of a PoxAddress, and have the chainstate module convert that tuple into a PoxAddress once needed.
Comment on lines +762 to +773
pub fn pox_reward_set_payouts_key() -> String {
format!("sortition_db::reward_set::payouts")
}

pub fn pox_reward_set_payouts_value(addrs: Vec<PoxAddress>, payout_per_addr: u128) -> String {
serde_json::to_string(&(addrs, payout_per_addr)).unwrap()
}

pub fn pox_reward_set_payouts_decode(addr_str: &str) -> (Vec<PoxAddress>, u128) {
let addrs_and_payout: (Vec<PoxAddress>, u128) = serde_json::from_str(addr_str).unwrap();
addrs_and_payout
}
Copy link
Member

Choose a reason for hiding this comment

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

Does this value need to be stored in the MARF, or can it just be indexed by the SortitionID in a sqlite table?

Copy link
Member

@kantai kantai Aug 11, 2022

Choose a reason for hiding this comment

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

As in, lookups are always of the form what was the payout set at <sortition-id> (which doesn't need to traverse the sortition history), and they're never what was the payout set at <sortition-id> or if there wasn't one, what was the last payout prior? (which could only be answered through parent traversal).

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 suppose it doesn't need to be written to the MARF. I can just write it to a DB table -- it would make the I/O faster at least.

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

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 looks great to me, @jcnelson, but I think it may not be necessary to store the PoX payout information in the sortition MARF, but it could instead just be an entry in a sortition-id indexed table. It's up to you if you think the difference would be substantial enough to merit changes.

@jcnelson
Copy link
Member Author

I think I might want to update the API for this to return the burn addresses as well (so then it's obvious how much total BTC was spent, given the per-output spend)

@jcnelson
Copy link
Member Author

Need to make the return type easier to understand:

  • Return burn addresses explicitly
  • Return burn addresses in the prepare phase, so that the number of entries in the list reflects what block-commits are actually doing (so it's easier to interpret the payout field)

@jcnelson
Copy link
Member Author

Okay, the above is done. cc @kantai @gregorycoppola @pavitthrap for review/approval.

Comment on lines +177 to +184
let address = outputs[0]
.address
.clone()
.try_into_stacks_address()
.ok_or_else(|| {
test_debug!("Invalid address: not representable as a StacksAddress");
op_error::InvalidInput
})?;
Copy link
Member

Choose a reason for hiding this comment

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

Is this a consensus change?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nope. First, .address isn't even used in LeaderKeyRegister. I'm even going so far as to remove it in my segwit PR. Second, .try_into_stacks_address() is infallible right now, since the only PoxAddress variant (Standard(addr)) contains a StacksAddress.

Copy link
Member

Choose a reason for hiding this comment

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

In that case, this should panic -- as it is currently written, it suggests that an error would invalidate the LeaderKeyRegister (similarly for all the operations that this try_into_stacks_address() method is invoked on).

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 reason it's written this way is because .try_into_stacks_address() will soon be fallible, i.e. on the arrival of the native segwit support. The reason it will be fallible is because there will not be a way to convert a segwit address into a principal type, so parsing StackStxOp and TransferStxOp must fail here if the intended recipient can't be represented as a principal.

Comment on lines 88 to 92
pub fn bytes(&self) -> Vec<u8> {
match *self {
PoxAddress::Standard(addr, _) => addr.bytes.0.to_vec(),
}
}
Copy link
Member

Choose a reason for hiding this comment

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

I think it's better to return a &[u8] here. The caller can then decide if they want to copy to a vector or not. This gets used for sorting, so requiring a bunch of copies would be inefficient there.

Copy link
Member Author

Choose a reason for hiding this comment

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

If you're referring for StacksChainState::make_reward_set(), the problem with doing this the way you suggest is that the reference &addrs.bytes.0 doesn't live long enough in the closure passed to .sort_by_key(). Maybe we should keep this as-is and use .sort_by_cached_key()?

Copy link
Member

Choose a reason for hiding this comment

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

I guess that's fine to leave as is then.

Comment on lines +57 to +59
/// A PoX address as seen by the .pox and .pox-2 contracts.
/// Used by the sortition DB and chains coordinator to extract addresses from the PoX contract to
/// build the reward set and to validate block-commits.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add "This comprises a larger set of possible addresses than StacksAddress"?

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, will do

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

Comment on lines 75 to 92
pub fn hashmode(&self) -> Option<AddressHashMode> {
match *self {
PoxAddress::Standard(_, hm) => hm.clone(),
}
}

#[cfg(any(test, feature = "testing"))]
pub fn version(&self) -> u8 {
self.hashmode()
.expect("FATAL: tried to load the hashmode of a PoxAddress which has none known")
as u8
}

pub fn bytes(&self) -> Vec<u8> {
match *self {
PoxAddress::Standard(addr, _) => addr.bytes.0.to_vec(),
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Can you add rustdocs to the public functions of this struct? I think as it abstracts multiple kinds of addresses, functions like "bytes" are less self-explanatory.

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, will do

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

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 looks good to me, I think there's just some superficial comments remaining: slight tweaks to an interface and some additional rustdocs.

Copy link
Contributor

@pavitthrap pavitthrap 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

Choose a reason for hiding this comment

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

remove dead code

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

@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 17, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants