Skip to content

Commit 4bbb1e5

Browse files
authored
Merge pull request #717 from Codex723/feat/contract-fuzzing-property-testing
feat(testing): added contract fuzzing and property-based testing
2 parents a16a53e + 7548011 commit 4bbb1e5

15 files changed

Lines changed: 578 additions & 2 deletions

File tree

.cargo-mutants.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ examine_globs = [
2626
"src/utils/crypto.rs",
2727
"src/commands/deploy.rs",
2828
"src/utils/templates.rs",
29+
# Contract testing infrastructure
30+
"src/utils/mock_soroban.rs",
31+
"src/utils/wasm_hash.rs",
32+
"src/utils/contract_mocks.rs",
33+
"src/utils/contract_testing.rs",
34+
"src/utils/test_generator.rs",
2935
]
3036

3137
# ── Skip ─────────────────────────────────────────────────────────────────────

.github/workflows/fuzzing.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ on:
77
- 'src/**'
88
- 'fuzz/**'
99
- 'tests/property_tests.rs'
10+
- 'tests/contract_property_tests.rs'
1011
- '.github/workflows/fuzzing.yml'
1112
- 'Cargo.toml'
1213
pull_request:
1314
paths:
1415
- 'src/**'
1516
- 'fuzz/**'
1617
- 'tests/property_tests.rs'
18+
- 'tests/contract_property_tests.rs'
1719
- '.github/workflows/fuzzing.yml'
1820
- 'Cargo.toml'
1921
# Allow manual dispatch with configurable fuzz duration.
@@ -66,6 +68,9 @@ jobs:
6668
- name: Run property-based tests
6769
run: cargo test --test property_tests --locked -- --test-threads=1
6870

71+
- name: Run contract property-based tests
72+
run: cargo test --test contract_property_tests --locked -- --test-threads=1
73+
6974
- name: Run all tests (includes property tests)
7075
run: cargo test --locked -- --test-threads=1
7176

@@ -122,6 +127,11 @@ jobs:
122127
- fuzz_wasm_hash
123128
- fuzz_encrypted_bundle_parse
124129
- fuzz_template_operations
130+
# Contract fuzzing harnesses
131+
- fuzz_wasm_validation
132+
- fuzz_contract_invocation
133+
- fuzz_contract_spec_parse
134+
- fuzz_test_generator
125135
steps:
126136
- uses: actions/checkout@v4
127137

FUZZING_GUIDE.md

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ in the contract crate and use the same `PROPTEST_CASES` setting as this guide.
4444
# Run property tests with default 256 cases per property.
4545
cargo test --test property_tests
4646

47+
# Run contract property tests.
48+
cargo test --test contract_property_tests
49+
4750
# Increase cases for a deeper search.
4851
PROPTEST_CASES=5000 cargo test --test property_tests
4952

@@ -63,6 +66,9 @@ cargo fuzz list --fuzz-dir fuzz
6366
# Run a specific target for 60 seconds.
6467
cargo fuzz run fuzz_validate_public_key --fuzz-dir fuzz -- -max_total_time=60
6568

69+
# Run a contract fuzz target.
70+
cargo fuzz run fuzz_wasm_validation --fuzz-dir fuzz -- -max_total_time=60
71+
6672
# Run with a size cap (good for initial exploration).
6773
cargo fuzz run fuzz_passphrase_strength --fuzz-dir fuzz \
6874
-- -max_total_time=120 -max_len=1024
@@ -120,6 +126,7 @@ Additional property suites live in their own files:
120126
|---|---|---|
121127
| [`tests/config_property_tests.rs`](tests/config_property_tests.rs) | Config round trips | TOML/JSON serialization preserves every value; merging is identity-on-empty, idempotent, and overlay-wins; malformed combinations (unknown network, duplicate wallet, non-HTTP endpoint, unknown overlay key) are rejected |
122128
| [`tests/wallet_import_property_tests.rs`](tests/wallet_import_property_tests.rs) | Wallet import/backup | The same invariants the wallet fuzz targets assert, run on stable in every `cargo test` sweep |
129+
| [`tests/contract_property_tests.rs`](tests/contract_property_tests.rs) | Contract testing infrastructure | WASM validation, WASM hash computation, mock contract invocation, mock storage, mock addresses, and mock environment invariants |
123130

