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

Solo Stacking stateful property-based tests #4725

Merged
merged 13 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
108 changes: 105 additions & 3 deletions contrib/core-contract-tests/tests/pox-4/pox_Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import fc from "fast-check";
import { Real, Stacker, Stub, StxAddress, Wallet } from "./pox_CommandModel";
import { GetStackingMinimumCommand } from "./pox_GetStackingMinimumCommand";
import { GetStxAccountCommand } from "./pox_GetStxAccountCommand";
import { StackStxCommand } from "./pox_StackStxCommand";
import { StackStxSigCommand } from "./pox_StackStxSigCommand";
import { StackStxAuthCommand } from "./pox_StackStxAuthCommand";
import { DelegateStxCommand } from "./pox_DelegateStxCommand";
import { DelegateStackStxCommand } from "./pox_DelegateStackStxCommand";
import { Simnet } from "@hirosystems/clarinet-sdk";
Expand All @@ -17,6 +18,10 @@ import { StackAggregationCommitIndexedSigCommand } from "./pox_StackAggregationC
import { StackAggregationCommitIndexedAuthCommand } from "./pox_StackAggregationCommitIndexedAuthCommand";
import { StackAggregationIncreaseCommand } from "./pox_StackAggregationIncreaseCommand";
import { DisallowContractCallerCommand } from "./pox_DisallowContractCallerCommand";
import { StackExtendAuthCommand } from "./pox_StackExtendAuthCommand";
import { StackExtendSigCommand } from "./pox_StackExtendSigCommand";
import { StackIncreaseAuthCommand } from "./pox_StackIncreaseAuthCommand";
import { StackIncreaseSigCommand } from "./pox_StackIncreaseSigCommand";

