@stacks/rendezvous 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -23
- package/dist/citizen.js +34 -33
- package/dist/heatstroke.js +35 -32
- package/dist/heatstroke.types.js +2 -0
- package/dist/invariant.js +120 -80
- package/dist/package.json +7 -3
- package/dist/property.js +55 -31
- package/dist/shared.js +87 -105
- package/dist/traits.js +389 -0
- package/dist/traits.types.js +2 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -28,16 +28,11 @@ root
|
|
|
28
28
|
|
|
29
29
|
### Installation
|
|
30
30
|
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
**Install the package locally**
|
|
34
|
-
|
|
35
31
|
```
|
|
36
|
-
npm install
|
|
37
|
-
npm run build
|
|
32
|
+
npm install @stacks/rendezvous
|
|
38
33
|
```
|
|
39
34
|
|
|
40
|
-
|
|
35
|
+
### Usage
|
|
41
36
|
|
|
42
37
|
```
|
|
43
38
|
npx rv <path-to-clarinet-project> <contract-name> <type>
|
|
@@ -45,22 +40,6 @@ npx rv <path-to-clarinet-project> <contract-name> <type>
|
|
|
45
40
|
|
|
46
41
|
---
|
|
47
42
|
|
|
48
|
-
**Install the package globally**
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
git clone https://github.com/stacks-network/rendezvous
|
|
52
|
-
npm install
|
|
53
|
-
npm install --global .
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Run the fuzzer from anywhere on your system:
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
rv <path-to-clarinet-project> <contract-name> <type>
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
43
|
### Configuration
|
|
65
44
|
|
|
66
45
|
**Positional arguments:**
|
package/dist/citizen.js
CHANGED
|
@@ -19,17 +19,17 @@ const path_1 = require("path");
|
|
|
19
19
|
const yaml_1 = __importDefault(require("yaml"));
|
|
20
20
|
const clarinet_sdk_1 = require("@hirosystems/clarinet-sdk");
|
|
21
21
|
/**
|
|
22
|
-
* Prepares the simnet
|
|
23
|
-
* as a first-class citizen,
|
|
24
|
-
* handles:
|
|
22
|
+
* Prepares the simnet instance and assures the target contract's corresponding
|
|
23
|
+
* test contract is treated as a first-class citizen, relying on their
|
|
24
|
+
* concatenation. This function handles:
|
|
25
25
|
* - Contract sorting by epoch based on the deployment plan.
|
|
26
26
|
* - Combining the target contract with its tests and deploying all contracts
|
|
27
27
|
* to the simnet.
|
|
28
28
|
*
|
|
29
|
-
* @param manifestDir
|
|
30
|
-
* @param sutContractName
|
|
29
|
+
* @param manifestDir The relative path to the manifest directory.
|
|
30
|
+
* @param sutContractName The target contract name.
|
|
31
31
|
* @returns The initialized simnet instance with all contracts deployed, with
|
|
32
|
-
* the
|
|
32
|
+
* the test contract treated as a first-class citizen of the target contract.
|
|
33
33
|
*/
|
|
34
34
|
const issueFirstClassCitizenship = (manifestDir, sutContractName) => __awaiter(void 0, void 0, void 0, function* () {
|
|
35
35
|
const manifestPath = (0, path_1.join)(manifestDir, "Clarinet.toml");
|
|
@@ -43,8 +43,8 @@ const issueFirstClassCitizenship = (manifestDir, sutContractName) => __awaiter(v
|
|
|
43
43
|
const sortedContractsByEpoch = (0, exports.groupContractsByEpochFromSimnetPlan)(simnetPlan);
|
|
44
44
|
yield simnet.initEmptySession();
|
|
45
45
|
// Combine the target contract with its tests into a single contract. The
|
|
46
|
-
// resulting contract will replace the target contract in the simnet.
|
|
47
|
-
|
|
46
|
+
// resulting contract will replace the target contract in the simnet.
|
|
47
|
+
/** The contract names mapped to the concatenated source code. */
|
|
48
48
|
const rendezvousSources = new Map([sutContractName]
|
|
49
49
|
.map((contractName) => (0, exports.buildRendezvousData)(simnetPlan, contractName, manifestDir))
|
|
50
50
|
.map((rendezvousContractData) => [
|
|
@@ -58,7 +58,7 @@ const issueFirstClassCitizenship = (manifestDir, sutContractName) => __awaiter(v
|
|
|
58
58
|
exports.issueFirstClassCitizenship = issueFirstClassCitizenship;
|
|
59
59
|
/**
|
|
60
60
|
* Groups contracts by epoch from the simnet plan.
|
|
61
|
-
* @param simnetPlan
|
|
61
|
+
* @param simnetPlan The simnet plan.
|
|
62
62
|
* @returns A record of contracts grouped by epoch. The record key is the epoch
|
|
63
63
|
* string, and the value is an array of contracts. Each contract is represented
|
|
64
64
|
* as a record with the contract name as the key and a record containing the
|
|
@@ -86,10 +86,10 @@ const groupContractsByEpochFromSimnetPlan = (simnetPlan) => {
|
|
|
86
86
|
};
|
|
87
87
|
exports.groupContractsByEpochFromSimnetPlan = groupContractsByEpochFromSimnetPlan;
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
90
|
-
* @param simnet
|
|
91
|
-
* @param contractsByEpoch
|
|
92
|
-
* @param getContractSourceFn
|
|
89
|
+
* Deploys the contracts to the simnet in the correct order.
|
|
90
|
+
* @param simnet The simnet instance.
|
|
91
|
+
* @param contractsByEpoch The record of contracts by epoch.
|
|
92
|
+
* @param getContractSourceFn The function to retrieve the contract source.
|
|
93
93
|
*/
|
|
94
94
|
const deployContracts = (simnet, contractsByEpoch, getContractSourceFn) => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
95
|
for (const [epoch, contracts] of Object.entries(contractsByEpoch)) {
|
|
@@ -109,18 +109,19 @@ const deployContracts = (simnet, contractsByEpoch, getContractSourceFn) => __awa
|
|
|
109
109
|
}
|
|
110
110
|
});
|
|
111
111
|
/**
|
|
112
|
-
* Conditionally
|
|
112
|
+
* Conditionally retrieves the contract source based on whether the contract is
|
|
113
113
|
* a SUT contract or not.
|
|
114
|
-
* @param
|
|
115
|
-
* @param
|
|
116
|
-
*
|
|
117
|
-
* @param
|
|
118
|
-
* @param
|
|
119
|
-
* @
|
|
114
|
+
* @param targetContractNames The list of target contract names.
|
|
115
|
+
* @param rendezvousSourcesMap The contract names mapped to the concatenated
|
|
116
|
+
* source code.
|
|
117
|
+
* @param contractName The contract name.
|
|
118
|
+
* @param contractProps The contract deployment properties.
|
|
119
|
+
* @param manifestDir The relative path to the manifest directory.
|
|
120
|
+
* @returns The contract source code.
|
|
120
121
|
*/
|
|
121
|
-
const getContractSource = (
|
|
122
|
-
if (
|
|
123
|
-
const contractSource =
|
|
122
|
+
const getContractSource = (targetContractNames, rendezvousSourcesMap, contractName, contractProps, manifestDir) => {
|
|
123
|
+
if (targetContractNames.includes(contractName)) {
|
|
124
|
+
const contractSource = rendezvousSourcesMap.get(contractName);
|
|
124
125
|
if (!contractSource) {
|
|
125
126
|
throw new Error(`Contract source not found for ${contractName}`);
|
|
126
127
|
}
|
|
@@ -134,11 +135,11 @@ const getContractSource = (sutContractNames, rendezvousMap, contractName, contra
|
|
|
134
135
|
};
|
|
135
136
|
exports.getContractSource = getContractSource;
|
|
136
137
|
/**
|
|
137
|
-
*
|
|
138
|
+
* Builds the Rendezvous data.
|
|
138
139
|
* @param simnetPlan The parsed simnet plan.
|
|
139
140
|
* @param contractName The contract name.
|
|
140
141
|
* @param manifestDir The relative path to the manifest directory.
|
|
141
|
-
* @returns The Rendezvous data representing
|
|
142
|
+
* @returns The Rendezvous data representing a record. The returned record
|
|
142
143
|
* contains the Rendezvous source code and the Rendezvous contract name.
|
|
143
144
|
*/
|
|
144
145
|
const buildRendezvousData = (simnetPlan, contractName, manifestDir) => {
|
|
@@ -157,7 +158,7 @@ const buildRendezvousData = (simnetPlan, contractName, manifestDir) => {
|
|
|
157
158
|
};
|
|
158
159
|
exports.buildRendezvousData = buildRendezvousData;
|
|
159
160
|
/**
|
|
160
|
-
*
|
|
161
|
+
* Retrieves the contract source code using the simnet plan.
|
|
161
162
|
* @param simnetPlan The parsed simnet plan.
|
|
162
163
|
* @param manifestDir The relative path to the manifest directory.
|
|
163
164
|
* @param sutContractName The target contract name.
|
|
@@ -179,7 +180,7 @@ const getSimnetPlanContractSource = (simnetPlan, manifestDir, sutContractName) =
|
|
|
179
180
|
}).toString();
|
|
180
181
|
};
|
|
181
182
|
/**
|
|
182
|
-
*
|
|
183
|
+
* Retrieves the test contract source code.
|
|
183
184
|
* @param simnetPlan The parsed simnet plan.
|
|
184
185
|
* @param sutContractName The target contract name.
|
|
185
186
|
* @param manifestDir The relative path to the manifest directory.
|
|
@@ -209,13 +210,13 @@ const getTestContractSource = (simnetPlan, sutContractName, manifestDir) => {
|
|
|
209
210
|
};
|
|
210
211
|
exports.getTestContractSource = getTestContractSource;
|
|
211
212
|
/**
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* @param
|
|
215
|
-
* @param
|
|
213
|
+
* Schedules a Rendezvous between the System Under Test (`SUT`) and the test
|
|
214
|
+
* contract.
|
|
215
|
+
* @param targetContractSource The target contract source code.
|
|
216
|
+
* @param tests The corresponding test contract source code.
|
|
216
217
|
* @returns The Rendezvous source code.
|
|
217
218
|
*/
|
|
218
|
-
function scheduleRendezvous(
|
|
219
|
+
function scheduleRendezvous(targetContractSource, tests) {
|
|
219
220
|
/**
|
|
220
221
|
* The `context` map tracks how many times each function has been called.
|
|
221
222
|
* This data can be useful for invariant tests to check behavior over time.
|
|
@@ -227,5 +228,5 @@ function scheduleRendezvous(sutContractSource, invariants) {
|
|
|
227
228
|
|
|
228
229
|
(define-public (update-context (function-name (string-ascii 100)) (called uint))
|
|
229
230
|
(ok (map-set context function-name {called: called})))`;
|
|
230
|
-
return `${
|
|
231
|
+
return `${targetContractSource}\n\n${context}\n\n${tests}`;
|
|
231
232
|
}
|
package/dist/heatstroke.js
CHANGED
|
@@ -1,38 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Heatstrokes Reporter
|
|
4
|
-
*
|
|
5
|
-
* This reporter integrates with `fast-check` to provide detailed and formatted
|
|
6
|
-
* outputs for failed property-based tests. It captures key information such as
|
|
7
|
-
* the contract, functions, arguments, outputs, and the specific invariant that
|
|
8
|
-
* failed, enabling quick identification of issues.
|
|
9
|
-
*/
|
|
10
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
3
|
exports.reporter = reporter;
|
|
4
|
+
const ansicolor_1 = require("ansicolor");
|
|
5
|
+
const shared_1 = require("./shared");
|
|
12
6
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* This function provides a detailed report when a fuzzing test fails including
|
|
16
|
-
* the contract, functions, arguments, outputs, and the specific invariant that
|
|
17
|
-
* failed.
|
|
7
|
+
* Heatstrokes Reporter
|
|
18
8
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* @
|
|
23
|
-
* @property runDetails.
|
|
24
|
-
* @property runDetails.
|
|
25
|
-
* @property runDetails.
|
|
9
|
+
* Provides a detailed report when a test run test ends. In case of failure,
|
|
10
|
+
* includes the contract, functions, arguments, outputs, and the specific
|
|
11
|
+
* invariant or property test that failed.
|
|
12
|
+
* @param runDetails The details of the test run provided by fast-check.
|
|
13
|
+
* @property `runDetails.failed`: Indicates if the test run failed.
|
|
14
|
+
* @property `runDetails.counterexample`: The input that caused the failure.
|
|
15
|
+
* @property `runDetails.numRuns`: The number of test cases that were run.
|
|
16
|
+
* @property `runDetails.seed`: The replay seed for reproducing the failure.
|
|
17
|
+
* @property `runDetails.path`: The replay path for reproducing the failure.
|
|
18
|
+
* @property `runDetails.error`: The error thrown during the test.
|
|
19
|
+
* @param radio The event emitter to log messages.
|
|
20
|
+
* @param type The type of test that failed: invariant or property.
|
|
21
|
+
* @returns void
|
|
26
22
|
*/
|
|
27
|
-
|
|
28
|
-
const shared_1 = require("./shared");
|
|
29
|
-
function reporter(
|
|
30
|
-
//@ts-ignore
|
|
31
|
-
runDetails, radio, type) {
|
|
23
|
+
function reporter(runDetails, radio, type) {
|
|
32
24
|
var _a, _b;
|
|
33
25
|
if (runDetails.failed) {
|
|
34
26
|
// Report general run data.
|
|
35
|
-
const r = runDetails.counterexample[0];
|
|
36
27
|
radio.emit("logFailure", `\nError: Property failed after ${runDetails.numRuns} tests.`);
|
|
37
28
|
radio.emit("logFailure", `Seed : ${runDetails.seed}`);
|
|
38
29
|
if (runDetails.path) {
|
|
@@ -40,15 +31,26 @@ runDetails, radio, type) {
|
|
|
40
31
|
}
|
|
41
32
|
switch (type) {
|
|
42
33
|
case "invariant": {
|
|
34
|
+
const r = runDetails.counterexample[0];
|
|
43
35
|
// Report specific run data for the invariant testing type.
|
|
44
36
|
radio.emit("logFailure", `\nCounterexample:`);
|
|
45
37
|
radio.emit("logFailure", `- Contract : ${(0, shared_1.getContractNameFromContractId)(r.rendezvousContractId)}`);
|
|
46
|
-
radio.emit("logFailure", `-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
radio.emit("logFailure", `- Functions: ${r.selectedFunctions
|
|
39
|
+
.map((selectedFunction) => selectedFunction.name)
|
|
40
|
+
.join(", ")} (${r.selectedFunctions
|
|
41
|
+
.map((selectedFunction) => selectedFunction.access)
|
|
42
|
+
.join(", ")})`);
|
|
43
|
+
radio.emit("logFailure", `- Arguments: ${r.selectedFunctionsArgsList
|
|
44
|
+
.map((selectedFunctionArgs) => JSON.stringify(selectedFunctionArgs))
|
|
45
|
+
.join(", ")}`);
|
|
46
|
+
radio.emit("logFailure", `- Callers : ${r.sutCallers
|
|
47
|
+
.map((sutCaller) => sutCaller[0])
|
|
48
|
+
.join(", ")}`);
|
|
49
|
+
radio.emit("logFailure", `- Outputs : ${r.selectedFunctions
|
|
50
|
+
.map((selectedFunction) => JSON.stringify(selectedFunction.outputs))
|
|
51
|
+
.join(", ")}`);
|
|
50
52
|
radio.emit("logFailure", `- Invariant: ${r.selectedInvariant.name} (${r.selectedInvariant.access})`);
|
|
51
|
-
radio.emit("logFailure", `- Arguments: ${JSON.stringify(r.
|
|
53
|
+
radio.emit("logFailure", `- Arguments: ${JSON.stringify(r.invariantArgs)}`);
|
|
52
54
|
radio.emit("logFailure", `- Caller : ${r.invariantCaller[0]}`);
|
|
53
55
|
radio.emit("logFailure", `\nWhat happened? Rendezvous went on a rampage and found a weak spot:\n`);
|
|
54
56
|
const formattedError = `The invariant "${r.selectedInvariant.name}" returned:\n\n${(_a = runDetails.error) === null || _a === void 0 ? void 0 : _a.toString().split("\n").map((line) => " " + line).join("\n")}\n`;
|
|
@@ -56,11 +58,12 @@ runDetails, radio, type) {
|
|
|
56
58
|
break;
|
|
57
59
|
}
|
|
58
60
|
case "test": {
|
|
61
|
+
const r = runDetails.counterexample[0];
|
|
59
62
|
// Report specific run data for the property testing type.
|
|
60
63
|
radio.emit("logFailure", `\nCounterexample:`);
|
|
61
64
|
radio.emit("logFailure", `- Test Contract : ${(0, shared_1.getContractNameFromContractId)(r.testContractId)}`);
|
|
62
65
|
radio.emit("logFailure", `- Test Function : ${r.selectedTestFunction.name} (${r.selectedTestFunction.access})`);
|
|
63
|
-
radio.emit("logFailure", `- Arguments : ${JSON.stringify(r.
|
|
66
|
+
radio.emit("logFailure", `- Arguments : ${JSON.stringify(r.functionArgs)}`);
|
|
64
67
|
radio.emit("logFailure", `- Caller : ${r.testCaller[0]}`);
|
|
65
68
|
radio.emit("logFailure", `- Outputs : ${JSON.stringify(r.selectedTestFunction.outputs)}`);
|
|
66
69
|
radio.emit("logFailure", `\nWhat happened? Rendezvous went on a rampage and found a weak spot:\n`);
|
package/dist/invariant.js
CHANGED
|
@@ -9,7 +9,22 @@ const transactions_1 = require("@stacks/transactions");
|
|
|
9
9
|
const heatstroke_1 = require("./heatstroke");
|
|
10
10
|
const fast_check_1 = __importDefault(require("fast-check"));
|
|
11
11
|
const ansicolor_1 = require("ansicolor");
|
|
12
|
-
const
|
|
12
|
+
const traits_1 = require("./traits");
|
|
13
|
+
/**
|
|
14
|
+
* Runs invariant testing on the target contract and logs the progress. Reports
|
|
15
|
+
* the test results through a custom reporter.
|
|
16
|
+
* @param simnet The Simnet instance.
|
|
17
|
+
* @param targetContractName The name of the target contract.
|
|
18
|
+
* @param rendezvousList The list of contract IDs for each target contract.
|
|
19
|
+
* @param rendezvousAllFunctions The map of all function interfaces for each
|
|
20
|
+
* target contract.
|
|
21
|
+
* @param seed The seed for reproducible invariant testing.
|
|
22
|
+
* @param path The path for reproducible invariant testing.
|
|
23
|
+
* @param runs The number of test runs.
|
|
24
|
+
* @param radio The custom logging event emitter.
|
|
25
|
+
* @returns void
|
|
26
|
+
*/
|
|
27
|
+
const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, radio) => {
|
|
13
28
|
// A map where the keys are the Rendezvous identifiers and the values are
|
|
14
29
|
// arrays of their SUT (System Under Test) functions. This map will be used
|
|
15
30
|
// to access the SUT functions for each Rendezvous contract afterwards.
|
|
@@ -18,35 +33,52 @@ const checkInvariants = (simnet, sutContractName, rendezvousList, rendezvousAllF
|
|
|
18
33
|
// arrays of their invariant functions. This map will be used to access the
|
|
19
34
|
// invariant functions for each Rendezvous contract afterwards.
|
|
20
35
|
const rendezvousInvariantFunctions = filterInvariantFunctions(rendezvousAllFunctions);
|
|
36
|
+
// The Rendezvous identifier is the first one in the list. Only one contract
|
|
37
|
+
// can be fuzzed at a time.
|
|
38
|
+
const rendezvousContractId = rendezvousList[0];
|
|
39
|
+
const traitReferenceSutFunctions = rendezvousSutFunctions
|
|
40
|
+
.get(rendezvousContractId)
|
|
41
|
+
.filter((fn) => (0, traits_1.isTraitReferenceFunction)(fn));
|
|
42
|
+
const traitReferenceInvariantFunctions = rendezvousInvariantFunctions
|
|
43
|
+
.get(rendezvousContractId)
|
|
44
|
+
.filter((fn) => (0, traits_1.isTraitReferenceFunction)(fn));
|
|
45
|
+
const projectTraitImplementations = (0, traits_1.extractProjectTraitImplementations)(simnet);
|
|
46
|
+
if (Object.entries(projectTraitImplementations).length === 0 &&
|
|
47
|
+
(traitReferenceSutFunctions.length > 0 ||
|
|
48
|
+
traitReferenceInvariantFunctions.length > 0)) {
|
|
49
|
+
const foundTraitReferenceMessage = traitReferenceSutFunctions.length > 0 &&
|
|
50
|
+
traitReferenceInvariantFunctions.length > 0
|
|
51
|
+
? "public functions and invariants"
|
|
52
|
+
: traitReferenceSutFunctions.length > 0
|
|
53
|
+
? "public functions"
|
|
54
|
+
: "invariants";
|
|
55
|
+
radio.emit("logMessage", (0, ansicolor_1.red)(`\nFound ${foundTraitReferenceMessage} referencing traits, but no trait implementations were found in the project.
|
|
56
|
+
\nNote: You can add contracts implementing traits either as project contracts or as Clarinet requirements. For more details, visit: https://www.hiro.so/clarinet/.
|
|
57
|
+
\n`));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const enrichedSutFunctionsInterfaces = traitReferenceSutFunctions.length > 0
|
|
61
|
+
? (0, traits_1.enrichInterfaceWithTraitData)(simnet.getContractAST(targetContractName), (0, traits_1.buildTraitReferenceMap)(rendezvousSutFunctions.get(rendezvousContractId)), rendezvousSutFunctions.get(rendezvousContractId), rendezvousContractId)
|
|
62
|
+
: rendezvousSutFunctions;
|
|
63
|
+
const enrichedInvariantFunctionsInterfaces = traitReferenceInvariantFunctions.length > 0
|
|
64
|
+
? (0, traits_1.enrichInterfaceWithTraitData)(simnet.getContractAST(targetContractName), (0, traits_1.buildTraitReferenceMap)(rendezvousInvariantFunctions.get(rendezvousContractId)), rendezvousInvariantFunctions.get(rendezvousContractId), rendezvousContractId)
|
|
65
|
+
: rendezvousInvariantFunctions;
|
|
21
66
|
// Set up local context to track SUT function call counts.
|
|
22
|
-
const localContext = (0, exports.initializeLocalContext)(
|
|
67
|
+
const localContext = (0, exports.initializeLocalContext)(enrichedSutFunctionsInterfaces);
|
|
23
68
|
// Set up context in simnet by initializing state for SUT.
|
|
24
|
-
(0, exports.initializeClarityContext)(simnet,
|
|
25
|
-
radio.emit("logMessage", `\nStarting invariant testing type for the ${
|
|
69
|
+
(0, exports.initializeClarityContext)(simnet, enrichedSutFunctionsInterfaces);
|
|
70
|
+
radio.emit("logMessage", `\nStarting invariant testing type for the ${targetContractName} contract...\n`);
|
|
26
71
|
const simnetAccounts = simnet.getAccounts();
|
|
27
72
|
const eligibleAccounts = new Map([...simnetAccounts].filter(([key]) => key !== "faucet"));
|
|
28
73
|
const simnetAddresses = Array.from(simnetAccounts.values());
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const rendezvousContractId = rendezvousList[0];
|
|
32
|
-
const functions = (0, shared_1.getFunctionsListForContract)(rendezvousSutFunctions, rendezvousContractId);
|
|
33
|
-
const invariantFunctions = (0, shared_1.getFunctionsListForContract)(rendezvousInvariantFunctions, rendezvousContractId);
|
|
74
|
+
const functions = (0, shared_1.getFunctionsListForContract)(enrichedSutFunctionsInterfaces, rendezvousContractId);
|
|
75
|
+
const invariants = (0, shared_1.getFunctionsListForContract)(enrichedInvariantFunctionsInterfaces, rendezvousContractId);
|
|
34
76
|
if ((functions === null || functions === void 0 ? void 0 : functions.length) === 0) {
|
|
35
|
-
radio.emit("logMessage", (0, ansicolor_1.red)(`No public functions found for the "${
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
if ((invariantFunctions === null || invariantFunctions === void 0 ? void 0 : invariantFunctions.length) === 0) {
|
|
39
|
-
radio.emit("logMessage", (0, ansicolor_1.red)(`No invariant functions found for the "${sutContractName}" contract. Beware, for your contract may be exposed to unforeseen issues.\n`));
|
|
77
|
+
radio.emit("logMessage", (0, ansicolor_1.red)(`No public functions found for the "${targetContractName}" contract. Without public functions, no state transitions can happen inside the contract, and the invariant test is not meaningful.\n`));
|
|
40
78
|
return;
|
|
41
79
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (eligibleFunctions.length === 0) {
|
|
45
|
-
radio.emit("logMessage", (0, ansicolor_1.red)(`No eligible public functions found for the "${sutContractName}" contract. Note: trait references are not supported.\n`));
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
if (eligibleInvariants.length === 0) {
|
|
49
|
-
radio.emit("logMessage", (0, ansicolor_1.red)(`No eligible invariant functions found for the "${sutContractName}" contract. Note: trait references are not supported.\n`));
|
|
80
|
+
if ((invariants === null || invariants === void 0 ? void 0 : invariants.length) === 0) {
|
|
81
|
+
radio.emit("logMessage", (0, ansicolor_1.red)(`No invariant functions found for the "${targetContractName}" contract. Beware, for your contract may be exposed to unforeseen issues.\n`));
|
|
50
82
|
return;
|
|
51
83
|
}
|
|
52
84
|
const radioReporter = (runDetails) => {
|
|
@@ -58,20 +90,25 @@ const checkInvariants = (simnet, sutContractName, rendezvousList, rendezvousAllF
|
|
|
58
90
|
// to the first contract in the list. The arbitrary is still needed,
|
|
59
91
|
// being used for reporting purposes in `heatstroke.ts`.
|
|
60
92
|
rendezvousContractId: fast_check_1.default.constant(rendezvousContractId),
|
|
61
|
-
sutCaller: fast_check_1.default.constantFrom(...eligibleAccounts.entries()),
|
|
62
93
|
invariantCaller: fast_check_1.default.constantFrom(...eligibleAccounts.entries()),
|
|
63
94
|
canMineBlocks: fast_check_1.default.boolean(),
|
|
64
95
|
})
|
|
65
96
|
.chain((r) => fast_check_1.default
|
|
66
97
|
.record({
|
|
67
|
-
|
|
68
|
-
|
|
98
|
+
selectedFunctions: fast_check_1.default.array(fast_check_1.default.constantFrom(...functions), {
|
|
99
|
+
minLength: 1, // At least one function must be selected.
|
|
100
|
+
}),
|
|
101
|
+
selectedInvariant: fast_check_1.default.constantFrom(...invariants),
|
|
69
102
|
})
|
|
70
103
|
.map((selectedFunctions) => (Object.assign(Object.assign({}, r), selectedFunctions))))
|
|
71
104
|
.chain((r) => fast_check_1.default
|
|
72
105
|
.record({
|
|
73
|
-
|
|
74
|
-
|
|
106
|
+
sutCallers: fast_check_1.default.array(fast_check_1.default.constantFrom(...eligibleAccounts.entries()), {
|
|
107
|
+
minLength: r.selectedFunctions.length,
|
|
108
|
+
maxLength: r.selectedFunctions.length,
|
|
109
|
+
}),
|
|
110
|
+
selectedFunctionsArgsList: fast_check_1.default.tuple(...r.selectedFunctions.map((selectedFunction) => fast_check_1.default.tuple(...(0, shared_1.functionToArbitrary)(selectedFunction, simnetAddresses, projectTraitImplementations)))),
|
|
111
|
+
invariantArgs: fast_check_1.default.tuple(...(0, shared_1.functionToArbitrary)(r.selectedInvariant, simnetAddresses, projectTraitImplementations)),
|
|
75
112
|
})
|
|
76
113
|
.map((args) => (Object.assign(Object.assign({}, r), args))))
|
|
77
114
|
.chain((r) => fast_check_1.default
|
|
@@ -90,58 +127,60 @@ const checkInvariants = (simnet, sutContractName, rendezvousList, rendezvousAllF
|
|
|
90
127
|
: fast_check_1.default.constant(0),
|
|
91
128
|
})
|
|
92
129
|
.map((burnBlocks) => (Object.assign(Object.assign({}, r), burnBlocks)))), (r) => {
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
130
|
+
const selectedFunctionsArgsCV = r.selectedFunctions.map((selectedFunction, index) => (0, shared_1.argsToCV)(selectedFunction, r.selectedFunctionsArgsList[index]));
|
|
131
|
+
const selectedInvariantArgsCV = (0, shared_1.argsToCV)(r.selectedInvariant, r.invariantArgs);
|
|
132
|
+
r.selectedFunctions.forEach((selectedFunction, index) => {
|
|
133
|
+
const [sutCallerWallet, sutCallerAddress] = r.sutCallers[index];
|
|
134
|
+
const printedFunctionArgs = r.selectedFunctionsArgsList[index]
|
|
135
|
+
.map((arg) => {
|
|
136
|
+
try {
|
|
137
|
+
return typeof arg === "object"
|
|
138
|
+
? JSON.stringify(arg)
|
|
139
|
+
: arg.toString();
|
|
140
|
+
}
|
|
141
|
+
catch (_a) {
|
|
142
|
+
return "[Circular]";
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
.join(" ");
|
|
97
146
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
147
|
+
const { result: functionCallResult } = simnet.callPublicFn(r.rendezvousContractId, selectedFunction.name, selectedFunctionsArgsCV[index], sutCallerAddress);
|
|
148
|
+
const functionCallResultJson = (0, transactions_1.cvToJSON)(functionCallResult);
|
|
149
|
+
if (functionCallResultJson.success) {
|
|
150
|
+
localContext[r.rendezvousContractId][selectedFunction.name]++;
|
|
151
|
+
simnet.callPublicFn(r.rendezvousContractId, "update-context", [
|
|
152
|
+
transactions_1.Cl.stringAscii(selectedFunction.name),
|
|
153
|
+
transactions_1.Cl.uint(localContext[r.rendezvousContractId][selectedFunction.name]),
|
|
154
|
+
], simnet.deployer);
|
|
155
|
+
radio.emit("logMessage", `₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
|
|
156
|
+
`Ӿ ${simnet.blockHeight.toString().padStart(8)} ` +
|
|
157
|
+
(0, ansicolor_1.dim)(`${sutCallerWallet} `) +
|
|
158
|
+
`${targetContractName} ` +
|
|
159
|
+
`${(0, ansicolor_1.underline)(selectedFunction.name)} ` +
|
|
160
|
+
printedFunctionArgs);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
radio.emit("logMessage", (0, ansicolor_1.dim)(`₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
|
|
164
|
+
`Ӿ ${simnet.blockHeight.toString().padStart(8)} ` +
|
|
165
|
+
`${sutCallerWallet} ` +
|
|
166
|
+
`${targetContractName} ` +
|
|
167
|
+
`${(0, ansicolor_1.underline)(selectedFunction.name)} ` +
|
|
168
|
+
printedFunctionArgs));
|
|
169
|
+
}
|
|
104
170
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const { result: functionCallResult } = simnet.callPublicFn(r.rendezvousContractId, r.selectedFunction.name, selectedFunctionArgs, sutCallerAddress);
|
|
110
|
-
const functionCallResultJson = (0, transactions_1.cvToJSON)(functionCallResult);
|
|
111
|
-
if (functionCallResultJson.success) {
|
|
112
|
-
localContext[r.rendezvousContractId][r.selectedFunction.name]++;
|
|
113
|
-
simnet.callPublicFn(r.rendezvousContractId, "update-context", [
|
|
114
|
-
transactions_1.Cl.stringAscii(r.selectedFunction.name),
|
|
115
|
-
transactions_1.Cl.uint(localContext[r.rendezvousContractId][r.selectedFunction.name]),
|
|
116
|
-
], simnet.deployer);
|
|
117
|
-
radio.emit("logMessage", `₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
|
|
118
|
-
`Ӿ ${simnet.blockHeight.toString().padStart(8)} ` +
|
|
119
|
-
(0, ansicolor_1.dim)(`${sutCallerWallet} `) +
|
|
120
|
-
`${sutContractName} ` +
|
|
121
|
-
`${(0, ansicolor_1.underline)(r.selectedFunction.name)} ` +
|
|
122
|
-
printedFunctionArgs);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
171
|
+
catch (error) {
|
|
172
|
+
// If the function call fails with a runtime error, log a dimmed
|
|
173
|
+
// message. Since the public function result is ignored, there's
|
|
174
|
+
// no need to throw an error.
|
|
125
175
|
radio.emit("logMessage", (0, ansicolor_1.dim)(`₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
|
|
126
176
|
`Ӿ ${simnet.blockHeight.toString().padStart(8)} ` +
|
|
127
177
|
`${sutCallerWallet} ` +
|
|
128
|
-
`${
|
|
129
|
-
`${(0, ansicolor_1.underline)(
|
|
178
|
+
`${targetContractName} ` +
|
|
179
|
+
`${(0, ansicolor_1.underline)(selectedFunction.name)} ` +
|
|
130
180
|
printedFunctionArgs));
|
|
131
181
|
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// If the function call fails with a runtime error, log a dimmed
|
|
135
|
-
// message. Since the public function result is ignored, there's
|
|
136
|
-
// no need to throw an error.
|
|
137
|
-
radio.emit("logMessage", (0, ansicolor_1.dim)(`₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
|
|
138
|
-
`Ӿ ${simnet.blockHeight.toString().padStart(8)} ` +
|
|
139
|
-
`${sutCallerWallet} ` +
|
|
140
|
-
`${sutContractName} ` +
|
|
141
|
-
`${(0, ansicolor_1.underline)(r.selectedFunction.name)} ` +
|
|
142
|
-
printedFunctionArgs));
|
|
143
|
-
}
|
|
144
|
-
const printedInvariantArgs = r.invariantArgsArb
|
|
182
|
+
});
|
|
183
|
+
const printedInvariantArgs = r.invariantArgs
|
|
145
184
|
.map((arg) => {
|
|
146
185
|
try {
|
|
147
186
|
return typeof arg === "object"
|
|
@@ -155,19 +194,19 @@ const checkInvariants = (simnet, sutContractName, rendezvousList, rendezvousAllF
|
|
|
155
194
|
.join(" ");
|
|
156
195
|
const [invariantCallerWallet, invariantCallerAddress] = r.invariantCaller;
|
|
157
196
|
try {
|
|
158
|
-
const { result: invariantCallResult } = simnet.callReadOnlyFn(r.rendezvousContractId, r.selectedInvariant.name,
|
|
197
|
+
const { result: invariantCallResult } = simnet.callReadOnlyFn(r.rendezvousContractId, r.selectedInvariant.name, selectedInvariantArgsCV, invariantCallerAddress);
|
|
159
198
|
const invariantCallResultJson = (0, transactions_1.cvToJSON)(invariantCallResult);
|
|
160
199
|
if (invariantCallResultJson.value === true) {
|
|
161
200
|
radio.emit("logMessage", `₿ ${simnet.burnBlockHeight.toString().padStart(8)} ` +
|
|
162
201
|
`Ӿ ${simnet.blockHeight.toString().padStart(8)} ` +
|
|
163
202
|
`${(0, ansicolor_1.dim)(invariantCallerWallet)} ` +
|
|
164
203
|
`${(0, ansicolor_1.green)("[PASS]")} ` +
|
|
165
|
-
`${
|
|
204
|
+
`${targetContractName} ` +
|
|
166
205
|
`${(0, ansicolor_1.underline)(r.selectedInvariant.name)} ` +
|
|
167
206
|
printedInvariantArgs);
|
|
168
207
|
}
|
|
169
208
|
if (!invariantCallResultJson.value) {
|
|
170
|
-
throw new Error(`Invariant failed for ${
|
|
209
|
+
throw new Error(`Invariant failed for ${targetContractName} contract: "${r.selectedInvariant.name}" returned ${invariantCallResultJson.value}`);
|
|
171
210
|
}
|
|
172
211
|
}
|
|
173
212
|
catch (error) {
|
|
@@ -178,7 +217,7 @@ const checkInvariants = (simnet, sutContractName, rendezvousList, rendezvousAllF
|
|
|
178
217
|
`Ӿ ${simnet.blockHeight.toString().padStart(8)} ` +
|
|
179
218
|
`${invariantCallerWallet} ` +
|
|
180
219
|
`[FAIL] ` +
|
|
181
|
-
`${
|
|
220
|
+
`${targetContractName} ` +
|
|
182
221
|
`${(0, ansicolor_1.underline)(r.selectedInvariant.name)} ` +
|
|
183
222
|
printedInvariantArgs));
|
|
184
223
|
// Re-throw the error for fast-check to catch and process.
|
|
@@ -197,7 +236,7 @@ const checkInvariants = (simnet, sutContractName, rendezvousList, rendezvousAllF
|
|
|
197
236
|
};
|
|
198
237
|
exports.checkInvariants = checkInvariants;
|
|
199
238
|
/**
|
|
200
|
-
*
|
|
239
|
+
* Initializes the local context, setting the number of times each function
|
|
201
240
|
* has been called to zero.
|
|
202
241
|
* @param rendezvousSutFunctions The Rendezvous functions.
|
|
203
242
|
* @returns The initialized local context.
|
|
@@ -218,14 +257,15 @@ const initializeClarityContext = (simnet, rendezvousSutFunctions) => rendezvousS
|
|
|
218
257
|
});
|
|
219
258
|
exports.initializeClarityContext = initializeClarityContext;
|
|
220
259
|
/**
|
|
221
|
-
* Filter the System Under Test (`SUT`) functions from the map of all
|
|
222
|
-
*
|
|
260
|
+
* Filter the System Under Test (`SUT`) functions from the map of all contract
|
|
261
|
+
* functions.
|
|
223
262
|
*
|
|
224
263
|
* The SUT functions are the ones that have `public` access since they are
|
|
225
264
|
* capable of changing the contract state, and they are not test functions.
|
|
226
265
|
* @param allFunctionsMap The map containing all the functions for each
|
|
266
|
+
* Rendezvous contract.
|
|
267
|
+
* @returns A map containing the filtered SUT functions for each Rendezvous
|
|
227
268
|
* contract.
|
|
228
|
-
* @returns A map containing only the SUT functions for each contract.
|
|
229
269
|
*/
|
|
230
270
|
const filterSutFunctions = (allFunctionsMap) => new Map(Array.from(allFunctionsMap, ([contractId, functions]) => [
|
|
231
271
|
contractId,
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacks/rendezvous",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Meet your contract's vulnerabilities head-on.",
|
|
5
5
|
"main": "app.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "npx -p typescript tsc --project tsconfig.json && node -e \"if (process.platform !== 'win32') require('fs').chmodSync('./dist/app.js', 0o755);\"",
|
|
11
|
-
"test": "npx
|
|
11
|
+
"test": "npx jest",
|
|
12
12
|
"test:coverage": "npx tsc --project tsconfig.json && npx jest --coverage"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"README.md",
|
|
26
26
|
"LICENSE"
|
|
27
27
|
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/stacks-network/rendezvous.git"
|
|
31
|
+
},
|
|
28
32
|
"dependencies": {
|
|
29
33
|
"@hirosystems/clarinet-sdk": "2.12.0",
|
|
30
34
|
"@stacks/transactions": "^6.16.1",
|
|
@@ -36,7 +40,7 @@
|
|
|
36
40
|
"@hirosystems/clarinet-sdk-wasm": "2.12.0",
|
|
37
41
|
"@types/jest": "^29.5.12",
|
|
38
42
|
"jest": "^29.7.0",
|
|
39
|
-
"ts-jest": "^29.2.
|
|
43
|
+
"ts-jest": "^29.2.5",
|
|
40
44
|
"typescript": "^5.5.4"
|
|
41
45
|
}
|
|
42
46
|
}
|