2020-12-02 14:52 阅读 3

PoX Lockup and Delegation

This PR implements the PoX STX lock-up and PoX lock-up delegation logic. Nearly all new functional code is in Clarity, and nearly all new Rust code is tests.

In This PR:

What is working: * You can Stack your STX and enter your PoX burnchain address into the reward set * You can register as a PoX stacking delegate on behalf of zero or more other Stackers * You can vote to disable PoX for the next reward cycle * You, as a delegate, can delegate-stack a bunch of Stacker's combined STX to earn one or more placements of your shared PoX address into the reward set * The node can iterate through a given reward cycle's PoX burnchain addresses, and determine how many uSTX are locked up by each. * The node can determine how many liquid uSTX exist at all block heights, so it can then determine how many times a PoX burnchain address should be considered for a reward.

What is still to do for PoX lockups, and will be handled in a separate PR: * Bitcoin-encoded PoX lock-ups, withdrawals, and delegations

New Clarity features: * total-liquid-ustx is a new Clarity global, which evaluates to the number of liquid micro-STX that exist (i.e. all uSTX that are not subject to any lock-up period in the boot code). * is-in-regtest is a new Clarity global, which evaluates to true if the code is running in a test environment. This is helpful for tweaking Clarity code to run faster -- e.g. with smaller registration/reward windows.

How It Works

I did my best to keep with the spirit of SIP-007, but the exact mechanics of delegation are a little bit different than what is described. Non-delegated STX lock-up works as you'd expect.

All of the PoX lock-up code is written in Clarity, in src/chainstate/stacks/boot/pox.clar.

Non-Delegated STX Lock-up

A brief description of the API:

(define-public (stack-stx (amount-ustx uint)
                          (pox-addr (tuple (version uint) (hashbytes (buff 20))))
                          (lock-period uint)))

This method is the entry point for self-service Stacking -- the Stacker is tx-sender. The method takes the number of micro-STX to stack, a PoX burnchain address (encoded as a tuple containing a 1-byte address hash mode (0-3) and a 20-byte hash160), and the number of reward cycles for which to lock up the STX. If successful, this method transfers the uSTX into the contract, registers the PoX burnchain address in a way that the node can find it for the next reward cycle, and prevents the uSTX from being withdrawn until the lock period has passed.

Delegated Stacking

Delegated Stacking works as a three-step operation:

  1. The delegate principal registers itself and declares its PoX burnchain address, when its tenure starts (in burn blocks), and how many reward cycles it will be active for.

  2. Before the delegate's tenure begins, the delegate's clients lock up their STX and indicate that their delegate will be responsible for registering the PoX address.

  3. The delegate initiates the PoX lockup, which registers the PoX burnchain address weighted by all of its clients' individually-locked STX. So, if Alice and Bob lock up 10 STX each and indicate Danielle as their delegate, then Danielle's PoX burnchain address will have 20 STX tied to it.

The rational here is that a collection of users will work together offline to agree on a PoX burnchain address and recovery address on their own, and eventually register one of their number as a delegate with the agreed-upon information (step 1). After independently inspecting the delegate record in the chainstate, the users lock up their STX (step 2). Once enough users have done so, the delegate activates Stacking for the agreed-upon PoX address (step 3).

These operations are covered by the following API:

(define-public (register-delegate (pox-addr (tuple (version uint) (hashbytes (buff 20))))
                                  (tenure-burn-block-begin uint)
                                  (tenure-reward-cycles uint))))

This is step 1, called by the delegate. The delegate registers themselves with their PoX address, the first burnchain block height of their tenure, and the number of reward cycles the tokens will be locked for.

(define-public (delegate-stx (delegate principal)
                             (amount-ustx uint)))

This is step 2, called by the delegate's clients. This locks up the client's STX behind the given delegate's PoX address.

(define-public (delegate-stack-stx))

This is step 3, called by the delegate. This activates Stacking for all the clients who called delegate-stx for this delegate.

Usage Notes

  • STX unlocks are automatic. Once the lock-up period passes -- either the one specified in (stack-stx) or the one specified by the delegate in (register-delegate), the user's tokens return to their account automatically. Behind the scenes, the recombination is done lazily -- i.e. on the next time the account spends tokens (either via a transaction fee, a token transfer, a token burn, or another STX lock-up).
  • At most one lock-up per account. Once an account locks it STX, it cannot lock again until the STX unlock. Users who want to stack their tokens multiple times concurrently must first distribute them across multiple accounts.
  • Contracts can be Stackers. A contract can stack the STX it holds, and a contract can act as a delegate for other Stackers (be they contracts or standard principals).
  • Delegate addresses are globally unique, and can never be reused.
  • PoX burnchain addresses are globally unique as long as they are present in at least one pending reward cycle. They may be re-used once they are absent from all pending reward cycles.