export function PoxCommands(
wallets: Map<StxAddress, Wallet>,
Expand All @@ -36,7 +41,7 @@ export function PoxCommands(
r.wallet,
)
),
// StackStxCommand
// StackStxSigCommand
fc.record({
wallet: fc.constantFrom(...wallets.values()),
authId: fc.nat(),
Expand All @@ -50,13 +55,110 @@ export function PoxCommands(
margin: number;
},
) =>
new StackStxCommand(
new StackStxSigCommand(
r.wallet,
r.authId,
r.period,
r.margin,
)
),
// StackStxAuthCommand
fc.record({
wallet: fc.constantFrom(...wallets.values()),
authId: fc.nat(),
period: fc.integer({ min: 1, max: 12 }),
margin: fc.integer({ min: 1, max: 9 }),
}).map((
r: {
wallet: Wallet;
authId: number;
period: number;
margin: number;
},
) =>
new StackStxAuthCommand(
r.wallet,
r.authId,
r.period,
r.margin,
)
),
// StackExtendAuthCommand
fc
.record({
wallet: fc.constantFrom(...wallets.values()),
authId: fc.nat(),
extendCount: fc.integer({ min: 1, max: 12 }),
currentCycle: fc.constant(currentCycle(network)),
})
.map(
(r: {
wallet: Wallet;
extendCount: number;
authId: number;
currentCycle: number;
}) =>
new StackExtendAuthCommand(
r.wallet,
r.extendCount,
r.authId,
r.currentCycle,
),
),
// StackExtendSigCommand
fc
.record({
wallet: fc.constantFrom(...wallets.values()),
authId: fc.nat(),
extendCount: fc.integer({ min: 1, max: 12 }),
currentCycle: fc.constant(currentCycle(network)),
})
.map(
(r: {
wallet: Wallet;
extendCount: number;
authId: number;
currentCycle: number;
}) =>
new StackExtendSigCommand(
r.wallet,
r.extendCount,
r.authId,
r.currentCycle,
),
),
// StackIncreaseAuthCommand
fc.record({
operator: fc.constantFrom(...wallets.values()),
increaseBy: fc.nat(),
authId: fc.nat(),
})
.map((r) => {
return new StackIncreaseAuthCommand(
r.operator,
r.increaseBy,
r.authId,
);
}),
// StackIncreaseSigCommand
fc.record({
operator: fc.constantFrom(...wallets.values()),
increaseBy: fc.nat(),
authId: fc.nat(),
})
.map((r) => {
return new StackIncreaseSigCommand(
r.operator,
r.increaseBy,
r.authId,
);
}),
// GetStackingMinimumCommand
fc
.record({
wallet: fc.constantFrom(...wallets.values()),
})
.map((r: { wallet: Wallet }) => new GetStackingMinimumCommand(r.wallet)),
// DelegateStxCommand
fc.record({
wallet: fc.constantFrom(...wallets.values()),
Expand Down
178 changes: 178 additions & 0 deletions contrib/core-contract-tests/tests/pox-4/pox_StackExtendAuthCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { poxAddressToTuple } from "@stacks/stacking";
import { logCommand, PoxCommand, Real, Stub, Wallet } from "./pox_CommandModel";
import {
currentCycle,
FIRST_BURNCHAIN_BLOCK_HEIGHT,
REWARD_CYCLE_LENGTH,
} from "./pox_Commands";
import { Cl, ClarityType, isClarityType } from "@stacks/transactions";
import { assert, expect } from "vitest";

export class StackExtendAuthCommand implements PoxCommand {
readonly wallet: Wallet;
readonly extendCount: number;
readonly authId: number;
readonly currentCycle: number;

/**
* Constructs a `StackExtendAuthCommand` to lock uSTX for stacking.
*
* This command calls `stack-extend` using an `authorization`.
*
* @param wallet - Represents the Stacker's wallet.
* @param extendCount - Represents the cycles to extend the stack with.
* @param authId - Unique auth-id for the authorization.
* @param currentCycle - Represents the current PoX reward cycle.
*
* Constraints for running this command include:
* - The Stacker must have locked uSTX.
* - The Stacker must be stacking solo.
* - The Stacker must not have delegated to a pool.
* - The new lock period must be less than or equal to 12.
*/
constructor(
wallet: Wallet,
extendCount: number,
authId: number,
currentCycle: number,
) {
this.wallet = wallet;
this.extendCount = extendCount;
this.authId = authId;
this.currentCycle = currentCycle;
}

check(model: Readonly<Stub>): boolean {
// Constraints for running this command include:
// - The Stacker must have locked uSTX.
// - The Stacker must be stacking solo.
// - The Stacker must not have delegated to a pool.
// - The new lock period must be less than or equal to 12.
const stacker = model.stackers.get(this.wallet.stxAddress)!;

const firstRewardCycle = stacker.firstLockedRewardCycle < this.currentCycle
? this.currentCycle
: stacker.firstLockedRewardCycle;
const firstExtendCycle = Math.floor(
(stacker.unlockHeight - FIRST_BURNCHAIN_BLOCK_HEIGHT) /
REWARD_CYCLE_LENGTH,
);
const lastExtendCycle = firstExtendCycle + this.extendCount - 1;
const totalPeriod = lastExtendCycle - firstRewardCycle + 1;

return (
model.stackingMinimum > 0 &&
stacker.isStacking &&
stacker.isStackingSolo &&
!stacker.hasDelegated &&
stacker.amountLocked > 0 &&
stacker.poolMembers.length === 0 &&
totalPeriod <= 12
);
}

run(model: Stub, real: Real): void {
model.trackCommandRun(this.constructor.name);
const currentRewCycle = currentCycle(real.network);

const stacker = model.stackers.get(this.wallet.stxAddress)!;

const { result: setAuthorization } = real.network.callPublicFn(
"ST000000000000000000002AMW42H.pox-4",
"set-signer-key-authorization",
[
// (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32))))
poxAddressToTuple(this.wallet.btcAddress),
// (period uint)
Cl.uint(this.extendCount),
// (reward-cycle uint)
Cl.uint(currentRewCycle),
// (topic (string-ascii 14))
Cl.stringAscii("stack-extend"),
// (signer-key (buff 33))
Cl.bufferFromHex(this.wallet.signerPubKey),
// (allowed bool)
Cl.bool(true),
// (max-amount uint)
Cl.uint(stacker.amountLocked),
// (auth-id uint)
Cl.uint(this.authId),
],
this.wallet.stxAddress,
);

expect(setAuthorization).toBeOk(Cl.bool(true));
const stackExtend = real.network.callPublicFn(
"ST000000000000000000002AMW42H.pox-4",
"stack-extend",
[
// (extend-count uint)
Cl.uint(this.extendCount),
// (pox-addr { version: (buff 1), hashbytes: (buff 32) })
poxAddressToTuple(this.wallet.btcAddress),
// (signer-sig (optional (buff 65)))
Cl.none(),
// (signer-key (buff 33))
Cl.bufferFromHex(this.wallet.signerPubKey),
// (max-amount uint)
Cl.uint(stacker.amountLocked),
// (auth-id uint)
Cl.uint(this.authId),
],
this.wallet.stxAddress,
);

const { result: firstExtendCycle } = real.network.callReadOnlyFn(
"ST000000000000000000002AMW42H.pox-4",
"burn-height-to-reward-cycle",
[Cl.uint(stacker.unlockHeight)],
this.wallet.stxAddress,
);
assert(isClarityType(firstExtendCycle, ClarityType.UInt));

const lastExtendCycle = Number(firstExtendCycle.value) + this.extendCount -
1;

const { result: extendedUnlockHeight } = real.network.callReadOnlyFn(
"ST000000000000000000002AMW42H.pox-4",
"reward-cycle-to-burn-height",
[Cl.uint(lastExtendCycle + 1)],
this.wallet.stxAddress,
);
assert(isClarityType(extendedUnlockHeight, ClarityType.UInt));

const newUnlockHeight = extendedUnlockHeight.value;

expect(stackExtend.result).toBeOk(
Cl.tuple({
stacker: Cl.principal(this.wallet.stxAddress),
"unlock-burn-height": Cl.uint(newUnlockHeight),
}),
);

// Get the wallet from the model and update it with the new state.
const wallet = model.stackers.get(this.wallet.stxAddress)!;
// Update model so that we know this wallet's unlock height was extended.
wallet.unlockHeight = Number(newUnlockHeight);

// Log to console for debugging purposes. This is not necessary for the
// test to pass but it is useful for debugging and eyeballing the test.
logCommand(
`₿ ${model.burnBlockHeight}`,
`✓ ${this.wallet.label}`,
"stack-extend-auth",
"extend-count",
this.extendCount.toString(),
);

// Refresh the model's state if the network gets to the next reward cycle.
model.refreshStateForNextRewardCycle(real);
}

toString() {
// fast-check will call toString() in case of errors, e.g. property failed.
// It will then make a minimal counterexample, a process called 'shrinking'
// https://github.com/dubzzz/fast-check/issues/2864#issuecomment-1098002642
return `${this.wallet.label} stack-extend auth extend-count ${this.extendCount}`;
}
}