Skip to content

Commit

Permalink
Merge pull request #4842 from stacks-network/feat/pox-4-stateful-prop…
Browse files Browse the repository at this point in the history
…-tree-logging

Stateful property-tests for the error/failure paths in PoX-4
  • Loading branch information
BowTiedRadone authored Jun 25, 2024
2 parents 75d5bc9 + 00c0738 commit 6586d94
Show file tree
Hide file tree
Showing 39 changed files with 4,718 additions and 133 deletions.
2,147 changes: 2,147 additions & 0 deletions contrib/boot-contracts-stateful-prop-tests/tests/pox-4/err_Commands.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { StackingClient } from "@stacks/stacking";

import fc from "fast-check";
import { PoxCommands } from "./pox_Commands.ts";
import { ErrCommands } from "./err_Commands.ts";

import fs from "fs";
import path from "path";
Expand Down Expand Up @@ -108,7 +109,8 @@ it("statefully interacts with PoX-4", async () => {
// commands are run at least once.
const statistics = fs.readdirSync(path.join(__dirname)).filter((file) =>
file.startsWith("pox_") && file.endsWith(".ts") &&
file !== "pox_CommandModel.ts" && file !== "pox_Commands.ts"
file !== "pox_CommandModel.ts" && file !== "pox_Commands.ts" &&
!file.includes("_Err")
).map((file) => file.slice(4, -3)); // Remove "pox_" prefix and ".ts" suffix.

// This is the initial state of the model.
Expand Down Expand Up @@ -143,17 +145,22 @@ it("statefully interacts with PoX-4", async () => {

simnet.setEpoch("3.0");

const successPath = PoxCommands(model.wallets, model.stackers, sut.network);
const failurePath = ErrCommands(model.wallets, model.stackers, sut.network);

fc.assert(
fc.property(
PoxCommands(model.wallets, model.stackers, sut.network),
// More on size: https://github.com/dubzzz/fast-check/discussions/2978
// More on cmds: https://github.com/dubzzz/fast-check/discussions/3026
fc.commands(successPath.concat(failurePath), { size: "xsmall" }),
(cmds) => {
const initialState = () => ({ model: model, real: sut });
fc.modelRun(initialState, cmds);
},
),
{
// Defines the number of test iterations to run; default is 100.
numRuns: 1000,
numRuns: 20000,
// Adjusts the level of detail in test reports. Default is 0 (minimal).
// At level 2, reports include extensive details, helpful for deep
// debugging. This includes not just the failing case and its seed, but
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ export class AllowContractCallerCommand implements PoxCommand {

const callerToAllow = model.stackers.get(this.allowanceTo.stxAddress)!;
// Update model so that we know this wallet has authorized a contract-caller.
// If the caller is already allowed, there's no need to add it again.
const callerToAllowIndexInAllowedList = wallet.allowedContractCallers
.indexOf(this.allowanceTo.stxAddress);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,49 @@ export class Stub {

reportCommandRuns() {
console.log("Command run method execution counts:");
this.statistics.forEach((count, commandName) => {
console.log(`${commandName}: ${count}`);
const orderedStatistics = Array.from(this.statistics.entries()).sort(
([keyA], [keyB]) => {
return keyA.localeCompare(keyB);
},
);

this.logAsTree(orderedStatistics);
}

private logAsTree(statistics: [string, number][]) {
const tree: { [key: string]: any } = {};

statistics.forEach(([commandName, count]) => {
const split = commandName.split("_");
let root: string = split[0],
rest: string = "base";

if (split.length > 1) {
rest = split.slice(1).join("_");
}
if (!tree[root]) {
tree[root] = {};
}
tree[root][rest] = count;
});

const printTree = (node: any, indent: string = "") => {
const keys = Object.keys(node);
keys.forEach((key, index) => {
const isLast = index === keys.length - 1;
const boxChar = isLast ? "└─ " : "├─ ";
if (key !== "base") {
if (typeof node[key] === "object") {
console.log(`${indent}${boxChar}${key}: ${node[key]["base"]}`);
printTree(node[key], indent + (isLast ? " " : "│ "));
} else {
console.log(`${indent}${boxChar}${key}: ${node[key]}`);
}
}
});
};

printTree(tree);
}

refreshStateForNextRewardCycle(real: Real) {
Expand Down Expand Up @@ -215,3 +255,258 @@ export const logCommand = (...items: (string | undefined)[]) => {

process.stdout.write(prettyPrint.join(""));
};

/**
* Helper function that checks if the minimum uSTX threshold was set in the model.
* @param model - the model at a given moment in time.
* @returns boolean.
*/
export const isStackingMinimumCalculated = (model: Readonly<Stub>): boolean =>
model.stackingMinimum > 0;

/**
* Helper function that checks if a stacker is currently stacking.
* @param stacker - the stacker's state at a given moment in time.
* @returns boolean.
*/
export const isStacking = (stacker: Stacker): boolean =>
stacker.isStacking;

/**
* Helper function that checks if a stacker has an active delegation.
* @param stacker - the stacker's state at a given moment in time.
* @returns boolean.
*/
export const isDelegating = (stacker: Stacker): boolean =>
stacker.hasDelegated;

/**
* Helper function that checks if the stacker is stacking using solo
* stacking methods.
* @param stacker - the stacker's state at a given moment in time.
* @returns boolean.
*/
export const isStackingSolo = (stacker: Stacker): boolean =>
stacker.isStackingSolo;

/**
* Helper function that checks if the stacker has locked uSTX.
* @param stacker - the stacker's state at a given moment in time.
* @returns boolean.
*/
export const isAmountLockedPositive = (stacker: Stacker): boolean =>
stacker.amountLocked > 0;

/**
* Helper function that checks if an operator has locked uSTX on
* behalf of at least one stacker.
* @param operator - the operator's state at a given moment in time.
* @returns boolean.
*/
export const hasLockedStackers = (operator: Stacker): boolean =>
operator.lockedAddresses.length > 0;

/**
* Helper function that checks if an operator has uSTX that was not
* yet committed.
* @param operator - the operator's state at a given moment in time.
* @returns boolean.
*
* NOTE: ATC is an abbreviation for "amount to commit".
*/
export const isATCPositive = (operator: Stacker): boolean =>
operator.amountToCommit > 0;

/**
* Helper function that checks if an operator's not committed uSTX
* amount is above the minimum stacking threshold.
* @param operator - the operator's state at a given moment in time.
* @param model - the model at a given moment in time.
* @returns boolean.
*
* NOTE: ATC is an abbreviation for "amount to commit".
*/ export const isATCAboveThreshold = (
operator: Stacker,
model: Readonly<Stub>,
): boolean => operator.amountToCommit >= model.stackingMinimum;

/**
* Helper function that checks if a uSTX amount fits within a stacker's
* delegation limit.
* @param stacker - the stacker's state at a given moment in time.
* @param amountToCheck - the uSTX amount to check.
* @returns boolean.
*/
export const isAmountWithinDelegationLimit = (
stacker: Stacker,
amountToCheck: bigint | number,
): boolean => stacker.delegatedMaxAmount >= Number(amountToCheck);

/**
* Helper function that checks if a given unlock burn height is within
* a stacker's delegation limit.
* @param stacker - the stacker's state at a given moment in time.
* @param unlockBurnHt - the verified unlock burn height.
* @returns boolean.
*
* NOTE: UBH is an abbreviation for "unlock burn height".
*/
export const isUBHWithinDelegationLimit = (
stacker: Stacker,
unlockBurnHt: number,
): boolean =>
stacker.delegatedUntilBurnHt === undefined ||
unlockBurnHt <= stacker.delegatedUntilBurnHt;

/**
* Helper function that checks if a given amount is within a stacker's
* unlocked uSTX balance.
* @param stacker - the stacker's state at a given moment in time.
* @param amountToCheck - the amount to check.
* @returns boolean.
*/
export const isAmountWithinBalance = (
stacker: Stacker,
amountToCheck: bigint | number,
): boolean => stacker.ustxBalance >= Number(amountToCheck);

/**
* Helper function that checks if a given amount is above the minimum
* stacking threshold.
* @param model - the model at a given moment in time.
* @param amountToCheck - the amount to check.
* @returns boolean.
*/
export const isAmountAboveThreshold = (
model: Readonly<Stub>,
amountToCheck: bigint | number,
): boolean => Number(amountToCheck) >= model.stackingMinimum;

/**
* Helper function that checks if an operator has at least one pool
* participant.
* @param operator - the operator's state at a given moment in time.
* @returns boolean.
*/
export const hasPoolMembers = (operator: Stacker): boolean =>
operator.poolMembers.length > 0;

/**
* Helper function that checks if a stacker is a pool member of a
* given operator.
* @param operator - the operator's state at a given moment in time.
* @param stacker - the stacker's state at a given moment in time.
* @returns boolean
*/
export const isStackerInOperatorPool = (
operator: Stacker,
stacker: Wallet,
): boolean => operator.poolMembers.includes(stacker.stxAddress);

/**
* Helper function that checks if a given stacker's funds are locked
* by a given operator.
* @param stacker - the stacker's state at a given moment in time.
* @param operator - the operator's state at a given moment in time.
* @returns boolean.
*/
export const isStackerLockedByOperator = (
operator: Stacker,
stacker: Wallet,
): boolean =>
operator.lockedAddresses.includes(
stacker.stxAddress,
);

/**
* Helper function that checks if a given stacker's unlock height is
* within the current reward cycle.
* @param stacker - the stacker's state at a given moment in time.
* @param model - the model at a given moment in time.
* @returns boolean.
*
* NOTE: RC is an abbreviation for "reward cycle".
*/
export const isUnlockedWithinCurrentRC = (
stackerWallet: Stacker,
model: Readonly<Stub>,
): boolean => (stackerWallet.unlockHeight <=
model.burnBlockHeight + REWARD_CYCLE_LENGTH);

/**
* Helper function that checks if the increase amount is within a given
* stacker's unlocked balance.
* @param stacker - the stacker's state at a given moment in time.
* @param increaseBy - the increase amount to check.
* @returns boolean.
*/
export const isIncreaseByWithinUnlockedBalance = (
stacker: Stacker,
increaseBy: number,
): boolean => increaseBy <= stacker.amountUnlocked;

/**
* Helper function that checks if the increase amount is greater than zero.
* @param increaseBy - the increase amount to check.
* @returns boolean.
*/
export const isIncreaseByGTZero = (increaseBy: number): boolean =>
increaseBy >= 1;

/**
* Helper function that checks if the increase amount does not exceed the
* PoX-4 maximum lock period.
* @param period - the period to check.
* @returns boolean.
*/
export const isPeriodWithinMax = (period: number) => period <= 12;

/**
* Helper function that checks if a given stacker is currently delegating
* to a given operator.
* @param stacker - the stacker's state at a given moment in time.
* @param operator - the operator's state at a given moment in time.
* @returns boolean.
*/
export const isStackerDelegatingToOperator = (
stacker: Stacker,
operator: Wallet,
): boolean => stacker.delegatedTo === operator.stxAddress;

/**
* Helper function that checks if a given increase amount is greater than
* zero.
* @param increaseAmount - the increase amount to check
* @returns boolean.
*/
export const isIncreaseAmountGTZero = (increaseAmount: number): boolean =>
increaseAmount > 0;

/**
* Helper function that checks if a given stacker's has issued an allowance
* to a potential contract caller.
* @param stacker - the stacker's state at a given moment in time.
* @param potentialAllowedStacker - the potential contract caller's state.
* @returns boolean.
*/
export const isAllowedContractCaller = (
stacker: Stacker,
potentialAllowedStacker: Wallet,
): boolean =>
stacker.allowedContractCallers.includes(
potentialAllowedStacker.stxAddress,
);

/**
* Helper function that checks if a given contract caller has been allowed by
* a given stacker.
* @param stacker - the stacker's state at a given moment in time.
* @param caller - the contract caller's state.
* @returns boolean.
*/
export const isCallerAllowedByStacker = (
stacker: Wallet,
caller: Stacker,
): boolean => caller.callerAllowedBy.includes(stacker.stxAddress);

export const isPositive = (value: number): boolean => value >= 0;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fc from "fast-check";
import { Real, Stacker, Stub, StxAddress, Wallet } from "./pox_CommandModel";
import { PoxCommand, Stacker, StxAddress, Wallet } from "./pox_CommandModel";
import { GetStackingMinimumCommand } from "./pox_GetStackingMinimumCommand";
import { GetStxAccountCommand } from "./pox_GetStxAccountCommand";
import { StackStxSigCommand } from "./pox_StackStxSigCommand";
Expand Down Expand Up @@ -27,7 +27,7 @@ export function PoxCommands(
wallets: Map<StxAddress, Wallet>,
stackers: Map<StxAddress, Stacker>,
network: Simnet,
): fc.Arbitrary<Iterable<fc.Command<Stub, Real>>> {
): fc.Arbitrary<PoxCommand>[] {
const cmds = [
// GetStackingMinimumCommand
fc.record({
Expand Down Expand Up @@ -452,9 +452,7 @@ export function PoxCommands(
),
];

// More on size: https://github.com/dubzzz/fast-check/discussions/2978
// More on cmds: https://github.com/dubzzz/fast-check/discussions/3026
return fc.commands(cmds, { size: "xsmall" });
return cmds;
}

export const REWARD_CYCLE_LENGTH = 1050;
Expand All @@ -481,7 +479,7 @@ export const currentCycleFirstBlock = (network: Simnet) =>
).result,
));

const nextCycleFirstBlock = (network: Simnet) =>
export const nextCycleFirstBlock = (network: Simnet) =>
Number(cvToValue(
network.callReadOnlyFn(
"ST000000000000000000002AMW42H.pox-4",
Expand Down
Loading

0 comments on commit 6586d94

Please sign in to comment.