Skip to content

Commit

Permalink
wip: PoX-4 StackStxCommand & stateful property tests planning
Browse files Browse the repository at this point in the history
This commit lays the groundwork for the StackStxCommand and
GetStackingMinimumCommand classes for PoX-4. It also proposes the
introduction of fast-check based stateful tests, similar to the efforts
for sBTC (stacks-network/sbtc#152).

As highlighted in #4548,
this initiative is part of an ongoing effort to embrace a more rigorous,
property-based testing strategy for PoX-4 interactions.

The planned stateful tests aim to simulate various stacking scenarios,
ensuring compliance with PoX-4 protocols and robust error handling. This
strategy is expected to greatly enhance test coverage and the reliability
of PoX-4 stacking operations, bolstering confidence in the protocol’s
robustness and correctness.

Note: This is an early-stage WIP commit. Implementation details and testing
strategies are subject to substantial development and refinement.
  • Loading branch information
moodmosaic committed Mar 19, 2024
1 parent d3f1a31 commit e7f6467
Show file tree
Hide file tree
Showing 5 changed files with 396 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, it } from "vitest";

import { initSimnet } from "@hirosystems/clarinet-sdk";
import { Real, Stub, StxAddress, Wallet } from "./pox_CommandModel.ts";

import {
getPublicKeyFromPrivate,
publicKeyToBtcAddress,
} from "@stacks/encryption";
import { StacksDevnet } from "@stacks/network";
import {
createStacksPrivateKey,
getAddressFromPrivateKey,
TransactionVersion,
} from "@stacks/transactions";
import { StackingClient } from "@stacks/stacking";

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

describe("PoX-4 invariant tests", () => {
it("statefully does solo stacking with a signature", async () => {
// SUT stands for "System Under Test".
const sut: Real = {
network: await initSimnet(),
};

// This is the initial state of the model.
const model: Stub = {
stackingMinimum: 0,
wallets: new Map<StxAddress, Wallet>(),
};

const wallets = [
"7287ba251d44a4d3fd9276c88ce34c5c52a038955511cccaf77e61068649c17801",
"d655b2523bcd65e34889725c73064feb17ceb796831c0e111ba1a552b0f31b3901",
"3eccc5dac8056590432db6a35d52b9896876a3d5cbdea53b72400bc9c2099fe801",
"7036b29cb5e235e5fd9b09ae3e8eec4404e44906814d5d01cbca968a60ed4bfb01",
"b463f0df6c05d2f156393eee73f8016c5372caa0e9e29a901bb7171d90dc4f1401",
].map((prvKey) => {
const pubKey = getPublicKeyFromPrivate(prvKey);
const devnet = new StacksDevnet();
const signerPrvKey = createStacksPrivateKey(prvKey);
const signerPubKey = getPublicKeyFromPrivate(signerPrvKey.data);
const btcAddress = publicKeyToBtcAddress(pubKey);
const stxAddress = getAddressFromPrivateKey(
prvKey,
TransactionVersion.Testnet,
);

return {
prvKey,
pubKey,
stxAddress,
btcAddress,
signerPrvKey,
signerPubKey,
client: new StackingClient(stxAddress, devnet),
ustxBalance: 100_000_000_000_000,
isStacking: false,
amountLocked: 0,
unlockHeight: 0,
};
});

// Add the wallets to the model.
wallets.forEach((wallet) => {
model.wallets.set(wallet.stxAddress, wallet);
});

simnet.setEpoch("3.0");

fc.assert(
fc.property(
PoxCommands(model.wallets),
(cmds) => {
const initialState = () => ({ model: model, real: sut });
fc.modelRun(initialState, cmds);
},
),
{
numRuns: 1,
verbose: 2,
},
);
});
});
32 changes: 32 additions & 0 deletions contrib/core-contract-tests/tests/pox-4/pox_CommandModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import fc from "fast-check";

import { Simnet } from "@hirosystems/clarinet-sdk";
import { StacksPrivateKey } from "@stacks/transactions";
import { StackingClient } from "@stacks/stacking";

export type StxAddress = string;

export type Stub = {
stackingMinimum: number;
wallets: Map<StxAddress, Wallet>;
};

export type Real = {
network: Simnet;
};

export type Wallet = {
prvKey: string;
pubKey: string;
stxAddress: string;
btcAddress: string;
signerPrvKey: StacksPrivateKey;
signerPubKey: string;
client: StackingClient;
ustxBalance: number;
isStacking: boolean;
amountLocked: number;
unlockHeight: number;
};

export type PoxCommand = fc.Command<Stub, Real>;
48 changes: 48 additions & 0 deletions contrib/core-contract-tests/tests/pox-4/pox_Commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import fc from "fast-check";
import { Real, Stub, StxAddress, Wallet } from "./pox_CommandModel";
import { GetStackingMinimumCommand } from "./pox_GetStackingMinimumCommand";
import { StackStxCommand } from "./pox_StackStxCommand";

export function PoxCommands(
wallets: Map<StxAddress, Wallet>,
): fc.Arbitrary<Iterable<fc.Command<Stub, Real>>> {
const cmds = [
// GetStackingMinimumCommand
fc.record({
wallet: fc.constantFrom(...wallets.values()),
}).map((
r: {
wallet: Wallet;
},
) =>
new GetStackingMinimumCommand(
r.wallet,
)
),
// StackStxCommand
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 StackStxCommand(
r.wallet,
r.authId,
r.period,
r.margin,
)
),
];

// 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: "large" });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { PoxCommand, Real, Stub, Wallet } from "./pox_CommandModel.ts";
import { assert } from "vitest";
import { ClarityType, isClarityType } from "@stacks/transactions";

/**
* Implements the `PoxCommand` interface to get the minimum stacking amount
* required for a given reward cycle.
*/
export class GetStackingMinimumCommand implements PoxCommand {
readonly wallet: Wallet;

/**
* Constructs a new `GetStackingMinimumCommand`.
*
* @param wallet The wallet information, including the STX address used to
* query the stacking minimum requirement.
*/
constructor(wallet: Wallet) {
this.wallet = wallet;
}

check(_model: Readonly<Stub>): boolean {
// Can always check the minimum number of uSTX to be stacked in the given
// reward cycle.
return true;
}

run(model: Stub, real: Real): void {
// Act
const { result: stackingMinimum } = real.network.callReadOnlyFn(
"ST000000000000000000002AMW42H.pox-4",
"get-stacking-minimum",
[],
this.wallet.stxAddress,
);
assert(isClarityType(stackingMinimum, ClarityType.UInt));

// Update the model with the new stacking minimum. This is important for
// the `check` method of the `StackStxCommand` class to work correctly, as
// we as other tests that may depend on the stacking minimum.
model.stackingMinimum = Number(stackingMinimum.value);

// 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.
console.info(
`✓ ${this.wallet.stxAddress.padStart(8, " ")} ${
"get-stacking-minimum".padStart(34, " ")
} ${"pox-4".padStart(12, " ")} ${
stackingMinimum.value.toString().padStart(13, " ")
}`,
);
}

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.stxAddress} get-stacking-minimum`;
}
}
Loading

0 comments on commit e7f6467

Please sign in to comment.