@stacks/rendezvous 0.1.2
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/LICENSE +674 -0
- package/README.md +127 -0
- package/dist/app.js +116 -0
- package/dist/citizen.js +231 -0
- package/dist/citizen.types.js +2 -0
- package/dist/heatstroke.js +76 -0
- package/dist/invariant.js +239 -0
- package/dist/invariant.types.js +2 -0
- package/dist/package.json +42 -0
- package/dist/property.js +247 -0
- package/dist/property.types.js +3 -0
- package/dist/shared.js +285 -0
- package/dist/shared.types.js +2 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img width="304" src="https://raw.githubusercontent.com/moodmosaic/nikosbaxevanis.com/gh-pages/images/rv.png" />
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
## Rendezvous `rv`: The Clarity Fuzzer
|
|
6
|
+
|
|
7
|
+
Rendezvous `rv` is a Clarity fuzzer designed to cut through your smart contract's defenses with precision. Uncover vulnerabilities with unmatched power and intensity. Get ready to meet your contract's vulnerabilities head-on.
|
|
8
|
+
|
|
9
|
+
### Prerequisites
|
|
10
|
+
|
|
11
|
+
- **Node.js**: Supported versions include 18, 20, and 22. Other versions may work, but they are untested.
|
|
12
|
+
|
|
13
|
+
### Inspiration
|
|
14
|
+
|
|
15
|
+
The `rv` fuzzer, inspired by John Hughes' paper _"Testing the Hard Stuff and Staying Sane"_[^1], ensures contract robustness with Clarity invariants and tests.
|
|
16
|
+
|
|
17
|
+
### Example Directory Structure
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
root
|
|
21
|
+
├── Clarinet.toml
|
|
22
|
+
├── contracts
|
|
23
|
+
│ ├── contract.clar
|
|
24
|
+
│ ├── contract.tests.clar
|
|
25
|
+
└── settings
|
|
26
|
+
└── Devnet.toml
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Installation
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
**Install the package locally**
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
npm install "https://github.com/stacks-network/rendezvous.git"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Run the fuzzer locally:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
npx rv <path-to-clarinet-project> <contract-name> <type>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
**Install the package globally**
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
git clone https://github.com/stacks-network/rendezvous
|
|
51
|
+
npm install
|
|
52
|
+
npm install --global .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Run the fuzzer from anywhere on your system:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
rv <path-to-clarinet-project> <contract-name> <type>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### Configuration
|
|
64
|
+
|
|
65
|
+
**Positional arguments:**
|
|
66
|
+
|
|
67
|
+
- `path-to-clarinet-project` - Path to the root directory of the Clarinet project (where Clarinet.toml exists).
|
|
68
|
+
- `contract-name` - Name of the contract to test, per Clarinet.toml.
|
|
69
|
+
- `type` - Type of test to run. Options:
|
|
70
|
+
- `test` - Run property-based tests.
|
|
71
|
+
- `invariant` - Run invariant tests.
|
|
72
|
+
|
|
73
|
+
**Options:**
|
|
74
|
+
|
|
75
|
+
- `--seed` – The seed to use for the replay functionality.
|
|
76
|
+
- `--path` – The path to use for the replay functionality.
|
|
77
|
+
- `--runs` – The number of test iterations to use for exercising the contracts.
|
|
78
|
+
(default: `100`)
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### Example (`test`)
|
|
83
|
+
|
|
84
|
+
Here's an example of a test that checks reversing a list twice returns the original:
|
|
85
|
+
|
|
86
|
+
```clarity
|
|
87
|
+
(define-public (test-reverse-list (seq (list 127 uint)))
|
|
88
|
+
(begin
|
|
89
|
+
(asserts!
|
|
90
|
+
(is-eq seq
|
|
91
|
+
(reverse-uint
|
|
92
|
+
(reverse-uint seq)))
|
|
93
|
+
(err u999))
|
|
94
|
+
(ok true)))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
You can run property-based tests using `rv` with the following command:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
rv example reverse test
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### Example (`invariant`)
|
|
106
|
+
|
|
107
|
+
Here's a Clarity invariant to detect a bug in the example counter contract:
|
|
108
|
+
|
|
109
|
+
```clarity
|
|
110
|
+
(define-read-only (invariant-counter-gt-zero)
|
|
111
|
+
(let
|
|
112
|
+
((increment-num-calls (default-to u0 (get called (map-get? context "increment"))))
|
|
113
|
+
(decrement-num-calls (default-to u0 (get called (map-get? context "decrement")))))
|
|
114
|
+
(if (> increment-num-calls decrement-num-calls)
|
|
115
|
+
(> (var-get counter) u0)
|
|
116
|
+
true)))
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
You can run invariant tests using `rv` with the following command:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
rv example counter invariant
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
[^1]: Hughes, J. (2004). _Testing the Hard Stuff and Staying Sane_. In Proceedings of the ACM SIGPLAN Workshop on Haskell (Haskell '04).
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
4
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
5
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
6
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
7
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
8
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
9
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.main = main;
|
|
14
|
+
const path_1 = require("path");
|
|
15
|
+
const events_1 = require("events");
|
|
16
|
+
const property_1 = require("./property");
|
|
17
|
+
const invariant_1 = require("./invariant");
|
|
18
|
+
const shared_1 = require("./shared");
|
|
19
|
+
const citizen_1 = require("./citizen");
|
|
20
|
+
const package_json_1 = require("./package.json");
|
|
21
|
+
const ansicolor_1 = require("ansicolor");
|
|
22
|
+
const logger = (log, logLevel = "log") => {
|
|
23
|
+
console[logLevel](log);
|
|
24
|
+
};
|
|
25
|
+
const helpMessage = `
|
|
26
|
+
rv v${package_json_1.version}
|
|
27
|
+
|
|
28
|
+
Usage: ./rv <path-to-clarinet-project> <contract-name> <type> [--seed=<seed>] [--path=<path>] [--runs=<runs>]
|
|
29
|
+
|
|
30
|
+
Positional arguments:
|
|
31
|
+
path-to-clarinet-project - The path to the Clarinet project.
|
|
32
|
+
contract-name - The name of the contract to be fuzzed.
|
|
33
|
+
type - The type to use for exercising the contracts. Possible values: test, invariant.
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
--seed - The seed to use for the replay functionality.
|
|
37
|
+
--path - The path to use for the replay functionality.
|
|
38
|
+
--runs - The runs to use for iterating over the tests. Default: 100.
|
|
39
|
+
--help - Show the help message.
|
|
40
|
+
`;
|
|
41
|
+
const parseOptionalArgument = (argName) => {
|
|
42
|
+
var _a;
|
|
43
|
+
return (_a = process.argv
|
|
44
|
+
.find((arg, idx) => idx >= 4 && arg.toLowerCase().startsWith(`--${argName}`))) === null || _a === void 0 ? void 0 : _a.split("=")[1];
|
|
45
|
+
};
|
|
46
|
+
function main() {
|
|
47
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
48
|
+
var _a;
|
|
49
|
+
const radio = new events_1.EventEmitter();
|
|
50
|
+
radio.on("logMessage", (log) => logger(log));
|
|
51
|
+
radio.on("logFailure", (log) => logger((0, ansicolor_1.red)(log), "error"));
|
|
52
|
+
const args = process.argv;
|
|
53
|
+
if (args.includes("--help")) {
|
|
54
|
+
radio.emit("logMessage", helpMessage);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
/** The relative path to the Clarinet project. */
|
|
58
|
+
const manifestDir = args[2];
|
|
59
|
+
if (!manifestDir || manifestDir.startsWith("--")) {
|
|
60
|
+
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."));
|
|
61
|
+
radio.emit("logMessage", helpMessage);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
/** The target contract name. */
|
|
65
|
+
const sutContractName = args[3];
|
|
66
|
+
if (!sutContractName || sutContractName.startsWith("--")) {
|
|
67
|
+
radio.emit("logMessage", (0, ansicolor_1.red)("\nNo target contract name provided. Please provide the contract name to be fuzzed."));
|
|
68
|
+
radio.emit("logMessage", helpMessage);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const type = (_a = args[4]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
72
|
+
if (!type || type.startsWith("--") || !["test", "invariant"].includes(type)) {
|
|
73
|
+
radio.emit("logMessage", (0, ansicolor_1.red)("\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant."));
|
|
74
|
+
radio.emit("logMessage", helpMessage);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
/** The relative path to `Clarinet.toml`. */
|
|
78
|
+
const manifestPath = (0, path_1.join)(manifestDir, "Clarinet.toml");
|
|
79
|
+
radio.emit("logMessage", `Using manifest path: ${manifestPath}`);
|
|
80
|
+
radio.emit("logMessage", `Target contract: ${sutContractName}`);
|
|
81
|
+
const seed = parseInt(parseOptionalArgument("seed"), 10) || undefined;
|
|
82
|
+
if (seed !== undefined) {
|
|
83
|
+
radio.emit("logMessage", `Using seed: ${seed}`);
|
|
84
|
+
}
|
|
85
|
+
const path = parseOptionalArgument("path") || undefined;
|
|
86
|
+
if (path !== undefined) {
|
|
87
|
+
radio.emit("logMessage", `Using path: ${path}`);
|
|
88
|
+
}
|
|
89
|
+
const runs = parseInt(parseOptionalArgument("runs"), 10) || undefined;
|
|
90
|
+
if (runs !== undefined) {
|
|
91
|
+
radio.emit("logMessage", `Using runs: ${runs}`);
|
|
92
|
+
}
|
|
93
|
+
const simnet = yield (0, citizen_1.issueFirstClassCitizenship)(manifestDir, sutContractName);
|
|
94
|
+
/**
|
|
95
|
+
* The list of contract IDs for the SUT contract names, as per the simnet.
|
|
96
|
+
*/
|
|
97
|
+
const rendezvousList = Array.from((0, shared_1.getSimnetDeployerContractsInterfaces)(simnet).keys()).filter((deployedContract) => [sutContractName].includes((0, shared_1.getContractNameFromContractId)(deployedContract)));
|
|
98
|
+
const rendezvousAllFunctions = (0, shared_1.getFunctionsFromContractInterfaces)(new Map(Array.from((0, shared_1.getSimnetDeployerContractsInterfaces)(simnet)).filter(([contractId]) => rendezvousList.includes(contractId))));
|
|
99
|
+
// Select the testing routine based on `type`.
|
|
100
|
+
// If "invariant", call `checkInvariants` to verify contract invariants.
|
|
101
|
+
// If "test", call `checkProperties` for property-based testing.
|
|
102
|
+
switch (type) {
|
|
103
|
+
case "invariant": {
|
|
104
|
+
(0, invariant_1.checkInvariants)(simnet, sutContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, radio);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "test": {
|
|
108
|
+
(0, property_1.checkProperties)(simnet, sutContractName, rendezvousList, rendezvousAllFunctions, seed, path, runs, radio);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (require.main === module) {
|
|
115
|
+
main();
|
|
116
|
+
}
|
package/dist/citizen.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.getTestContractSource = exports.buildRendezvousData = exports.getContractSource = exports.groupContractsByEpochFromSimnetPlan = exports.issueFirstClassCitizenship = void 0;
|
|
16
|
+
exports.scheduleRendezvous = scheduleRendezvous;
|
|
17
|
+
const fs_1 = require("fs");
|
|
18
|
+
const path_1 = require("path");
|
|
19
|
+
const yaml_1 = __importDefault(require("yaml"));
|
|
20
|
+
const clarinet_sdk_1 = require("@hirosystems/clarinet-sdk");
|
|
21
|
+
/**
|
|
22
|
+
* Prepares the simnet environment and assures the target contract is treated
|
|
23
|
+
* as a first-class citizen, by combining it with its tests. This function
|
|
24
|
+
* handles:
|
|
25
|
+
* - Contract sorting by epoch based on the deployment plan.
|
|
26
|
+
* - Combining the target contract with its tests and deploying all contracts
|
|
27
|
+
* to the simnet.
|
|
28
|
+
*
|
|
29
|
+
* @param manifestDir - The relative path to the manifest directory.
|
|
30
|
+
* @param sutContractName - The target contract name.
|
|
31
|
+
* @returns The initialized simnet instance with all contracts deployed, with
|
|
32
|
+
* the target contract treated as a first-class citizen.
|
|
33
|
+
*/
|
|
34
|
+
const issueFirstClassCitizenship = (manifestDir, sutContractName) => __awaiter(void 0, void 0, void 0, function* () {
|
|
35
|
+
const manifestPath = (0, path_1.join)(manifestDir, "Clarinet.toml");
|
|
36
|
+
// Initialize the simnet, to generate the simnet plan and instance. The empty
|
|
37
|
+
// session will be set up, and contracts will be deployed in the correct
|
|
38
|
+
// order based on the simnet plan a few lines below.
|
|
39
|
+
const simnet = yield (0, clarinet_sdk_1.initSimnet)(manifestPath);
|
|
40
|
+
const simnetPlan = yaml_1.default.parse((0, fs_1.readFileSync)((0, path_1.join)(manifestDir, "deployments", "default.simnet-plan.yaml"), {
|
|
41
|
+
encoding: "utf-8",
|
|
42
|
+
}));
|
|
43
|
+
const sortedContractsByEpoch = (0, exports.groupContractsByEpochFromSimnetPlan)(simnetPlan);
|
|
44
|
+
yield simnet.initEmptySession();
|
|
45
|
+
// Combine the target contract with its tests into a single contract. The
|
|
46
|
+
// resulting contract will replace the target contract in the simnet. This
|
|
47
|
+
// map stores the contract name and its corresponding source code.
|
|
48
|
+
const rendezvousSources = new Map([sutContractName]
|
|
49
|
+
.map((contractName) => (0, exports.buildRendezvousData)(simnetPlan, contractName, manifestDir))
|
|
50
|
+
.map((rendezvousContractData) => [
|
|
51
|
+
rendezvousContractData.rendezvousContractName,
|
|
52
|
+
rendezvousContractData.rendezvousSource,
|
|
53
|
+
]));
|
|
54
|
+
// Deploy the contracts to the simnet in the correct order.
|
|
55
|
+
yield deployContracts(simnet, sortedContractsByEpoch, (name, props) => (0, exports.getContractSource)([sutContractName], rendezvousSources, name, props, manifestDir));
|
|
56
|
+
return simnet;
|
|
57
|
+
});
|
|
58
|
+
exports.issueFirstClassCitizenship = issueFirstClassCitizenship;
|
|
59
|
+
/**
|
|
60
|
+
* Groups contracts by epoch from the simnet plan.
|
|
61
|
+
* @param simnetPlan - The simnet plan.
|
|
62
|
+
* @returns A record of contracts grouped by epoch. The record key is the epoch
|
|
63
|
+
* string, and the value is an array of contracts. Each contract is represented
|
|
64
|
+
* as a record with the contract name as the key and a record containing the
|
|
65
|
+
* contract path and clarity version as the value.
|
|
66
|
+
*/
|
|
67
|
+
const groupContractsByEpochFromSimnetPlan = (simnetPlan) => {
|
|
68
|
+
return simnetPlan.plan.batches.reduce((acc, batch) => {
|
|
69
|
+
const epoch = batch.epoch;
|
|
70
|
+
const contracts = batch.transactions
|
|
71
|
+
.filter((tx) => tx["emulated-contract-publish"])
|
|
72
|
+
.map((tx) => {
|
|
73
|
+
const contract = tx["emulated-contract-publish"];
|
|
74
|
+
return {
|
|
75
|
+
[contract["contract-name"]]: {
|
|
76
|
+
path: contract.path,
|
|
77
|
+
clarity_version: contract["clarity-version"],
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
if (contracts.length > 0) {
|
|
82
|
+
acc[epoch] = (acc[epoch] || []).concat(contracts);
|
|
83
|
+
}
|
|
84
|
+
return acc;
|
|
85
|
+
}, {});
|
|
86
|
+
};
|
|
87
|
+
exports.groupContractsByEpochFromSimnetPlan = groupContractsByEpochFromSimnetPlan;
|
|
88
|
+
/**
|
|
89
|
+
* Deploy 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
|
+
*/
|
|
94
|
+
const deployContracts = (simnet, contractsByEpoch, getContractSourceFn) => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
for (const [epoch, contracts] of Object.entries(contractsByEpoch)) {
|
|
96
|
+
// Move to the next epoch and deploy the contracts in the correct order.
|
|
97
|
+
simnet.setEpoch(epoch);
|
|
98
|
+
for (const contract of contracts.flatMap(Object.entries)) {
|
|
99
|
+
const [name, props] = contract;
|
|
100
|
+
const source = getContractSourceFn(name, props);
|
|
101
|
+
// For requirement contracts, use the original sender. The sender address
|
|
102
|
+
// is included in the path:
|
|
103
|
+
// "./.cache/requirements/<address>.contract-name.clar".
|
|
104
|
+
const sender = props.path.includes(".cache")
|
|
105
|
+
? props.path.split("requirements")[1].slice(1).split(".")[0]
|
|
106
|
+
: simnet.deployer;
|
|
107
|
+
simnet.deployContract(name, source, { clarityVersion: props.clarity_version }, sender);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
/**
|
|
112
|
+
* Conditionally retrieve the contract source based on whether the contract is
|
|
113
|
+
* a SUT contract or not.
|
|
114
|
+
* @param sutContractNames - The list of SUT contract names.
|
|
115
|
+
* @param rendezvousMap - The rendezvous map.
|
|
116
|
+
* @param contractName - The contract name.
|
|
117
|
+
* @param contractProps - The contract properties.
|
|
118
|
+
* @param manifestDir - The relative path to the manifest directory.
|
|
119
|
+
* @returns The contract source.
|
|
120
|
+
*/
|
|
121
|
+
const getContractSource = (sutContractNames, rendezvousMap, contractName, contractProps, manifestDir) => {
|
|
122
|
+
if (sutContractNames.includes(contractName)) {
|
|
123
|
+
const contractSource = rendezvousMap.get(contractName);
|
|
124
|
+
if (!contractSource) {
|
|
125
|
+
throw new Error(`Contract source not found for ${contractName}`);
|
|
126
|
+
}
|
|
127
|
+
return contractSource;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, contractProps.path), {
|
|
131
|
+
encoding: "utf-8",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
exports.getContractSource = getContractSource;
|
|
136
|
+
/**
|
|
137
|
+
* Build the Rendezvous data.
|
|
138
|
+
* @param simnetPlan The parsed simnet plan.
|
|
139
|
+
* @param contractName The contract name.
|
|
140
|
+
* @param manifestDir The relative path to the manifest directory.
|
|
141
|
+
* @returns The Rendezvous data representing an object. The returned object
|
|
142
|
+
* contains the Rendezvous source code and the Rendezvous contract name.
|
|
143
|
+
*/
|
|
144
|
+
const buildRendezvousData = (simnetPlan, contractName, manifestDir) => {
|
|
145
|
+
try {
|
|
146
|
+
const sutContractSource = getSimnetPlanContractSource(simnetPlan, manifestDir, contractName);
|
|
147
|
+
const testContractSource = (0, exports.getTestContractSource)(simnetPlan, contractName, manifestDir);
|
|
148
|
+
const rendezvousSource = scheduleRendezvous(sutContractSource, testContractSource);
|
|
149
|
+
return {
|
|
150
|
+
rendezvousSource: rendezvousSource,
|
|
151
|
+
rendezvousContractName: contractName,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
throw new Error(`Error processing "${contractName}" contract: ${e.message}`);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
exports.buildRendezvousData = buildRendezvousData;
|
|
159
|
+
/**
|
|
160
|
+
* Get the contract source code from the simnet plan.
|
|
161
|
+
* @param simnetPlan The parsed simnet plan.
|
|
162
|
+
* @param manifestDir The relative path to the manifest directory.
|
|
163
|
+
* @param sutContractName The target contract name.
|
|
164
|
+
* @returns The contract source code.
|
|
165
|
+
*/
|
|
166
|
+
const getSimnetPlanContractSource = (simnetPlan, manifestDir, sutContractName) => {
|
|
167
|
+
var _a;
|
|
168
|
+
// Filter for transactions that contain "emulated-contract-publish".
|
|
169
|
+
const contractInfo = (_a = simnetPlan.plan.batches
|
|
170
|
+
.flatMap((batch) => batch.transactions)
|
|
171
|
+
.find((transaction) => transaction["emulated-contract-publish"] &&
|
|
172
|
+
transaction["emulated-contract-publish"]["contract-name"] ===
|
|
173
|
+
sutContractName)) === null || _a === void 0 ? void 0 : _a["emulated-contract-publish"];
|
|
174
|
+
if (contractInfo == undefined) {
|
|
175
|
+
throw new Error(`"${sutContractName}" contract not found in Clarinet.toml.`);
|
|
176
|
+
}
|
|
177
|
+
return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, contractInfo.path), {
|
|
178
|
+
encoding: "utf-8",
|
|
179
|
+
}).toString();
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Get the test contract source code.
|
|
183
|
+
* @param simnetPlan The parsed simnet plan.
|
|
184
|
+
* @param sutContractName The target contract name.
|
|
185
|
+
* @param manifestDir The relative path to the manifest directory.
|
|
186
|
+
* @returns The test contract source code.
|
|
187
|
+
*/
|
|
188
|
+
const getTestContractSource = (simnetPlan, sutContractName, manifestDir) => {
|
|
189
|
+
var _a;
|
|
190
|
+
const contractInfo = (_a = simnetPlan.plan.batches
|
|
191
|
+
.flatMap((batch) => batch.transactions)
|
|
192
|
+
.find((transaction) => transaction["emulated-contract-publish"] &&
|
|
193
|
+
transaction["emulated-contract-publish"]["contract-name"] ===
|
|
194
|
+
sutContractName)) === null || _a === void 0 ? void 0 : _a["emulated-contract-publish"];
|
|
195
|
+
const sutContractPath = contractInfo.path;
|
|
196
|
+
const extension = ".clar";
|
|
197
|
+
if (!sutContractPath.endsWith(extension)) {
|
|
198
|
+
throw new Error(`Invalid contract extension for the "${sutContractName}" contract.`);
|
|
199
|
+
}
|
|
200
|
+
const testContractPath = sutContractPath.replace(extension, `.tests${extension}`);
|
|
201
|
+
try {
|
|
202
|
+
return (0, fs_1.readFileSync)((0, path_1.join)(manifestDir, testContractPath), {
|
|
203
|
+
encoding: "utf-8",
|
|
204
|
+
}).toString();
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
throw new Error(`Error retrieving the corresponding test contract for the "${sutContractName}" contract. ${e.message}`);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
exports.getTestContractSource = getTestContractSource;
|
|
211
|
+
/**
|
|
212
|
+
* Schedule a Rendezvous between the System Under Test (`SUT`) and the
|
|
213
|
+
* invariants.
|
|
214
|
+
* @param sutContractSource The SUT contract source code.
|
|
215
|
+
* @param invariants The invariants contract source code.
|
|
216
|
+
* @returns The Rendezvous source code.
|
|
217
|
+
*/
|
|
218
|
+
function scheduleRendezvous(sutContractSource, invariants) {
|
|
219
|
+
/**
|
|
220
|
+
* The `context` map tracks how many times each function has been called.
|
|
221
|
+
* This data can be useful for invariant tests to check behavior over time.
|
|
222
|
+
*/
|
|
223
|
+
const context = `(define-map context (string-ascii 100) {
|
|
224
|
+
called: uint
|
|
225
|
+
;; other data
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
(define-public (update-context (function-name (string-ascii 100)) (called uint))
|
|
229
|
+
(ok (map-set context function-name {called: called})))`;
|
|
230
|
+
return `${sutContractSource}\n\n${context}\n\n${invariants}`;
|
|
231
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.reporter = reporter;
|
|
12
|
+
/**
|
|
13
|
+
* Reports the details of a failed property-based test run.
|
|
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.
|
|
18
|
+
*
|
|
19
|
+
* @param runDetails - The details of the test run provided by fast-check.
|
|
20
|
+
* @property runDetails.failed - Indicates if the property test failed.
|
|
21
|
+
* @property runDetails.counterexample - The input that caused the failure.
|
|
22
|
+
* @property runDetails.numRuns - The number of test cases that were run.
|
|
23
|
+
* @property runDetails.seed - The seed used to generate the test cases.
|
|
24
|
+
* @property runDetails.path - The path to reproduce the failing test.
|
|
25
|
+
* @property runDetails.error - The error thrown during the test.
|
|
26
|
+
*/
|
|
27
|
+
const ansicolor_1 = require("ansicolor");
|
|
28
|
+
const shared_1 = require("./shared");
|
|
29
|
+
function reporter(
|
|
30
|
+
//@ts-ignore
|
|
31
|
+
runDetails, radio, type) {
|
|
32
|
+
var _a, _b;
|
|
33
|
+
if (runDetails.failed) {
|
|
34
|
+
// Report general run data.
|
|
35
|
+
const r = runDetails.counterexample[0];
|
|
36
|
+
radio.emit("logFailure", `\nError: Property failed after ${runDetails.numRuns} tests.`);
|
|
37
|
+
radio.emit("logFailure", `Seed : ${runDetails.seed}`);
|
|
38
|
+
if (runDetails.path) {
|
|
39
|
+
radio.emit("logFailure", `Path : ${runDetails.path}`);
|
|
40
|
+
}
|
|
41
|
+
switch (type) {
|
|
42
|
+
case "invariant": {
|
|
43
|
+
// Report specific run data for the invariant testing type.
|
|
44
|
+
radio.emit("logFailure", `\nCounterexample:`);
|
|
45
|
+
radio.emit("logFailure", `- Contract : ${(0, shared_1.getContractNameFromContractId)(r.rendezvousContractId)}`);
|
|
46
|
+
radio.emit("logFailure", `- Function : ${r.selectedFunction.name} (${r.selectedFunction.access})`);
|
|
47
|
+
radio.emit("logFailure", `- Arguments: ${JSON.stringify(r.functionArgsArb)}`);
|
|
48
|
+
radio.emit("logFailure", `- Caller : ${r.sutCaller[0]}`);
|
|
49
|
+
radio.emit("logFailure", `- Outputs : ${JSON.stringify(r.selectedFunction.outputs)}`);
|
|
50
|
+
radio.emit("logFailure", `- Invariant: ${r.selectedInvariant.name} (${r.selectedInvariant.access})`);
|
|
51
|
+
radio.emit("logFailure", `- Arguments: ${JSON.stringify(r.invariantArgsArb)}`);
|
|
52
|
+
radio.emit("logFailure", `- Caller : ${r.invariantCaller[0]}`);
|
|
53
|
+
radio.emit("logFailure", `\nWhat happened? Rendezvous went on a rampage and found a weak spot:\n`);
|
|
54
|
+
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`;
|
|
55
|
+
radio.emit("logFailure", formattedError);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "test": {
|
|
59
|
+
// Report specific run data for the property testing type.
|
|
60
|
+
radio.emit("logFailure", `\nCounterexample:`);
|
|
61
|
+
radio.emit("logFailure", `- Test Contract : ${(0, shared_1.getContractNameFromContractId)(r.testContractId)}`);
|
|
62
|
+
radio.emit("logFailure", `- Test Function : ${r.selectedTestFunction.name} (${r.selectedTestFunction.access})`);
|
|
63
|
+
radio.emit("logFailure", `- Arguments : ${JSON.stringify(r.functionArgsArb)}`);
|
|
64
|
+
radio.emit("logFailure", `- Caller : ${r.testCaller[0]}`);
|
|
65
|
+
radio.emit("logFailure", `- Outputs : ${JSON.stringify(r.selectedTestFunction.outputs)}`);
|
|
66
|
+
radio.emit("logFailure", `\nWhat happened? Rendezvous went on a rampage and found a weak spot:\n`);
|
|
67
|
+
const formattedError = `The test function "${r.selectedTestFunction.name}" returned:\n\n${(_b = runDetails.error) === null || _b === void 0 ? void 0 : _b.toString().split("\n").map((line) => " " + line).join("\n")}\n`;
|
|
68
|
+
radio.emit("logFailure", formattedError);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
radio.emit("logMessage", (0, ansicolor_1.green)(`\nOK, ${type === "invariant" ? "invariants" : "properties"} passed after ${runDetails.numRuns} runs.\n`));
|
|
75
|
+
}
|
|
76
|
+
}
|