124131
### Writing new properties
125132

@@ -164,6 +171,10 @@ macro that receives raw bytes and should **never panic** regardless of input.
164171
| `fuzz_wallet_backup_parse` | Wallet backup documents: malformed JSON, truncated files, invalid StrKeys, oversized inputs, Unicode names |
165172
| `fuzz_wallet_import_envelope` | Encrypted backup envelopes: base64 fields, salt/nonce lengths, truncated ciphertext, KDF parameters, plaintext/encrypted classification |
166173
| `fuzz_wallet_backup_structured` | Near-valid backup documents built with `arbitrary::Arbitrary`, to reach the semantic checks the byte-level harness rarely hits |
174+
| `fuzz_wasm_validation` | WASM binary validation: magic header, minimum size, panic-freedom |
175+
| `fuzz_contract_invocation` | Mock contract client invocation: call counting, return/error determinism, reset |
176+
| `fuzz_contract_spec_parse` | Contract test spec JSON parsing: malformed JSON, truncated docs, hostile Unicode |
177+
| `fuzz_test_generator` | Test case generation from source: malformed Rust, empty files, arbitrary fragments |
167178

168179
### Wallet import & backup harnesses
169180

@@ -203,6 +214,46 @@ The invariants asserted by the harnesses:
203214
- **No misclassification** — a JSON document is never treated as an encrypted
204215
bundle, which would prompt for a passphrase that does not exist.
205216

217+
### Contract fuzzing harnesses
218+
219+
The contract fuzzing harnesses target the Soroban contract testing infrastructure
220+
in StarForge. These harnesses exercise the mock Soroban environment, WASM
221+
validation, contract spec parsing, and test case generation — all of which
222+
process inputs that could come from untrusted contract source files or test
223+
specifications.
224+
225+
```bash
226+
# WASM validation: magic header, minimum size, panic-freedom.
227+
cargo fuzz run fuzz_wasm_validation --fuzz-dir fuzz -- -max_total_time=60
228+
229+
# Mock contract invocation: call counting, return/error determinism.
230+
cargo fuzz run fuzz_contract_invocation --fuzz-dir fuzz -- -max_total_time=60
231+
232+
# Contract test spec JSON parsing: malformed JSON, truncated docs.
233+
cargo fuzz run fuzz_contract_spec_parse --fuzz-dir fuzz -- -max_total_time=60
234+
235+
# Test case generation from source: malformed Rust, empty files.
236+
cargo fuzz run fuzz_test_generator --fuzz-dir fuzz -- -max_total_time=60
237+
```
238+
239+
Seed corpora ship under `fuzz/corpus/fuzz_wasm_validation/`,
240+
`fuzz/corpus/fuzz_contract_spec_parse/`, and
241+
`fuzz/corpus/fuzz_test_generator/`, covering valid WASM binaries, valid
242+
contract test specs, and valid Rust source fragments respectively.
243+
244+
The invariants asserted by the contract harnesses:
245+
246+
- **Totality** — every input is handled gracefully; nothing panics.
247+
- **WASM validation** — inputs shorter than 8 bytes or without the `\0asm`
248+
magic header are rejected; valid headers with sufficient length are accepted.
249+
- **Mock invocation** — call counts always match the number of invocations;
250+
pre-configured return values and errors are returned deterministically;
251+
errors take priority over return values; reset clears all state.
252+
- **Spec parsing** — malformed JSON, truncated documents, and hostile Unicode
253+
produce errors, never panics.
254+
- **Test generation** — malformed Rust source, empty files, and arbitrary
255+
fragments produce errors or empty results, never panics.
256+
206257
### Running a target
207258

