@stacks/rendezvous 0.10.0 → 0.11.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 +1 -2
- package/dist/app.js +54 -47
- package/dist/invariant.js +64 -33
- package/dist/package.json +3 -3
- package/dist/property.js +34 -17
- package/dist/test.utils.js +21 -0
- package/dist/traits.js +51 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Rendezvous `rv` is a Clarity fuzzer designed to cut through your smart contract'
|
|
|
8
8
|
|
|
9
9
|
### Prerequisites
|
|
10
10
|
|
|
11
|
-
- **Node.js**: Supported versions include
|
|
11
|
+
- **Node.js**: Supported versions include 20, 22, and 23. Other versions may work, but they are untested.
|
|
12
12
|
|
|
13
13
|
### Inspiration
|
|
14
14
|
|
|
@@ -53,7 +53,6 @@ npx rv <path-to-clarinet-project> <contract-name> <type>
|
|
|
53
53
|
**Options:**
|
|
54
54
|
|
|
55
55
|
- `--seed` – The seed to use for the replay functionality.
|
|
56
|
-
- `--path` – The path to use for the replay functionality.
|
|
57
56
|
- `--runs` – The number of test iterations to use for exercising the contracts.
|
|
58
57
|
(default: `100`)
|
|
59
58
|
- `--bail` – Stop after the first failure.
|
package/dist/app.js
CHANGED
|
@@ -25,6 +25,7 @@ const citizen_1 = require("./citizen");
|
|
|
25
25
|
const package_json_1 = require("./package.json");
|
|
26
26
|
const ansicolor_1 = require("ansicolor");
|
|
27
27
|
const fs_1 = require("fs");
|
|
28
|
+
const util_1 = require("util");
|
|
28
29
|
const dialer_1 = require("./dialer");
|
|
29
30
|
const logger = (log, logLevel = "log") => {
|
|
30
31
|
console[logLevel](log);
|
|
@@ -71,7 +72,7 @@ exports.tryParseRemoteDataSettings = tryParseRemoteDataSettings;
|
|
|
71
72
|
const helpMessage = `
|
|
72
73
|
rv v${package_json_1.version}
|
|
73
74
|
|
|
74
|
-
Usage: rv <path-to-clarinet-project> <contract-name> <type> [--seed=<seed>] [--
|
|
75
|
+
Usage: rv <path-to-clarinet-project> <contract-name> <type> [--seed=<seed>] [--runs=<runs>] [--dial=<path-to-dialers-file>] [--help]
|
|
75
76
|
|
|
76
77
|
Positional arguments:
|
|
77
78
|
path-to-clarinet-project - The path to the Clarinet project.
|
|
@@ -80,45 +81,61 @@ const helpMessage = `
|
|
|
80
81
|
|
|
81
82
|
Options:
|
|
82
83
|
--seed - The seed to use for the replay functionality.
|
|
83
|
-
--path - The path to use for the replay functionality.
|
|
84
84
|
--runs - The runs to use for iterating over the tests. Default: 100.
|
|
85
85
|
--bail - Stop after the first failure.
|
|
86
86
|
--dial – The path to a JavaScript file containing custom pre- and post-execution functions (dialers).
|
|
87
87
|
--help - Show the help message.
|
|
88
88
|
`;
|
|
89
|
-
const parseBooleanOption = (argName) => process.argv.slice(4).includes(`--${argName}`);
|
|
90
|
-
const parseOption = (argName) => {
|
|
91
|
-
var _a;
|
|
92
|
-
return (_a = process.argv
|
|
93
|
-
.find((arg, idx) => idx >= 4 && arg.toLowerCase().startsWith(`--${argName}`))) === null || _a === void 0 ? void 0 : _a.split("=")[1];
|
|
94
|
-
};
|
|
95
89
|
function main() {
|
|
96
90
|
return __awaiter(this, void 0, void 0, function* () {
|
|
97
|
-
var _a;
|
|
98
91
|
const radio = new events_1.EventEmitter();
|
|
99
92
|
radio.on("logMessage", (log) => logger(log));
|
|
100
93
|
radio.on("logFailure", (log) => logger((0, ansicolor_1.red)(log), "error"));
|
|
101
|
-
const
|
|
102
|
-
|
|
94
|
+
const { positionals: positionalArgs, values: options } = (0, util_1.parseArgs)({
|
|
95
|
+
allowPositionals: true,
|
|
96
|
+
args: process.argv.slice(2),
|
|
97
|
+
options: {
|
|
98
|
+
seed: { type: "string" },
|
|
99
|
+
runs: { type: "string" },
|
|
100
|
+
dial: { type: "string" },
|
|
101
|
+
bail: { type: "boolean" },
|
|
102
|
+
help: { type: "boolean", short: "h" },
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const [manifestDir, sutContractName, type] = positionalArgs;
|
|
106
|
+
const runConfig = {
|
|
107
|
+
/** The relative path to the Clarinet project. */
|
|
108
|
+
manifestDir: manifestDir,
|
|
109
|
+
/** The target contract name. */
|
|
110
|
+
sutContractName: sutContractName,
|
|
111
|
+
/** The type of testing to be executed. Valid values: test, invariant. */
|
|
112
|
+
type: type === null || type === void 0 ? void 0 : type.toLowerCase(),
|
|
113
|
+
/** The seed to use for the replay functionality. */
|
|
114
|
+
seed: options.seed ? parseInt(options.seed, 10) : undefined,
|
|
115
|
+
/** The number of runs to use. */
|
|
116
|
+
runs: options.runs ? parseInt(options.runs, 10) : undefined,
|
|
117
|
+
/** Whether to bail on the first failure. */
|
|
118
|
+
bail: options.bail || false,
|
|
119
|
+
/** The path to the dialer file. */
|
|
120
|
+
dial: options.dial || undefined,
|
|
121
|
+
/** Whether to show the help message. */
|
|
122
|
+
help: options.help || false,
|
|
123
|
+
};
|
|
124
|
+
if (runConfig.help) {
|
|
103
125
|
radio.emit("logMessage", helpMessage);
|
|
104
126
|
return;
|
|
105
127
|
}
|
|
106
|
-
|
|
107
|
-
const manifestDir = args[2];
|
|
108
|
-
if (!manifestDir || manifestDir.startsWith("--")) {
|
|
128
|
+
if (!runConfig.manifestDir) {
|
|
109
129
|
radio.emit("logMessage", (0, ansicolor_1.red)("\nNo path to Clarinet project provided. Supply it immediately or face the relentless scrutiny of your contract's vulnerabilities."));
|
|
110
130
|
radio.emit("logMessage", helpMessage);
|
|
111
131
|
return;
|
|
112
132
|
}
|
|
113
|
-
|
|
114
|
-
const sutContractName = args[3];
|
|
115
|
-
if (!sutContractName || sutContractName.startsWith("--")) {
|
|
133
|
+
if (!runConfig.sutContractName) {
|
|
116
134
|
radio.emit("logMessage", (0, ansicolor_1.red)("\nNo target contract name provided. Please provide the contract name to be fuzzed."));
|
|
117
135
|
radio.emit("logMessage", helpMessage);
|
|
118
136
|
return;
|
|
119
137
|
}
|
|
120
|
-
|
|
121
|
-
if (!type || type.startsWith("--") || !["test", "invariant"].includes(type)) {
|
|
138
|
+
if (!runConfig.type || !["test", "invariant"].includes(runConfig.type)) {
|
|
122
139
|
radio.emit("logMessage", (0, ansicolor_1.red)("\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant."));
|
|
123
140
|
radio.emit("logMessage", helpMessage);
|
|
124
141
|
return;
|
|
@@ -127,59 +144,49 @@ function main() {
|
|
|
127
144
|
* The relative path to the manifest file, either `Clarinet.toml` or
|
|
128
145
|
* `Clarinet-<contract-name>.toml`. If the latter exists, it is used.
|
|
129
146
|
*/
|
|
130
|
-
const manifestPath = (0, path_1.join)(manifestDir, (0, exports.getManifestFileName)(manifestDir, sutContractName));
|
|
147
|
+
const manifestPath = (0, path_1.join)(runConfig.manifestDir, (0, exports.getManifestFileName)(runConfig.manifestDir, runConfig.sutContractName));
|
|
131
148
|
radio.emit("logMessage", `Using manifest path: ${manifestPath}`);
|
|
132
|
-
radio.emit("logMessage", `Target contract: ${sutContractName}`);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
radio.emit("logMessage", `Using seed: ${seed}`);
|
|
136
|
-
}
|
|
137
|
-
const path = parseOption("path") || undefined;
|
|
138
|
-
if (path !== undefined) {
|
|
139
|
-
radio.emit("logMessage", `Using path: ${path}`);
|
|
149
|
+
radio.emit("logMessage", `Target contract: ${runConfig.sutContractName}`);
|
|
150
|
+
if (runConfig.seed !== undefined) {
|
|
151
|
+
radio.emit("logMessage", `Using seed: ${runConfig.seed}`);
|
|
140
152
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
radio.emit("logMessage", `Using runs: ${runs}`);
|
|
153
|
+
if (runConfig.runs !== undefined) {
|
|
154
|
+
radio.emit("logMessage", `Using runs: ${runConfig.runs}`);
|
|
144
155
|
}
|
|
145
|
-
|
|
146
|
-
if (bail) {
|
|
156
|
+
if (runConfig.bail) {
|
|
147
157
|
radio.emit("logMessage", `Bailing on first failure.`);
|
|
148
158
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
* custom pre and post-execution JavaScript functions to be executed before
|
|
152
|
-
* and after the public function calls during invariant testing.
|
|
153
|
-
*/
|
|
154
|
-
const dialPath = parseOption("dial") || undefined;
|
|
155
|
-
if (dialPath !== undefined) {
|
|
156
|
-
radio.emit("logMessage", `Using dial path: ${dialPath}`);
|
|
159
|
+
if (runConfig.dial !== undefined) {
|
|
160
|
+
radio.emit("logMessage", `Using dial path: ${runConfig.dial}`);
|
|
157
161
|
}
|
|
158
162
|
/**
|
|
159
163
|
* The dialer registry, which is used to keep track of all the custom dialers
|
|
160
164
|
* registered by the user using the `--dial` flag.
|
|
161
165
|
*/
|
|
162
|
-
const dialerRegistry =
|
|
166
|
+
const dialerRegistry = runConfig.dial !== undefined
|
|
167
|
+
? new dialer_1.DialerRegistry(runConfig.dial)
|
|
168
|
+
: undefined;
|
|
163
169
|
if (dialerRegistry !== undefined) {
|
|
164
170
|
dialerRegistry.registerDialers();
|
|
165
171
|
}
|
|
166
172
|
const remoteDataSettings = (0, exports.tryParseRemoteDataSettings)(manifestPath, radio);
|
|
167
|
-
const simnet = yield (0, citizen_1.issueFirstClassCitizenship)(manifestDir, manifestPath, remoteDataSettings, sutContractName);
|
|
173
|
+
const simnet = yield (0, citizen_1.issueFirstClassCitizenship)(runConfig.manifestDir, manifestPath, remoteDataSettings, runConfig.sutContractName);
|
|
168
174
|
/**
|
|
169
175
|
* The list of contract IDs for the SUT contract names, as per the simnet.
|
|
170
176
|
*/
|
|
171
|
-
const rendezvousList = Array.from((0, shared_1.getSimnetDeployerContractsInterfaces)(simnet).keys()).filter((deployedContract) => (0, shared_1.getContractNameFromContractId)(deployedContract) ===
|
|
177
|
+
const rendezvousList = Array.from((0, shared_1.getSimnetDeployerContractsInterfaces)(simnet).keys()).filter((deployedContract) => (0, shared_1.getContractNameFromContractId)(deployedContract) ===
|
|
178
|
+
runConfig.sutContractName);
|
|
172
179
|
const rendezvousAllFunctions = (0, shared_1.getFunctionsFromContractInterfaces)(new Map(Array.from((0, shared_1.getSimnetDeployerContractsInterfaces)(simnet)).filter(([contractId]) => rendezvousList.includes(contractId))));
|
|
173
180
|
// Select the testing routine based on `type`.
|
|
174
181
|
// If "invariant", call `checkInvariants` to verify contract invariants.
|
|
175
182
|
// If "test", call `checkProperties` for property-based testing.
|
|
176
|
-
switch (type) {
|
|
183
|
+
switch (runConfig.type) {
|
|
177
184
|
case "invariant": {
|
|
178
|
-
yield (0, invariant_1.checkInvariants)(simnet, sutContractName, rendezvousList, rendezvousAllFunctions, seed,
|
|
185
|
+
yield (0, invariant_1.checkInvariants)(simnet, runConfig.sutContractName, rendezvousList, rendezvousAllFunctions, runConfig.seed, runConfig.runs, runConfig.bail, dialerRegistry, radio);
|
|
179
186
|
break;
|
|
180
187
|
}
|
|
181
188
|
case "test": {
|
|
182
|
-
(0, property_1.checkProperties)(simnet, sutContractName, rendezvousList, rendezvousAllFunctions, seed,
|
|
189
|
+
(0, property_1.checkProperties)(simnet, runConfig.sutContractName, rendezvousList, rendezvousAllFunctions, runConfig.seed, runConfig.runs, runConfig.bail, radio);
|
|
183
190
|
break;
|
|
184
191
|
}
|
|
185
192
|
}
|
package/dist/invariant.js
CHANGED
|
@@ -29,7 +29,6 @@ const dialer_1 = require("./dialer");
|
|
|
29
29
|
* @param rendezvousAllFunctions The map of all function interfaces for each
|
|
30
30
|
* target contract.
|
|
31
31
|
* @param seed The seed for reproducible invariant testing.
|
|
32
|
-
* @param path The path for reproducible invariant testing.
|
|
33
32
|
* @param runs The number of test runs.
|
|
34
33
|
* @param bail Stop execution after the first failure and prevent further
|
|
35
34
|
* shrinking.
|
|
@@ -37,7 +36,7 @@ const dialer_1 = require("./dialer");
|
|
|
37
36
|
* @param radio The custom logging event emitter.
|
|
38
37
|
* @returns void
|
|
39
38
|
*/
|
|
40
|
-
const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed,
|
|
39
|
+
const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, runs, bail, dialerRegistry, radio) => __awaiter(void 0, void 0, void 0, function* () {
|
|
41
40
|
const statistics = {
|
|
42
41
|
sut: {
|
|
43
42
|
successful: new Map(),
|
|
@@ -48,13 +47,14 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
|
|
|
48
47
|
failed: new Map(),
|
|
49
48
|
},
|
|
50
49
|
};
|
|
50
|
+
// The Rendezvous identifier is the first one in the list. Only one contract
|
|
51
|
+
// can be fuzzed at a time.
|
|
52
|
+
const rendezvousContractId = rendezvousList[0];
|
|
51
53
|
// A map where the keys are the Rendezvous identifiers and the values are
|
|
52
54
|
// arrays of their SUT (System Under Test) functions. This map will be used
|
|
53
55
|
// to access the SUT functions for each Rendezvous contract afterwards.
|
|
54
56
|
const rendezvousSutFunctions = filterSutFunctions(rendezvousAllFunctions);
|
|
55
|
-
//
|
|
56
|
-
// can be fuzzed at a time.
|
|
57
|
-
const rendezvousContractId = rendezvousList[0];
|
|
57
|
+
// Initialize the statistics for the SUT functions.
|
|
58
58
|
for (const functionInterface of rendezvousSutFunctions.get(rendezvousContractId)) {
|
|
59
59
|
statistics.sut.successful.set(functionInterface.name, 0);
|
|
60
60
|
statistics.sut.failed.set(functionInterface.name, 0);
|
|
@@ -63,47 +63,79 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
|
|
|
63
63
|
// arrays of their invariant functions. This map will be used to access the
|
|
64
64
|
// invariant functions for each Rendezvous contract afterwards.
|
|
65
65
|
const rendezvousInvariantFunctions = filterInvariantFunctions(rendezvousAllFunctions);
|
|
66
|
+
// Initialize the statistics for the invariant functions.
|
|
66
67
|
for (const functionInterface of rendezvousInvariantFunctions.get(rendezvousContractId)) {
|
|
67
68
|
statistics.invariant.successful.set(functionInterface.name, 0);
|
|
68
69
|
statistics.invariant.failed.set(functionInterface.name, 0);
|
|
69
70
|
}
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const traitReferenceInvariantFunctions =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const projectTraitImplementations = (0, traits_1.extractProjectTraitImplementations)(simnet);
|
|
77
|
-
if (Object.entries(projectTraitImplementations).length === 0 &&
|
|
78
|
-
(traitReferenceSutFunctions.length > 0 ||
|
|
79
|
-
traitReferenceInvariantFunctions.length > 0)) {
|
|
80
|
-
const foundTraitReferenceMessage = traitReferenceSutFunctions.length > 0 &&
|
|
81
|
-
traitReferenceInvariantFunctions.length > 0
|
|
82
|
-
? "public functions and invariants"
|
|
83
|
-
: traitReferenceSutFunctions.length > 0
|
|
84
|
-
? "public functions"
|
|
85
|
-
: "invariants";
|
|
86
|
-
radio.emit("logMessage", (0, ansicolor_1.red)(`\nFound ${foundTraitReferenceMessage} referencing traits, but no trait implementations were found in the project.
|
|
87
|
-
\nNote: You can add contracts implementing traits either as project contracts or as Clarinet requirements. For more details, visit: https://www.hiro.so/clarinet/.
|
|
88
|
-
\n`));
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
71
|
+
const sutFunctions = rendezvousSutFunctions.get(rendezvousContractId);
|
|
72
|
+
const traitReferenceSutFunctions = sutFunctions.filter(traits_1.isTraitReferenceFunction);
|
|
73
|
+
const invariantFunctions = rendezvousInvariantFunctions.get(rendezvousContractId);
|
|
74
|
+
const traitReferenceInvariantFunctions = invariantFunctions.filter(traits_1.isTraitReferenceFunction);
|
|
75
|
+
const sutTraitReferenceMap = (0, traits_1.buildTraitReferenceMap)(sutFunctions);
|
|
76
|
+
const invariantTraitReferenceMap = (0, traits_1.buildTraitReferenceMap)(invariantFunctions);
|
|
91
77
|
const enrichedSutFunctionsInterfaces = traitReferenceSutFunctions.length > 0
|
|
92
|
-
? (0, traits_1.enrichInterfaceWithTraitData)(simnet.getContractAST(targetContractName),
|
|
78
|
+
? (0, traits_1.enrichInterfaceWithTraitData)(simnet.getContractAST(targetContractName), sutTraitReferenceMap, sutFunctions, rendezvousContractId)
|
|
93
79
|
: rendezvousSutFunctions;
|
|
94
80
|
const enrichedInvariantFunctionsInterfaces = traitReferenceInvariantFunctions.length > 0
|
|
95
|
-
? (0, traits_1.enrichInterfaceWithTraitData)(simnet.getContractAST(targetContractName),
|
|
81
|
+
? (0, traits_1.enrichInterfaceWithTraitData)(simnet.getContractAST(targetContractName), invariantTraitReferenceMap, invariantFunctions, rendezvousContractId)
|
|
96
82
|
: rendezvousInvariantFunctions;
|
|
83
|
+
// Map all the project/requirement contracts to the traits they implement.
|
|
84
|
+
const projectTraitImplementations = (0, traits_1.extractProjectTraitImplementations)(simnet);
|
|
85
|
+
// Extract SUT functions with missing trait implementations. These functions
|
|
86
|
+
// will be skipped during invariant testing. Otherwise, the invariant testing
|
|
87
|
+
// routine can fail during argument generation.
|
|
88
|
+
const sutFunctionsWithMissingTraits = (0, traits_1.getNonTestableTraitFunctions)(enrichedSutFunctionsInterfaces, sutTraitReferenceMap, projectTraitImplementations, rendezvousContractId);
|
|
89
|
+
// Extract invariant functions with missing trait implementations. These
|
|
90
|
+
// functions will be skipped during invariant testing. Otherwise, the
|
|
91
|
+
// invariant testing routine can fail during argument generation.
|
|
92
|
+
const invariantFunctionsWithMissingTraits = (0, traits_1.getNonTestableTraitFunctions)(enrichedInvariantFunctionsInterfaces, invariantTraitReferenceMap, projectTraitImplementations, rendezvousContractId);
|
|
93
|
+
if (sutFunctionsWithMissingTraits.length > 0 ||
|
|
94
|
+
invariantFunctionsWithMissingTraits.length > 0) {
|
|
95
|
+
if (sutFunctionsWithMissingTraits.length > 0) {
|
|
96
|
+
const functionList = sutFunctionsWithMissingTraits
|
|
97
|
+
.map((fn) => ` - ${fn}`)
|
|
98
|
+
.join("\n");
|
|
99
|
+
radio.emit("logMessage", (0, ansicolor_1.yellow)(`\nWarning: The following SUT functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`));
|
|
100
|
+
}
|
|
101
|
+
if (invariantFunctionsWithMissingTraits.length > 0) {
|
|
102
|
+
const functionList = invariantFunctionsWithMissingTraits
|
|
103
|
+
.map((fn) => ` - ${fn}`)
|
|
104
|
+
.join("\n");
|
|
105
|
+
radio.emit("logMessage", (0, ansicolor_1.yellow)(`\nWarning: The following invariant functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`));
|
|
106
|
+
}
|
|
107
|
+
radio.emit("logMessage", (0, ansicolor_1.yellow)(`Note: You can add contracts implementing traits either as project contracts or as Clarinet requirements.\n`));
|
|
108
|
+
}
|
|
109
|
+
// Filter out functions with missing trait implementations from the enriched
|
|
110
|
+
// map.
|
|
111
|
+
const executableSutFunctions = new Map([
|
|
112
|
+
[
|
|
113
|
+
rendezvousContractId,
|
|
114
|
+
enrichedSutFunctionsInterfaces
|
|
115
|
+
.get(rendezvousContractId)
|
|
116
|
+
.filter((f) => !sutFunctionsWithMissingTraits.includes(f.name)),
|
|
117
|
+
],
|
|
118
|
+
]);
|
|
119
|
+
// Filter out functions with missing trait implementations from the enriched
|
|
120
|
+
// map.
|
|
121
|
+
const executableInvariantFunctions = new Map([
|
|
122
|
+
[
|
|
123
|
+
rendezvousContractId,
|
|
124
|
+
enrichedInvariantFunctionsInterfaces
|
|
125
|
+
.get(rendezvousContractId)
|
|
126
|
+
.filter((f) => !invariantFunctionsWithMissingTraits.includes(f.name)),
|
|
127
|
+
],
|
|
128
|
+
]);
|
|
97
129
|
// Set up local context to track SUT function call counts.
|
|
98
|
-
const localContext = (0, exports.initializeLocalContext)(
|
|
130
|
+
const localContext = (0, exports.initializeLocalContext)(executableSutFunctions);
|
|
99
131
|
// Set up context in simnet by initializing state for SUT.
|
|
100
|
-
(0, exports.initializeClarityContext)(simnet,
|
|
132
|
+
(0, exports.initializeClarityContext)(simnet, executableSutFunctions);
|
|
101
133
|
radio.emit("logMessage", `\nStarting invariant testing type for the ${targetContractName} contract...\n`);
|
|
102
134
|
const simnetAccounts = simnet.getAccounts();
|
|
103
135
|
const eligibleAccounts = new Map([...simnetAccounts].filter(([key]) => key !== "faucet"));
|
|
104
136
|
const simnetAddresses = Array.from(simnetAccounts.values());
|
|
105
|
-
const functions = (0, shared_1.getFunctionsListForContract)(
|
|
106
|
-
const invariants = (0, shared_1.getFunctionsListForContract)(
|
|
137
|
+
const functions = (0, shared_1.getFunctionsListForContract)(executableSutFunctions, rendezvousContractId);
|
|
138
|
+
const invariants = (0, shared_1.getFunctionsListForContract)(executableInvariantFunctions, rendezvousContractId);
|
|
107
139
|
if ((functions === null || functions === void 0 ? void 0 : functions.length) === 0) {
|
|
108
140
|
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`));
|
|
109
141
|
return;
|
|
@@ -325,7 +357,6 @@ const checkInvariants = (simnet, targetContractName, rendezvousList, rendezvousA
|
|
|
325
357
|
})), {
|
|
326
358
|
endOnFailure: bail,
|
|
327
359
|
numRuns: runs,
|
|
328
|
-
path: path,
|
|
329
360
|
reporter: radioReporter,
|
|
330
361
|
seed: seed,
|
|
331
362
|
verbose: true,
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacks/rendezvous",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Meet your contract's vulnerabilities head-on.",
|
|
5
5
|
"main": "app.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,8 @@
|
|
|
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 jest
|
|
12
|
-
"test:coverage": "npx
|
|
11
|
+
"test": "npx jest",
|
|
12
|
+
"test:coverage": "npx jest --coverage"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"stacks",
|
package/dist/property.js
CHANGED
|
@@ -19,14 +19,13 @@ const traits_1 = require("./traits");
|
|
|
19
19
|
* @param rendezvousAllFunctions A map of all target contract IDs to their
|
|
20
20
|
* function interfaces.
|
|
21
21
|
* @param seed The seed for reproducible property-based tests.
|
|
22
|
-
* @param path The path for reproducible property-based tests.
|
|
23
22
|
* @param runs The number of test runs.
|
|
24
23
|
* @param bail Stop execution after the first failure and prevent further
|
|
25
24
|
* shrinking.
|
|
26
25
|
* @param radio The custom logging event emitter.
|
|
27
26
|
* @returns void
|
|
28
27
|
*/
|
|
29
|
-
const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed,
|
|
28
|
+
const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousAllFunctions, seed, runs, bail, radio) => {
|
|
30
29
|
const statistics = {
|
|
31
30
|
test: {
|
|
32
31
|
successful: new Map(),
|
|
@@ -44,20 +43,39 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
|
|
|
44
43
|
statistics.test.discarded.set(functionInterface.name, 0);
|
|
45
44
|
statistics.test.failed.set(functionInterface.name, 0);
|
|
46
45
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const allTestFunctions = testContractsTestFunctions.get(testContractId);
|
|
47
|
+
const traitReferenceFunctionsCount = allTestFunctions.filter(traits_1.isTraitReferenceFunction).length;
|
|
48
|
+
const traitReferenceMap = (0, traits_1.buildTraitReferenceMap)(allTestFunctions);
|
|
49
|
+
const enrichedTestFunctionsInterfaces = traitReferenceFunctionsCount > 0
|
|
50
|
+
? (0, traits_1.enrichInterfaceWithTraitData)(simnet.getContractAST(targetContractName), traitReferenceMap, allTestFunctions, testContractId)
|
|
51
|
+
: testContractsTestFunctions;
|
|
50
52
|
const projectTraitImplementations = (0, traits_1.extractProjectTraitImplementations)(simnet);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
// If the tests contain trait reference functions, extract the functions with
|
|
54
|
+
// missing trait implementations. These functions need to be skipped during
|
|
55
|
+
// property-based testing. Otherwise, the property-based testing routine will
|
|
56
|
+
// eventually fail during argument generation.
|
|
57
|
+
const functionsMissingTraitImplementations = traitReferenceFunctionsCount > 0
|
|
58
|
+
? (0, traits_1.getNonTestableTraitFunctions)(enrichedTestFunctionsInterfaces, traitReferenceMap, projectTraitImplementations, testContractId)
|
|
59
|
+
: [];
|
|
60
|
+
// If the tests contain trait reference functions without eligible trait
|
|
61
|
+
// implementations, log a warning and filter out the functions.
|
|
62
|
+
if (functionsMissingTraitImplementations.length > 0) {
|
|
63
|
+
const functionList = functionsMissingTraitImplementations
|
|
64
|
+
.map((fn) => ` - ${fn}`)
|
|
65
|
+
.join("\n");
|
|
66
|
+
radio.emit("logMessage", (0, ansicolor_1.yellow)(`\nWarning: The following test functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`));
|
|
67
|
+
radio.emit("logMessage", (0, ansicolor_1.yellow)(`Note: You can add contracts implementing traits either as project contracts or as requirements.\n`));
|
|
57
68
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
// Filter out test functions with missing trait implementations from the
|
|
70
|
+
// enriched map.
|
|
71
|
+
const executableTestContractsTestFunctions = new Map([
|
|
72
|
+
[
|
|
73
|
+
testContractId,
|
|
74
|
+
enrichedTestFunctionsInterfaces
|
|
75
|
+
.get(testContractId)
|
|
76
|
+
.filter((functionInterface) => !functionsMissingTraitImplementations.includes(functionInterface.name)),
|
|
77
|
+
],
|
|
78
|
+
]);
|
|
61
79
|
radio.emit("logMessage", `\nStarting property testing type for the ${targetContractName} contract...\n`);
|
|
62
80
|
// Search for discard functions, for each test function. This map will
|
|
63
81
|
// be used to pair the test functions with their corresponding discard
|
|
@@ -69,7 +87,7 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
|
|
|
69
87
|
// Pair each test function with its corresponding discard function. When a
|
|
70
88
|
// test function is selected, Rendezvous will first call its discard
|
|
71
89
|
// function, to allow or prevent the property test from running.
|
|
72
|
-
const testContractsPairedFunctions = new Map(Array.from(
|
|
90
|
+
const testContractsPairedFunctions = new Map(Array.from(executableTestContractsTestFunctions, ([contractId, functions]) => [
|
|
73
91
|
contractId,
|
|
74
92
|
new Map(functions.map((f) => {
|
|
75
93
|
var _a;
|
|
@@ -87,7 +105,7 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
|
|
|
87
105
|
const simnetAccounts = simnet.getAccounts();
|
|
88
106
|
const eligibleAccounts = new Map([...simnetAccounts].filter(([key]) => key !== "faucet"));
|
|
89
107
|
const simnetAddresses = Array.from(simnetAccounts.values());
|
|
90
|
-
const testFunctions = (0, shared_1.getFunctionsListForContract)(
|
|
108
|
+
const testFunctions = (0, shared_1.getFunctionsListForContract)(executableTestContractsTestFunctions, testContractId);
|
|
91
109
|
if ((testFunctions === null || testFunctions === void 0 ? void 0 : testFunctions.length) === 0) {
|
|
92
110
|
radio.emit("logMessage", (0, ansicolor_1.red)(`No test functions found for the "${targetContractName}" contract.\n`));
|
|
93
111
|
return;
|
|
@@ -223,7 +241,6 @@ const checkProperties = (simnet, targetContractName, rendezvousList, rendezvousA
|
|
|
223
241
|
}), {
|
|
224
242
|
endOnFailure: bail,
|
|
225
243
|
numRuns: runs,
|
|
226
|
-
path: path,
|
|
227
244
|
reporter: radioReporter,
|
|
228
245
|
seed: seed,
|
|
229
246
|
verbose: true,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createIsolatedTestEnvironment = createIsolatedTestEnvironment;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const os_1 = require("os");
|
|
7
|
+
/**
|
|
8
|
+
* Creates an isolated test environment by copying the Clarinet project to a
|
|
9
|
+
* unique temporary directory. This prevents race conditions when multiple
|
|
10
|
+
* tests try to initialize simnet concurrently.
|
|
11
|
+
*
|
|
12
|
+
* @param manifestDir - The absolute path to the manifest directory.
|
|
13
|
+
* @param testPrefix - Prefix for the temporary directory name.
|
|
14
|
+
* @returns The path to the temporary directory containing the isolated project
|
|
15
|
+
* copy.
|
|
16
|
+
*/
|
|
17
|
+
function createIsolatedTestEnvironment(manifestDir, testPrefix) {
|
|
18
|
+
const tempDir = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), testPrefix));
|
|
19
|
+
(0, fs_1.cpSync)(manifestDir, tempDir, { recursive: true });
|
|
20
|
+
return tempDir;
|
|
21
|
+
}
|
package/dist/traits.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.extractProjectTraitImplementations = exports.isTraitReferenceFunction = exports.getContractIdsImplementingTrait = exports.buildTraitReferenceMap = exports.getTraitReferenceData = exports.enrichInterfaceWithTraitData = void 0;
|
|
3
|
+
exports.getNonTestableTraitFunctions = exports.extractProjectTraitImplementations = exports.isTraitReferenceFunction = exports.getContractIdsImplementingTrait = exports.buildTraitReferenceMap = exports.getTraitReferenceData = exports.enrichInterfaceWithTraitData = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Enriches a contract interface with trait reference data. Before enrichment,
|
|
6
6
|
* the contract interface lacks trait reference data for parameters. This
|
|
@@ -329,10 +329,15 @@ const getContractIdsImplementingTrait = (trait, projectTraitImplementations) =>
|
|
|
329
329
|
const filteredContracts = contracts.filter((contractId) => {
|
|
330
330
|
var _a;
|
|
331
331
|
const traitImplemented = (_a = projectTraitImplementations[contractId]) === null || _a === void 0 ? void 0 : _a.some((implementedTrait) => {
|
|
332
|
-
var _a, _b;
|
|
333
|
-
const isTraitNamesMatch = implementedTrait.name ===
|
|
332
|
+
var _a, _b, _c, _d;
|
|
333
|
+
const isTraitNamesMatch = implementedTrait.name ===
|
|
334
|
+
((_a = trait.import.Imported) === null || _a === void 0 ? void 0 : _a.name) ||
|
|
335
|
+
implementedTrait.name ===
|
|
336
|
+
((_b = trait.import.Defined) === null || _b === void 0 ? void 0 : _b.name);
|
|
334
337
|
const isTraitIssuersMatch = JSON.stringify(implementedTrait.contract_identifier.issuer) ===
|
|
335
|
-
JSON.stringify((
|
|
338
|
+
JSON.stringify((_c = trait.import.Imported) === null || _c === void 0 ? void 0 : _c.contract_identifier.issuer) ||
|
|
339
|
+
JSON.stringify(implementedTrait.contract_identifier.issuer) ===
|
|
340
|
+
JSON.stringify((_d = trait.import.Defined) === null || _d === void 0 ? void 0 : _d.contract_identifier.issuer);
|
|
336
341
|
return isTraitNamesMatch && isTraitIssuersMatch;
|
|
337
342
|
});
|
|
338
343
|
return traitImplemented;
|
|
@@ -403,3 +408,45 @@ const extractProjectTraitImplementations = (simnet) => {
|
|
|
403
408
|
return projectTraitImplementations;
|
|
404
409
|
};
|
|
405
410
|
exports.extractProjectTraitImplementations = extractProjectTraitImplementations;
|
|
411
|
+
/**
|
|
412
|
+
* Filters functions that reference traits without eligible implementations in
|
|
413
|
+
* the project. Recursively checks nested parameters for trait reference types.
|
|
414
|
+
* @param enrichedFunctionsInterfaces The map of contract IDs to enriched
|
|
415
|
+
* function interfaces with trait reference data.
|
|
416
|
+
* @param traitReferenceMap The map of function names to their trait reference
|
|
417
|
+
* parameter data.
|
|
418
|
+
* @param projectTraitImplementations The record of contract IDs to their
|
|
419
|
+
* implemented traits.
|
|
420
|
+
* @param contractId The contract ID to filter functions for.
|
|
421
|
+
* @returns An array of function names with trait references without eligible
|
|
422
|
+
* trait implementations.
|
|
423
|
+
*/
|
|
424
|
+
const getNonTestableTraitFunctions = (enrichedFunctionsInterfaces, traitReferenceMap, projectTraitImplementations, contractId) => {
|
|
425
|
+
const hasTraitReferenceWithoutImplementation = (type) => {
|
|
426
|
+
if (!type)
|
|
427
|
+
return false;
|
|
428
|
+
if (typeof type === "object" && "trait_reference" in type) {
|
|
429
|
+
const contractIdsImplementingTrait = (0, exports.getContractIdsImplementingTrait)(type.trait_reference, projectTraitImplementations);
|
|
430
|
+
return contractIdsImplementingTrait.length === 0;
|
|
431
|
+
}
|
|
432
|
+
if (typeof type === "object") {
|
|
433
|
+
return (("list" in type &&
|
|
434
|
+
hasTraitReferenceWithoutImplementation(type.list.type)) ||
|
|
435
|
+
("tuple" in type &&
|
|
436
|
+
type.tuple.some((item) => hasTraitReferenceWithoutImplementation(item.type))) ||
|
|
437
|
+
("optional" in type &&
|
|
438
|
+
hasTraitReferenceWithoutImplementation(type.optional)) ||
|
|
439
|
+
("response" in type &&
|
|
440
|
+
(hasTraitReferenceWithoutImplementation(type.response.ok) ||
|
|
441
|
+
hasTraitReferenceWithoutImplementation(type.response.error))));
|
|
442
|
+
}
|
|
443
|
+
return false;
|
|
444
|
+
};
|
|
445
|
+
return Array.from(traitReferenceMap.keys()).filter((functionName) => {
|
|
446
|
+
var _a, _b;
|
|
447
|
+
const enrichedFunctionInterface = (_a = enrichedFunctionsInterfaces
|
|
448
|
+
.get(contractId)) === null || _a === void 0 ? void 0 : _a.find((f) => f.name === functionName);
|
|
449
|
+
return ((_b = enrichedFunctionInterface === null || enrichedFunctionInterface === void 0 ? void 0 : enrichedFunctionInterface.args.some((param) => hasTraitReferenceWithoutImplementation(param.type))) !== null && _b !== void 0 ? _b : false);
|
|
450
|
+
});
|
|
451
|
+
};
|
|
452
|
+
exports.getNonTestableTraitFunctions = getNonTestableTraitFunctions;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacks/rendezvous",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Meet your contract's vulnerabilities head-on.",
|
|
5
5
|
"main": "app.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,8 @@
|
|
|
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 jest
|
|
12
|
-
"test:coverage": "npx
|
|
11
|
+
"test": "npx jest",
|
|
12
|
+
"test:coverage": "npx jest --coverage"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"stacks",
|