Recovering the PoX Reward Set

The act of registering a PoX address includes the act of setting up the following data maps:

(define-map reward-cycle-pox-address-list
    ((reward-cycle uint) (index uint))
        (pox-addr (tuple (version uint) (hashbytes (buff 20))))
        (total-ustx uint)

(define-map reward-cycle-pox-address-list-len
    ((reward-cycle uint))
    ((len uint))

The reward-cycle-pox-address-list serves as a two-dimensional append-only array, where each "row" is the list of PoX addresses and their locked uSTX in a given (enumerated) reward cycle, and each "column" is its index. Registering a PoX address through stack-stx or delegate-stack-stx appends a PoX address and the number of locked uSTX to this data map, and internally, the Stacks node walks through the current reward cycle's PoX address and uSTX lock-up list (using reward-cycle-pox-address-list-len to know how many addresses to expect).

PoX addresses and their uSTX are loaded into each reward cycle "in advance" -- a single call to stack-stx or delegate-stack-stx will insert up to 12 rows into reward-cycle-pox-addresses (one for each reward cycle), so that the node will consider the PoX address for the next 1-12 reward cycles.

Rejecting PoX

This PR addresses #1755 by adding the following API call:

(define-public (reject-pox))

By calling this method, the caller votes with all of their STX tokens to reject PoX for the next reward cycle. If enough liquid STX tokens do this, then PoX is disabled in the next reward cycle. The method StacksChainState::get_reward_addresses takes into consideration how many STX tokens have rejected PoX, and will return an empty reward address set if enough of them do.

The amount of STX that must reject is a protocol-defined fraction of the total liquid STX. Currently, this is set to 25%. I have updated SIP-007 to reflect this in this PR.


  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享

13条回答 默认 最新

  • weixin_39832643 weixin_39832643 2020-12-02 14:52

    Before reviewing, please be aware of the following system limitations that we may want to consider addressing:

    • There's no way to increase the amount of tokens you've Stacked once you register your PoX address. This means that if the total liquid STX increases while you are Stacking, and your Stacked total falls beneath the stacking minimum for the reward cycle, your tokens remain locked but you will not receive a PoX reward. This could potentially be changed without too much effort, but I don't know yet if it's necessary -- a Stacker (or delegate) can determine in advance how many uSTX will need to be locked up a priori by looking at the coinbase release schedule and the lockup schedule.

    • Related to the above, this PR contains a stub lockup contract that contains some of the machinery for processing STX unlocks from token sales. This code will eventually be able to help Stackers determine the maximum number of liquid STX per future block, so they can Stack accordingly. Perhaps this would be preferable to build out, instead of making it possible to adjust the amount of locked STX once the user has Stacked?

    • The reward-cycle-pox-address-list will include PoX addresses that had at least the minimum uSTX locked to them at the time they were locked, but NOT the current minimum. This discrepancy manifests in two ways:

    • If Alice locks up STX before 25% of the total liquid STX are locked, and later on Bob locks up STX and pushes the total locked STX to over 25% of the total liquid STX, then Alice's PoX address will no longer have enough STX locked on it (and per the above, she can't increase it). However, her address will still show up in this map.
    • As more blocks are mined, and as more tokens unlock, the total liquid STX steadily increases. Because PoX addresses are registered to multiple reward cycles in this map when the STX are locked, it is possible that the total liquid STX will increase to the point that a PoX address no longer has the minimum required STX locked on it.

    As a result, the Stacks node will need to determine the current minimum STX lock requirement on each reward cycle, and filter out addresses pulled from this map that do not meet the minimum lockup.

    • The number of addresses the node will iterate through per reward cycle is bound by the protocol, but we don't exploit this in the current design. Specifically, a reward set can be no bigger than (5,000 * 0.75 + 20,000 * 0.25), or 8,750 PoX addresses, because only 20,000 * 0.25 = 5,000 addresses at the less-than-25%-lockup minimum can be registered at a lock-up minimum of 1/20,000th the total liquid STX before the lockup minimum is bumped to the greater-than-or-equal-to-25%-lockup minimum (meaning that there would then be space for at most 3,750 = 5,000 * 0.75 addresses with the 1/5000th liquid STX minimum lockup). Bound or not, the PoX lockup code needs to enforce to keep the maximum number of registered addresses small enough that it can be efficiently loaded (it currently does so by making sure a PoX address has enough locked STX on it at the time of the call before storing it). Without this enforcement, someone can DoS the node by locking up 1 uSTX/PoX-address to fill this map up to the point where loading all PoX addresses would take a long time. Right now, the code does not exploit this property to cap the size of the reward set explicitly; instead, the node treats each PoX address and its lock-up as a single key/value pair (meaning that obtaining each PoX address and its lock-up costs 1 MARF read + 1 Sqlite3 read, or 8,750 MARF'ed key lookups to obtain the reward set). The reason for this is that it keeps the writes cheap when registering a PoX address, which is important from a UX perspective because the user pays for them in transaction fees. An alternative design would be for the node to pre-allocate a massive 8,750-entry list per reward cycle, and have PoX address registration insert an entry into this list. This might be preferable to the current design because the node would be able to read the entire PoX reward set in a single (big) MARF'ed read. But, I don't know if this is possible given the write expense it imposes on users. Would love some feedback on which design would be overall better for users?
    点赞 评论 复制链接分享
  • weixin_39994665 weixin_39994665 2020-12-02 14:52

    I think the first point of contention with this approach is the requirement that STX be withdrawn once unlocked. There was some discussion around SIP-007 (and the thinking around how much to implement via smart contract) about whether or not the lockup should require transferring STX at all. I believe the conclusion was that STX lockup should be native to the blockchain -- an address doesn't transfer it's STX to lock them, and an address wouldn't need to send a second transaction to claim their now unlocked funds. Now, I don't think it's necessarily vital that the lockup be somehow native (i.e., an entry in the accounts table tells the VM how much STX an address owns but cannot transfer), but I think it is pretty important that this work without requiring withdraws.

    点赞 评论 复制链接分享
  • weixin_39832643 weixin_39832643 2020-12-02 14:52

    I think the first point of contention with this approach is the requirement that STX be withdrawn once unlocked. There was some discussion around SIP-007 (and the thinking around how much to implement via smart contract) about whether or not the lockup should require transferring STX at all. I believe the conclusion was that STX lockup should be native to the blockchain -- an address doesn't transfer it's STX to lock them, and an address wouldn't need to send a second transaction to claim their now unlocked funds. Now, I don't think it's necessarily vital that the lockup be somehow native (i.e., an entry in the accounts table tells the VM how much STX an address owns but cannot transfer), but I think it is pretty important that this work without requiring withdraws.

    I agree that this would be desirable, but I don't think it's achievable without some significant user-hostile constraints (see below). But regardless, the withdraw requirements are not described in SIP-007 (which is what I was going off of when writing this code), so I had to improvise here. I think once this is settled, SIP-007 should be updated to describe how withdrawals work.

    Regarding why I think this is unachievable as described, we need to think about just how many different accounts could be involved in an automated unlocking process. An individual unlock is going to take at least 1 MARF + 1 Sqlite3 write. Because we allow arbitrarily small amounts of STX to be combined through delegation, the I/O requirements for this grow unbounded. You could come up with an easy way to attack the system: (1) have a bunch of accounts (e.g. a basically-unbound number) delegate 1 uSTX to a single delegate over a long period of time, (2) Stack them all at once, and (3) when the delegate's STX unlock, the node would be required to refund all those accounts their 1 uSTX at the same block height, stalling the entire network for an attacker-controlled amount of time.

    If we want automatic unlocking, it will be necessary to restrict how many accounts can be auto-unlocked at the same time. None of the ways we can do this appeal to me, but here are some options:

    • Only automatically unlock STX if more than a certain threshold are stacked (so, only the rich get auto-unlock).
    • Apply a transaction fee a priori to Stacking, so the Stacker is forced to pay for their unlock later (but, I don't know how to calculate this, nor do I know how to price this right in the face of a dynamic, potentially-adversarial fee market).
    • Only allow a certain number of distinct Stacker accounts to be Stacking at once, across all delegates (but, this effectively means only the rich get to Stack, since they'll out-bid everyone else to be counted in this Stacker set).
    • Do phased unlocking according to some prioritization (but this screws over people who fall towards the end of the priority list -- their STX are locked up for potentially a long, unbound amount time, and unless we cap the maximum number of concurrent Stackers, this won't work).

    Let me know what you have in mind? If I had to pick one, I'd go with the first option, which I think is the "least hostile" option.

    点赞 评论 复制链接分享
  • weixin_39994665 weixin_39994665 2020-12-02 14:52

    Yes, those concerns are definitely valid. However, there are a couple of design constraints that we should be able to work with to get automated unlocks to work. First, there is a minimum threshold for rewards specified in SIP-007, which we can also use as the minimum threshold for Stacking locks. This would place a minimum threshold for a Stacking lockup at 1/20,000th of the STX circulating supply. Additionally, the minimum period for a Stacking lockup would be 1 reward cycle, which poses a fairly large barrier to using this maliciously -- not only do you need a large amount of STX to create the lockups, you also need to keep them idle for a large period of time. Finally, we could also explore a scheme that limited each STX address to exactly one active lockup at a time (they could add more STX to lockup or bump the lockup time, but they wouldn't be able to have multiple unlock times). In such a scheme, we could process unlocks lazily, i.e., at the time of a stx-transfer? call, because we'd at most need to perform one check.

    点赞 评论 复制链接分享
  • weixin_39832643 weixin_39832643 2020-12-02 14:52

    Comment to myself: This PR incorrectly assumes that the anchor block registration period is not overlapping with the last PoX reward cycle. This should not be the case -- the last 250 blocks of the PoX reward cycle are the registration period for the next PoX reward cycle. The code will need to be updated to reflect this.

    点赞 评论 复制链接分享
  • weixin_39832643 weixin_39832643 2020-12-02 14:52

    Comment to myself: ~~When #1787 merges, both the test framework and the PoX contract should load the reward cycle length from the Burnchain struct. There should be a method in the PoX contract that can only get called once that sets the reward cycle length, and the boot code installation method should invoke it from within the node to do so.~~

    Actually, the node should calculate what reward cycle we're in, and the Clarity VM should direct (get-current-reward-cycle) to the node's native method. The point is to make sure there's a single source of truth for this.

    点赞 评论 复制链接分享
  • weixin_39832643 weixin_39832643 2020-12-02 14:52

    Okay, this is now ready for review. Stacked tokens automatically unlock.

    点赞 评论 复制链接分享
  • weixin_39832643 weixin_39832643 2020-12-02 14:52

    Don't be intimidated by the size -- over 80% of the new lines are just tests ;) The PoX contract is only 700 lines of Clarity, including comments. The new code in StacksChainState is only a couple hundred lines, and there's a combined smattering of a couple hundred lines in src/vm to make it so tokens automatically unlock on spend.

    点赞 评论 复制链接分享
  • weixin_39832643 weixin_39832643 2020-12-02 14:52

    To merge, we'll go with a static minimum Stacking participation cut-off (instead of a dynamic adjusting minimum Stacking cut-off). We will need to re-visit whether or not we do static or dynamic cut-offs, and whether or not we automatically unlock Stackers who fail to get a single reward address.

    In addition, we'll need to reduce the initial liquid STX balance in testnet in order to ensure that miners can acquire enough STX in a reasonable amount of time to get 1/20,000th of the liquid supply. CC

    点赞 评论 复制链接分享
  • weixin_39649736 weixin_39649736 2020-12-02 14:52

    can we close this since it's superseded by #1850 ?

    点赞 评论 复制链接分享
  • weixin_39832643 weixin_39832643 2020-12-02 14:52

    No, I think this should remain open, but possibly in draft status. I don't think we should close this until we have consensus on the following design concerns

    • delegation
    • pooled Stacking
    • dynamic minimum Stacking threshold

    This PR addresses these things, whereas #1850 does not.

    点赞 评论 复制链接分享
  • weixin_39760389 weixin_39760389 2020-12-02 14:52

    I was under the impression that consensus was reached on the fact that this PR should not be addressing delegation (https://github.com/blockstack/stacks-blockchain/pull/1790#discussion_r479555066)? Same goes for stacking threshold (https://github.com/blockstack/stacks-blockchain/pull/1790#issuecomment-683844372). Are we switching gears?

    点赞 评论 复制链接分享
  • weixin_39832643 weixin_39832643 2020-12-02 14:52

    I meant, we need consensus on how these things are going to work. I think this code, and the discussion points in it, should remain visible until we have a plan for them. For example, it could be the case that we decide to use code in this PR after all (but we don't know this yet, because right now all of the above design points have been put in flux).

    点赞 评论 复制链接分享