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

Stacks 2.1: PoX 2 stack-extend, delegate-stack-extend #2755

Merged
merged 21 commits into from
Aug 27, 2021
Merged

Conversation

kantai
Copy link
Member

@kantai kantai commented Jun 30, 2021

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 the pox_2_tests module does integration testing for extension across the epoch boundary -- testing that a user that locks via pox in Epoch 2.0 can stack-extend in pox-2 in Epoch 2.1.

(err ERR_STACKING_PERMISSION_DENIED))

;; tx-sender principal must not be stacking
(asserts! (is-none (get-stacker-info tx-sender))
Copy link
Member

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?

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 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.

Copy link

@philiphacks philiphacks Jul 15, 2021

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?

  1. Arkadiko has a smart contract where users deposit their STX tokens to borrow our stablecoin against that STX position (called an Arkadiko vault)
  2. 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)
  3. 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.
  4. At the end of each cycle, the Arkadiko address receives the btc

Any thoughts?

Copy link
Member Author

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.

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 👍

Copy link
Contributor

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?

Copy link
Member Author

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).

Copy link
Member

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)
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 a method-level comment block to describe this new method, and indicate that it's a new change in 2.1?

Copy link
Member Author

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.

Copy link
Contributor

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

Copy link
Member Author

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)
Copy link
Member

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.

Copy link
Member

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.

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'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.

Copy link
Member Author

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))
Copy link
Member

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?

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'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.

Copy link
Member

Choose a reason for hiding this comment

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

That's fair.

Copy link
Member Author

Choose a reason for hiding this comment

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

Checks added in bbd826d

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.

Hey Aaron.. looks good but had some misc comments/questions.

src/chainstate/stacks/boot/mod.rs Outdated Show resolved Hide resolved
src/vm/docs/mod.rs Outdated Show resolved Hide resolved
src/vm/functions/special.rs Show resolved Hide resolved
src/chainstate/stacks/boot/contract_tests.rs Show resolved Hide resolved
src/chainstate/stacks/boot/pox_2_tests.rs Show resolved Hide resolved
src/chainstate/stacks/boot/pox_2_tests.rs Show resolved Hide resolved
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.

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.

src/chainstate/stacks/boot/pox_2_tests.rs Outdated Show resolved Hide resolved
src/chainstate/stacks/boot/pox_2_tests.rs Outdated Show resolved Hide resolved
src/chainstate/stacks/boot/pox_2_tests.rs Outdated Show resolved Hide resolved
@@ -504,13 +504,22 @@ impl Burnchain {
self.first_block_height + reward_cycle * (self.pox_constants.reward_cycle_length as u64) + 1
Copy link
Contributor

Choose a reason for hiding this comment

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

Do the implementations of #2532 and #2522 have to be done in the same PR?

Copy link
Member Author

@kantai kantai Aug 20, 2021

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.

src/vm/functions/special.rs Show resolved Hide resolved
)


(define-public (stack-extend (extend-count uint)
Copy link
Contributor

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)
Copy link
Contributor

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?

Copy link
Member Author

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")
Copy link
Contributor

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"?

Copy link
Member

Choose a reason for hiding this comment

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

yes, in this context

src/vm/database/structures.rs Show resolved Hide resolved
src/burnchains/burnchain.rs Show resolved Hide resolved
src/chainstate/stacks/boot/pox_2_tests.rs Outdated Show resolved Hide resolved
src/chainstate/stacks/boot/pox_2_tests.rs Show resolved Hide resolved
src/vm/functions/special.rs Show resolved Hide resolved
src/chainstate/stacks/boot/pox_2_tests.rs Show resolved Hide resolved
src/chainstate/stacks/boot/pox_2_tests.rs Outdated Show resolved Hide resolved
.0
.to_string(),
"(err 2)".to_string()
);
Copy link
Member

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.

Copy link
Member Author

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.

@kantai kantai changed the base branch from feat/stacks-2.1-initialization to next August 20, 2021 16:19
@kantai
Copy link
Member Author

kantai commented Aug 25, 2021

Thanks for the reviews -- the existing comments have been addressed, and this PR is ready for review again.

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.

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);
Copy link
Contributor

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

Copy link
Member Author

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");
Copy link
Contributor

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

Copy link
Contributor

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

https://stackoverflow.com/questions/26469715/how-do-i-write-a-rust-unit-test-that-ensures-that-a-panic-has-occurred

Copy link
Member Author

@kantai kantai Aug 26, 2021

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) => {
Copy link
Contributor

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

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 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");
Copy link
Contributor

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

Copy link
Contributor

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

https://stackoverflow.com/questions/26469715/how-do-i-write-a-rust-unit-test-that-ensures-that-a-panic-has-occurred

Copy link
Member Author

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
Copy link
Contributor

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.

Copy link
Member Author

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) => {
Copy link
Contributor

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?


return Ok(());
} else {
// nothing to do -- the function failed
Copy link
Contributor

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
Copy link
Contributor

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);
Copy link
Contributor

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?

Copy link
Member Author

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.

@gregorycoppola
Copy link
Contributor

Thanks for an ambitious PR by the way!

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, 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",
Copy link
Contributor

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]?

src/chainstate/stacks/boot/contract_tests.rs Outdated Show resolved Hide resolved
src/chainstate/stacks/boot/contract_tests.rs Outdated Show resolved Hide resolved
src/chainstate/stacks/boot/pox-2.clar Outdated Show resolved Hide resolved

;; standard can-stack-stx checks
(try! (can-stack-stx pox-addr amount-ustx first-extend-cycle lock-period))

Copy link
Contributor

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?

Copy link
Member Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants