-
Notifications
You must be signed in to change notification settings - Fork 659
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
wip: PoX-4 StackStxCommand & stateful property tests planning
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
1 parent
d3f1a31
commit e7f6467
Showing
5 changed files
with
396 additions
and
0 deletions.
There are no files selected for viewing
87 changes: 87 additions & 0 deletions
87
contrib/core-contract-tests/tests/pox-4/pox-4.stateful-prop.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
32
contrib/core-contract-tests/tests/pox-4/pox_CommandModel.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }); | ||
} |
60 changes: 60 additions & 0 deletions
60
contrib/core-contract-tests/tests/pox-4/pox_GetStackingMinimumCommand.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`; | ||
} | ||
} |
Oops, something went wrong.