Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,6 @@ docker-compose.override.yml

# Soroban test snapshots emitted by soroban-sdk testutils during `cargo test`
test_snapshots/

# proptest shrink artifacts (regenerated on demand; not source)
*.proptest-regressions
48 changes: 48 additions & 0 deletions FUZZING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ File: [`tests/property_tests.rs`](tests/property_tests.rs)
| Structural invariants | Validated keys satisfy structural constraints independently (cross-check) |
| `KdfOptions` | All-None is `is_default()`; any Some value is not |

Additional property suites live in their own files:

| File | Property group | What it tests |
|---|---|---|
| [`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 |
| [`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 |

### Writing new properties

Properties follow this pattern:
Expand Down Expand Up @@ -154,6 +161,47 @@ macro that receives raw bytes and should **never panic** regardless of input.
| `fuzz_wasm_hash` | SHA-256 WASM hash; determinism and format checks |
| `fuzz_encrypted_bundle_parse` | Encrypted bundle parser via validate_secret_key |
| `fuzz_template_operations` | Structured template inputs via `arbitrary::Arbitrary` |
| `fuzz_wallet_backup_parse` | Wallet backup documents: malformed JSON, truncated files, invalid StrKeys, oversized inputs, Unicode names |
| `fuzz_wallet_import_envelope` | Encrypted backup envelopes: base64 fields, salt/nonce lengths, truncated ciphertext, KDF parameters, plaintext/encrypted classification |
| `fuzz_wallet_backup_structured` | Near-valid backup documents built with `arbitrary::Arbitrary`, to reach the semantic checks the byte-level harness rarely hits |

### Wallet import & backup harnesses

`starforge wallet import --file` and `starforge backup restore` read files that
came from outside the tool, so their parsers are a trust boundary. All three
harnesses drive [`src/utils/wallet_import.rs`](src/utils/wallet_import.rs),
which is deliberately free of prompting, disk access, and config writes.

```bash
# Byte-level: malformed JSON, truncated documents, byte soup.
cargo fuzz run fuzz_wallet_backup_parse -- -dict=fuzz/dicts/wallet_backup.dict

# Envelope: base64 fields, truncated ciphertext, bad KDF parameters.
cargo fuzz run fuzz_wallet_import_envelope -- -dict=fuzz/dicts/wallet_backup.dict

# Structured: near-valid documents that reach the semantic checks.
cargo fuzz run fuzz_wallet_backup_structured
```

Seed corpora ship under `fuzz/corpus/fuzz_wallet_backup_parse/` and
`fuzz/corpus/fuzz_wallet_import_envelope/`, covering a valid backup, a
watch-only backup, an empty wallet list, an unsupported version, a truncated
document, a name carrying a right-to-left override, 3/5/6-part envelopes, a
truncated ciphertext, and a non-base64 envelope.

The invariants asserted by the harnesses:

- **Totality** — every input returns a `WalletImportError`; nothing panics.
- **Size first** — the size limit is checked before the JSON parser runs, so an
oversized file cannot drive a large allocation.
- **Accepted implies valid** — an accepted backup has version `1`, at least one
wallet, no duplicate names, no control characters in a name, and a 56-character
`G…` public key on every entry.
- **Envelope shape** — an accepted envelope has a 16-byte salt, a 12-byte nonce,
a ciphertext of at least 16 bytes (one AES-GCM tag), and non-zero KDF
parameters.
- **No misclassification** — a JSON document is never treated as an encrypted
bundle, which would prompt for a passphrase that does not exist.

### Running a target

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,11 @@ StarForge has comprehensive documentation covering all aspects of the project:
### ?? Feature Documentation
- **[TEMPLATE_MARKETPLACE.md](TEMPLATE_MARKETPLACE.md)** - Template marketplace feature
- **[QUICK_START_TEMPLATES.md](QUICK_START_TEMPLATES.md)** - Template quick start guide
- **[docs/SIMULATION_RESOURCES.md](docs/SIMULATION_RESOURCES.md)** - CPU, memory, footprint, and minimum resource fees from simulation
- **[docs/CORRELATION_IDS.md](docs/CORRELATION_IDS.md)** - Correlating structured logs across one invocation
- **[docs/CONFIGURATION.md](docs/CONFIGURATION.md)** - Config parsing, overlay merging, and validation rules
- **[docs/WALLET_IMPORT_SECURITY.md](docs/WALLET_IMPORT_SECURITY.md)** - Limits enforced on untrusted wallet backups
- **[FUZZING_GUIDE.md](FUZZING_GUIDE.md)** - Property-based tests, fuzz targets, mutation testing

### ?? Navigation
- **[DOCUMENTATION_INDEX.md](DOCUMENTATION_INDEX.md)** - Complete documentation index
Expand Down
34 changes: 34 additions & 0 deletions docs/COMMAND_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Browse every top-level command and its most important flags. For wallet, templat
| `-q, --quiet` | Suppress banner and decorative output |
| `--log-format human\|json` | Structured log format (default: `human`) |
| `--log-dir <PATH>` | Optional rotating log directory |
| `--correlation-id <ID>` | Tie every log line of this invocation together (8–64 chars of `[A-Za-z0-9_-]`); defaults to `$STARFORGE_CORRELATION_ID` or a generated value — see [CORRELATION_IDS.md](CORRELATION_IDS.md) |
| `-h, --help` | Command help |
| `-V, --version` | CLI version |

Expand Down Expand Up @@ -50,6 +51,10 @@ starforge tutorial next
| `sign` | Sign a payload with a saved wallet |
| `multisig` | Multisig helpers (create, add-signer, submit) |

`import --file` accepts a plaintext backup JSON or an encrypted bundle, detected
automatically. See [WALLET_IMPORT_SECURITY.md](WALLET_IMPORT_SECURITY.md) for the
limits enforced on untrusted backup files.

---

## `multisig`
Expand Down Expand Up @@ -96,6 +101,10 @@ starforge multisig notify proposal.json --message "Please sign the treasury paym

**`deploy` flags:** `--network`, `--wallet`, `--optimize`, `--simulate`, `--yes`, `--execute`

`--simulate` and `--dry-run` print the simulated CPU, memory, and ledger
footprint alongside the minimum resource fee and a recommended fee that
includes a safety margin. See [SIMULATION_RESOURCES.md](SIMULATION_RESOURCES.md).

```bash
starforge deploy --wasm target/wasm32v1-none/release/token.wasm \
--wallet deployer --network testnet --simulate
Expand Down Expand Up @@ -192,6 +201,27 @@ Coverage analysis tracks Soroban contract functions, line spans, branch paths, u

---

## `simulate` / `cost` — resource fees

| Command | Purpose |
|---------|---------|
| `simulate resources --file <JSON>` | Report CPU, memory, footprint, and minimum resource fee from a saved `simulateTransaction` response |
| `simulate resources --contract <ID> --function <NAME>` | The same, simulated live against Soroban RPC |
| `cost resources --file <JSON>` | Price a simulation and check it against configured budgets (`--enforce` to gate CI) |

Shared flags: `--margin <PERCENT>` (default `20`), `--inclusion-fee <STROOPS>`
(default `100`). `simulate resources` also takes `--json`.

```bash
starforge simulate resources --file simulation.json --json
starforge simulate resources --contract CCPYZ... --function balance --network testnet
starforge cost resources --file simulation.json --network mainnet --enforce
```

Full reference: [SIMULATION_RESOURCES.md](SIMULATION_RESOURCES.md).

---

## `advanced-perf`

| Subcommand | Purpose |
Expand Down Expand Up @@ -385,3 +415,7 @@ starforge my-plugin <args>

- [API_REFERENCE.md](../API_REFERENCE.md) — detailed per-command examples and output samples
- [DEVELOPER_GUIDE.md](../DEVELOPER_GUIDE.md) — contributing and local development
- [SIMULATION_RESOURCES.md](SIMULATION_RESOURCES.md) — CPU, memory, footprint, and resource fees
- [CORRELATION_IDS.md](CORRELATION_IDS.md) — correlating structured logs across an invocation
- [CONFIGURATION.md](CONFIGURATION.md) — config parsing, overlays, and validation rules
- [WALLET_IMPORT_SECURITY.md](WALLET_IMPORT_SECURITY.md) — limits on untrusted wallet backups
162 changes: 162 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Configuration: Parsing, Merging, and Serialization

StarForge's configuration lives in `~/.config/starforge/` (a local SQLite store,
with `config.toml` still read for backwards compatibility). This document
covers the parts a developer or CI author needs: the pure API, the overlay
merge rules, and what makes a configuration invalid.

---

## Pure API

Everything in this list is a pure function — no filesystem, no database, no
environment. That is what makes configuration handling testable and what keeps
`cargo test` from mutating the developer's real config.

| Function | Purpose |
|---|---|
| `config::parse_config_str(&str)` | Parse a configuration from TOML |
| `config::parse_config_json(&str)` | Parse a configuration from JSON |
| `config::to_toml_string(&Config)` | Serialize to TOML |
| `config::to_json_string(&Config)` | Serialize to JSON |
| `config::parse_overlay_str(&str)` | Parse a partial overlay from TOML |
| `config::merge_configs(base, overlay)` | Layer an overlay onto a base and validate |
| `config::validate_config(&Config)` | Validate a whole configuration |
| `config::validate_network_exists(&Config, &str)` | Check a network reference against *that* config |

Round trips are exact in both formats: parsing what was serialized yields an
equal `Config`, and crossing formats (TOML → JSON → TOML) is stable. This is
enforced by [`tests/config_property_tests.rs`](../tests/config_property_tests.rs)
over hundreds of generated configurations.

> **Field order matters in `Config`.** TOML requires a table's scalar values to
> be emitted before its sub-tables. All scalars are declared first, then tables,
> then arrays of tables. Moving a scalar below a table makes `to_toml_string`
> fail at runtime. Deserialization is by key, so the order can change without
> breaking existing files.

---

## Overlays

A `ConfigOverlay` is a partial configuration layered on top of a base — for a
project-local file, a CI environment, or a named profile.

```toml
# overlay.toml
network = "mainnet"
telemetry_enabled = false

[networks.staging]
horizon_url = "https://horizon-staging.example.com"
soroban_rpc_url = "https://rpc-staging.example.com"
```

### Precedence

| Field | Rule |
|---|---|
| `network`, `telemetry_enabled`, `wallet_encryption` | Overlay wins when set; base kept otherwise |
| `feature_flags`, `plugin_trust`, `ai_telemetry` | Replaced **wholesale** when present |
| `networks` | Merged by key; an overlay entry replaces the base entry of that name |
| `wallets` | Appended; a duplicate name is an error |
| `version`, `install_id` | Always from the base — an overlay may not forge either |

`feature_flags`, `plugin_trust`, and `ai_telemetry` are replaced rather than
field-merged on purpose: a partially specified trust policy that silently
inherits half of the base allowlist is a security footgun.

Wallets are appended rather than overwritten because they hold key material. An
overlay whose wallet name collides with the base is rejected:

```
Overlay wallet 'deployer' already exists in the base configuration; rename it
or remove it from the overlay
```

### Properties

Guaranteed, and tested over generated inputs:

- **Identity** — merging an empty overlay changes nothing.
- **Idempotence** — applying the same overlay twice adds nothing the first
application did not already add.
- **Validation** — `merge_configs` validates the result, so a merge can never
produce a configuration that `save` would reject.

### Unknown keys are rejected

`ConfigOverlay` uses `deny_unknown_fields`. A typo is a hard error:

```
$ starforge ... # with `netwrok = "mainnet"` in the overlay
Failed to parse configuration overlay TOML: unknown field `netwrok`
```

A silently ignored `netwrok` key would leave a deploy pointed at the wrong
network. The main `Config` parser stays lenient about unknown keys so a file
written by a newer StarForge still loads.

---

## What makes a configuration invalid

`validate_config` rejects these *combinations* — each value may be well-formed
on its own:

| Rejected | Why |
|---|---|
| Empty `version` | Schema version is required for migration |
| Empty or whitespace `network` | No active network |
| Active network not in `networks` and not built in | Dangling reference |
| Empty `networks` map | Nothing to connect to |
| A non-`http(s)` endpoint URL | Only HTTP(S) endpoints are supported |
| A wallet on an unknown network | Dangling reference |
| An invalid wallet name, public key, or secret key | Malformed entry |
| Two wallets with the same name | Ambiguous reference |
| An invalid plugin trust source | Malformed allowlist entry |

Built-in networks (`testnet`, `mainnet`, `docker-testnet`) always resolve, even
if they are absent from the `networks` map.

---

## Migration note

`validate_network_exists` used to fall back to loading the on-disk
configuration when a network was missing from the `Config` it was handed. That
made validation depend on — and, through `load()`, *write to* — global state:
the same in-memory `Config` could validate differently on two machines, and
validating a value in a test opened and migrated the developer's real database.

It is now pure: it consults the supplied `Config` and the built-in names, and
nothing else.

**If you relied on the old behaviour** (calling `validate_network_exists` with a
partially populated `Config` and expecting it to consult the saved config),
load the configuration explicitly first and pass that in. `validate_network`,
which does read from disk by design, is unchanged.

`validate_config` also now rejects duplicate wallet names. A configuration that
already contains duplicates will fail to save until one is renamed — previously
the duplicate silently shadowed the other on lookup.

---

## Security

- Wallet secrets in a configuration are stored either as plaintext StrKeys or
as encrypted bundles; `validate_config` accepts both shapes but never logs
either. Error messages quote the wallet name, not the key.
- An overlay cannot replace an existing wallet, so a hostile overlay file
cannot swap out a deployer key.
- An overlay cannot set `install_id`, which is used for deterministic
feature-flag bucketing.

---

## See also

- [docs/COMMAND_REFERENCE.md](COMMAND_REFERENCE.md) — the `config` command
- [FUZZING_GUIDE.md](../FUZZING_GUIDE.md) — running the property suites
- [tests/config_property_tests.rs](../tests/config_property_tests.rs)
Loading
Loading