208259
```bash
@@ -313,7 +364,7 @@ The fuzzing CI pipeline is defined in
313364

314365
| Job | Trigger | What it does |
315366
|---|---|---|
316-
| `property-tests` | Every push / PR | Runs `cargo test --test property_tests` with 2 000 cases |
367+
| `property-tests` | Every push / PR | Runs `cargo test --test property_tests` and `cargo test --test contract_property_tests` with 2 000 cases |
317368
| `fuzz-build` | Every push / PR | Compiles all fuzz targets (catches compilation errors) |
318369
| `fuzz-smoke` | Every push / PR | 30-second smoke run per target in a matrix |
319370
| `coverage` | Every push / PR | Generates LCOV + JSON; uploads to Codecov |
@@ -335,7 +386,7 @@ both systems:
335386
### Property test
336387

337388
```rust
338-
// In tests/property_tests.rs
389+
// In tests/contract_property_tests.rs
339390
proptest! {
340391
#[test]
341392
fn prop_my_contract_validates_input(amount in valid_amount_string()) {
@@ -377,3 +428,8 @@ because they process untrusted external input or handle cryptographic material:
377428
| `check_passphrase_strength` | `utils/crypto.rs` | zxcvbn integration, minimum length gate |
378429
| `compute_local_wasm_hash` | `commands/deploy.rs` | On-chain hash consistency |
379430
| `validate_contract_id` | `utils/config.rs` | Contract address validation |
431+
| `validate_wasm` | `utils/mock_soroban.rs` | WASM binary validation, magic header checks |
432+
| `compute_wasm_hash` | `utils/wasm_hash.rs` | WASM hash computation, environment validation |
433+
| `MockContractClient::invoke` | `utils/contract_mocks.rs` | Mock contract invocation, call logging |
434+
| `load_contract_test_spec` | `utils/contract_testing.rs` | Contract test spec parsing (JSON/TOML) |
435+
| `generate_from_source` | `utils/test_generator.rs` | Test case generation from Rust source |

fuzz/Cargo.toml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ hex = "0.4"
2525
# JSON assembly for the structured wallet-backup harness.
2626
serde_json = "1.0"
2727

28+
# Temp-file creation for harnesses that write fuzzer input to disk.
29+
tempfile = "3.8"
30+
2831
# ── Fuzz targets ─────────────────────────────────────────────────────────────
2932
# Each [[bin]] entry corresponds to a file under fuzz/fuzz_targets/.
3033
# Run a specific target with:
@@ -101,3 +104,31 @@ name = "fuzz_wallet_backup_structured"
101104
path = "fuzz_targets/fuzz_wallet_backup_structured.rs"
102105
test = false
103106
doc = false
107+
108+
# ── Contract fuzzing harnesses ───────────────────────────────────────────────
109+
# These harnesses target Soroban contract testing infrastructure: WASM
110+
# validation, mock contract invocation, spec parsing, and test generation.
111+
112+
[[bin]]
113+
name = "fuzz_wasm_validation"
114+
path = "fuzz_targets/fuzz_wasm_validation.rs"
115+
test = false
116+
doc = false
117+
118+
[[bin]]
119+
name = "fuzz_contract_invocation"
120+
path = "fuzz_targets/fuzz_contract_invocation.rs"
121+
test = false
122+
doc = false
123+
124+
[[bin]]
125+
name = "fuzz_contract_spec_parse"
126+
path = "fuzz_targets/fuzz_contract_spec_parse.rs"
127+
test = false
128+
doc = false
129+
130+
[[bin]]
131+
name = "fuzz_test_generator"
132+
path = "fuzz_targets/fuzz_test_generator.rs"
133+
test = false
134+
doc = false
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"contract_id":"CA3C5F6A9B2D1E0F4C7A8B3D5E6F1A2C3B4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F","environment":"testnet","test_cases":[{"name":"test_transfer","description":"Test basic transfer","inputs":{"from":"GABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","to":"GABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","amount":"100"},"expected":{"success":true}]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"contract_id":"CA3C5F6A9B2D1E0F4C7A8B3D5E6F1A2C3B4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F","environment":"testnet","test_cases":[{"name":"test_transfer","description":"Test basic transfer","inputs":{"from":"GABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","to":"GABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789","amount":"100"},"expected":{"success":true}}]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// empty source file for fuzzing
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#[test]
2+
fn test_transfer() {
3+
let result = contract.transfer("alice", "bob", 100);
4+
assert!(result.is_ok());
5+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
\0asm\x01\x00\x00\x00\x01\x07\x01\x03\x65\x6e\x76\x00\x02\x00\x00

0 commit comments

Comments
 (0)