-
Notifications
You must be signed in to change notification settings - Fork 671
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
Stacks 2.1: PoX 2 stack-extend
, delegate-stack-extend
#2755
Conversation
(err ERR_STACKING_PERMISSION_DENIED)) | ||
|
||
;; tx-sender principal must not be stacking | ||
(asserts! (is-none (get-stacker-info tx-sender)) |
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 code doesn't look any different from .pox
-- in particular, the get-stacker-info
code is the same, as are the checks here. Were any changes made in .pox-2
from .pox
, or is the goal of this PR just to add stack-extend
and delegate-stack-extend
without e.g. implementing continuous stacking?
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 only changes to PoX made in this PR are #2532 (stack-extend) and #2522 (fixing the contract allowance expiration).
stack-extend
is how continuous stacking is implemented -- a caller that is currently locked can call extend to lengthen their lockup and "restack" without the current cooldown period.
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.
@kantai would the following use case supported by this?
- Arkadiko has a smart contract where users deposit their STX tokens to borrow our stablecoin against that STX position (called an Arkadiko vault)
- As soon as the Arkadiko contract has the minimum amount of uSTX, we will call
stack-stx
with all the STX from vaults that signalled to stack in PoX (they all arrive in 1 address, so we're not really a pool but similar, we group STX tokens) - Whenever a new vault (i.e. new STX is deposited) is created or destroyed (i.e. STX gets withdrawn), we can call
stack-extend
to increase/decrease the amount of STX stacked in PoX through our contract. - At the end of each cycle, the Arkadiko address receives the btc
Any thoughts?
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.
That use case is intended to be serviced via the stack-increase
#2533, but it seems likely that the use case would also want to use stack-extend
and (potentially) stack-decrease
.
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.
Nice, great to hear that this will be possible. In the current implementation, we always stack 1 cycle and then have a cool down cycle, so there's no optimal usage of PoX & bitcoin yield within Arkadiko yet.
Stacks 2.1 is going to be a big improvement on this then 👍
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.
Back to Jude's question.. could this have been written as a module, that didn't copy-and-paste code, but instead just adds some new code?
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.
Back to Jude's question.. could this have been written as a module, that didn't copy-and-paste code, but instead just adds some new code?
I think this could be interpreted in two ways. First, could this have been written as a contract that just adds the new code? The answer is "yes" but with a huge caveat: the new contract would need to share its data-space with the original PoX contract -- i.e., it would need direct access to all the underlying data-var, data-map, etc. structures of the original PoX contract. This is problematic for a couple reasons. Firstly, it only works if those structures don't need schema changes. This is true for stack-extend
, but once we try introducing things like allowing unlocks or stacking increases, we either need to do schema changes, or introduce new auxiliary data-structures that would need to be updated/maintained through contract-call interposition, etc. Secondly, it would require special-casing the data-structure lookup code in the VM so that the new contract used the old contract's data-space. This is possible, but would introduce an otherwise unnecessary potential source for a pretty scary class of bugs (contracts that use prior contracts' data-spaces).
I think the second way to interpret this is as follows: could this have been written as a contract that just includes the code of the prior contract as a rust literal string include? The answer here is "probably", but I don't think it'd be the best way to implement the new contract. Doing that would obscure the contract implementation, and if the original functions end up being changed, it'd be hard to follow exactly how they're being changed (you'd need to mentally track the changes to the string inclusion).
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.
Back to Jude's question.. could this have been written as a module, that didn't copy-and-paste code, but instead just adds some new code?
I was not asking this. I very much agree with @kantai that having .pox-2
be the same as .pox
plus the new code is a lot less error-prone since you don't have to share data-spaces between contracts.
I was asking specifically if stack-stx
needed to change to accommodate continuous stacking. The answer is "no."
) | ||
|
||
|
||
(define-public (stack-extend (extend-count uint) |
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.
Can you add a method-level comment block to describe this new method, and indicate that it's a new change in 2.1?
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 probably makes more sense to document the method in the docs
module, but I can add a marker here to indicate that it is new in 2.1.
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 other methods are documented in this file
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.
Added documentation for these methods in bbd826d
(err ERR_STACKING_PERMISSION_DENIED)) | ||
|
||
;; tx-sender must be locked | ||
(asserts! (> amount-ustx u0) |
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.
Shouldn't we also be checking that this meets the minimum amount? I don't see a call to can-stack-stx
, for example.
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.
More generally -- do we want to permit someone to stack-extend
their locked STX indefinitely, even if the quantity stacked becomes smaller than the minimum amount? I don't think it matters much either way from a technical standpoint, but it does affect how the user finds out that they won't clinch a reward address -- they can either get an error here, or they can wait until a reward cycle completes where they have no reward slots.
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 not sure if it ultimately makes much difference in terms of alerting the user -- the can-stack-stx
check is performed at the time of the extension, so if the liquid supply of STX increases during their stacking (e.g., they stack for 12 cycles, but the supply increases during cycle 6), they wouldn't be alerted either way.
The biggest difference is in how the user needs to deal with the consequences of this. In any event, to meet the new threshold, they will need to invoke stack-increase
to increase their lock and stacking amount. However, if the can-stack-stx
check is in place, then it imposes an ordering constraint: they would need to invoke stack-increase
before extending.
I think it probably does make sense to add the check here, as it seems the least surprising behavior.
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.
Added can-stack-stx
and minimal-can-stack-stx
checks to stack-extend
and delegate-stack-extend
respectively.
(first-extend-cycle (burn-height-to-reward-cycle unlock-height)) | ||
(last-extend-cycle (- (+ first-extend-cycle extend-count) u1)) | ||
(cur-cycle (current-pox-reward-cycle)) | ||
(lock-period (- last-extend-cycle cur-cycle)) |
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 can't underflow either, right?
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 not so worried about an underflow -- it would just abort the tx with a runtime error. But we should check to make sure that extend-count is positive and first-extend-cycle is in the future.
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.
That's fair.
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.
Checks added in bbd826d
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.
Hey Aaron.. looks good but had some misc comments/questions.
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.
Huge change. I think it looks pretty good.. trying to follow along with what is tested, and make sure each of the edge cases in the ".clar" file is tested.
@@ -504,13 +504,22 @@ impl Burnchain { | |||
self.first_block_height + reward_cycle * (self.pox_constants.reward_cycle_length as u64) + 1 |
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.
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.
They don't absolutely have to be, but it seemed easy to address #2522 when pox-2 code was first introduced.
) | ||
|
||
|
||
(define-public (stack-extend (extend-count uint) |
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 other methods are documented in this file
(new-unlock-ht (reward-cycle-to-burn-height (+ u1 last-extend-cycle)))) | ||
|
||
;; must be called directly by the tx-sender or by an allowed contract-caller | ||
(asserts! (check-caller-allowed) |
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.
do these various asserts have corresponding tests?
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.
Yes, check-caller-allowed
is tested via unit tests in pox_2_contract_caller_units
.
@@ -228,6 +229,9 @@ impl fmt::Display for Error { | |||
Error::MemPoolError(ref s) => fmt::Display::fmt(s, f), | |||
Error::NoTransactionsToMine => write!(f, "No transactions to mine"), | |||
Error::PoxAlreadyLocked => write!(f, "Account has already locked STX for PoX"), | |||
Error::PoxExtendNotLocked => { | |||
write!(f, "Account has not already locked STX for PoX extend") |
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.
newbie question: is "stacking" == "locking"?
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.
yes, in this context
.0 | ||
.to_string(), | ||
"(err 2)".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.
Just be sure to add a test in here to make sure that you can't extend "into the past". stack-extend
should only allow you to extend into the future.
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.
Added a test in e0f6077 that covers the closest to this case that I think is possible -- it attempts to call stack-extend
after the lock for a user expired (that would be the only way to extend into the past). This case is caught early in invocation, because the user has no locked up funds.
…ck to delegate-stack-extend
Thanks for the reviews -- the existing comments have been addressed, and this PR is ready for review again. |
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.
Seems OK as far as I can tell at this point.
The notes I put about the unit tests can be ignored if not appropriate... I just tried to be exhaustive in listing them since we don't have the UI for it in GitHub anymore.
let mut snapshot = db.get_stx_balance_snapshot(principal); | ||
|
||
if !snapshot.has_locked_tokens() { | ||
return Err(Error::PoxExtendNotLocked); |
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 line isn't covered by unit tests
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.
That's not surprising -- this line shouldn't be reachable because the PoX smart contract checks that the account has locked tokens before getting to the line. This check is just defensive.
|
||
if !self.has_locked_tokens() { | ||
// caller needs to have checked this | ||
panic!("FATAL: account does not have locked tokens"); |
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 covered by unit tests.. kind of a drawback of panicing instead of InternalError is that you can't test this case
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 learned you actually can test panics in rust actually!.. not that you have to test this
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.
True, but in a lot of cases (like this one), we use the panic because this line should be unreachable.
)); | ||
} | ||
} | ||
Err(ChainstateError::DefunctPoxContract) => { |
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 tested in unit tests
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 don't think this error case is reachable -- parse_lock_extend_v2
doesn't throw DefunctPoxContract.
|
||
if unlock_burn_height <= self.burn_block_height { | ||
// caller needs to have checked this | ||
panic!("FATAL: cannot set a lock with expired unlock burn height"); |
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 covered by unit tests.. kind of a drawback of panicing instead of InternalError is that you can't test this case
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 learned you actually can test panics in rust actually!.. not that you have to test this
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.
True, but in a lot of cases (like this one), we use the panic because this line should be unreachable.
@@ -697,9 +705,9 @@ impl<'a> ClarityBlockConnection<'a> { | |||
// instantiate PoX 2 contract... | |||
|
|||
let pox_2_code = if mainnet { | |||
&*BOOT_CODE_POX_MAINNET | |||
&*POX_2_MAINNET_CODE |
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 line isn't covered by unit tests.. could be worth testing.
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.
Perhaps, but I think that we would need a separate PR to do this -- this is a general issue in the codebase that there is no testing infrastructure to exercise the mainnet conditions (because in general, when testing modes are enabled, every such branch is testnet).
Err(ChainstateError::DefunctPoxContract) => { | ||
return Err(Error::Runtime(RuntimeErrorType::DefunctPoxContract, None)) | ||
} | ||
Err(e) => { |
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.
can we put this answer in the comment?
src/vm/functions/special.rs
Outdated
|
||
return Ok(()); | ||
} else { | ||
// nothing to do -- the function failed |
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.
can we put this answer in the comment?
@@ -486,3 +489,938 @@ fn test_simple_pox_lockup_transition_pox_2() { | |||
"Charlie tx0 should have committed okay" | |||
); | |||
} | |||
|
|||
/// In this test case, two Stackers, Alice and Bob stack and interact with the |
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 like this explanation. Thanks.
tip.block_height, | ||
); | ||
|
||
let tip_index_block = peer.tenure_with_txs(&[alice_lockup], &mut coinbase_nonce); |
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.
Where is the run-time error?
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's checked in the receipts at line 851 -- transactions with runtime errors still get included in the block, but they just fail with a runtime error. The transaction receipt doesn't capture the actual runtime error thrown, but that's a deficiency in our transaction receipt format.
Thanks for an ambitious PR by the way! |
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, just had a few minor comments!
.0 | ||
.to_string(), | ||
"(err 9)".to_string(), | ||
"After revocation, stack-through shouldn't be an allowed caller for POX_ADDR[0] in the PoX2 contract", |
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.
POX_ADDR[0]
-> USER_KEYS[0]
?
|
||
;; standard can-stack-stx checks | ||
(try! (can-stack-stx pox-addr amount-ustx first-extend-cycle lock-period)) | ||
|
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.
Do we want to call check-pox-lock-period
in stack-extend
?
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 isn't necessary -- the lock-period
is checked by can-stack-stx
This implements #2532 and fixes #2522 in a new
pox-2
contract.Test coverage is supplied through two testing structures.
contract_tests
contains "unit" tests for these new functions and fixes. This tests the contract purely using a Clarity VM -- so it can check how the contract itself is working, but not how it interacts with the SortitionDB / ChainsCoordinator when constructing reward sets.test_pox_extend_transition_pox_2
in thepox_2_tests
module does integration testing for extension across the epoch boundary -- testing that a user that locks viapox
in Epoch 2.0 canstack-extend
inpox-2
in Epoch 2.1.