Skip to content

Feat/2026 hardening 690 694 696 697 - #703

Merged
Nanle-code merged 4 commits into
Nanle-code:masterfrom
nonso7:feat/2026-hardening-690-694-696-697
Jul 29, 2026
Merged

Feat/2026 hardening 690 694 696 697#703
Nanle-code merged 4 commits into
Nanle-code:masterfrom
nonso7:feat/2026-hardening-690-694-696-697

Conversation

@nonso7

@nonso7 nonso7 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Description

Brief description of what this PR does and why.

Closes #(issue number)

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Changes Made

  • Change 1
  • Change 2
  • Change 3

Testing

How has this been tested?

Describe the tests you ran and how to reproduce them.

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed

Test Coverage

Describe what scenarios have been tested:

  • Happy path:
  • Edge cases:
  • Error handling:

Code Quality Checklist

  • My code follows the style guidelines of this project (cargo fmt)
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings (cargo clippy -- -D warnings)
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • The CI checks pass (format, clippy, tests)

Breaking Changes

  • This PR introduces breaking changes

If checked, describe the breaking changes and migration path:

Documentation

  • README.md updated
  • DEVELOPER_GUIDE.md updated (if applicable)
  • API_REFERENCE.md updated (if applicable)
  • No documentation changes needed

Screenshots (if applicable)

Add screenshots or GIFs for UI changes.

Additional Context

Add any other context about the PR here.


Note: Make sure all tests pass locally before submitting:

cargo test
cargo fmt --all
cargo clippy -- -D warnings

Closes #690
Closes #694
Closes #696
Closes #697

nonso7 added 4 commits July 29, 2026 15:06
Soroban charges for what a transaction actually does, and simulateTransaction
is the only source of those numbers. StarForge now parses the full resource
accounting out of a simulation response and reports it wherever a fee is shown.

New `utils::simulation_resources`:
- parses CPU instructions, linear memory, ledger footprint (decoded from the
  `transactionData` XDR), minimum resource fee, and the restore preamble;
- derives a submittable fee = resource + restore + margin + inclusion, with
  overflow-checked arithmetic and a validated 0-1000% margin range;
- accepts numeric fields as JSON numbers or strings (stellar-rpc emits u64 as
  strings) and rejects negative, fractional, and non-numeric values instead of
  coercing them to zero;
- caps input at 8 MiB before the JSON parser runs and decodes XDR with an
  explicit depth limit.

Wiring:
- `starforge simulate resources` reports the numbers from a saved response
  (--file) or a live simulation (--contract/--function), with --margin,
  --inclusion-fee, and --json;
- `starforge cost resources` prices a simulation and checks it against the
  configured budgets, with --enforce to gate CI;
- `starforge deploy --simulate/--dry-run` prints CPU, memory, footprint, and a
  recommended fee inline.

Behaviour change: SimulationResult::fee previously reported `cost.cpuInsns`,
an instruction count rather than a fee. It now reports `minResourceFee`,
falling back to 100000 stroops only when the server reports no resource
accounting at all. The struct gained an optional `resources` field, defaulted
for serde so older serialised values still deserialise.

Compatibility, security, and migration notes: docs/SIMULATION_RESOURCES.md.
Tests: 19 unit tests in the new module plus 3 in utils::soroban covering the
primary flow, the margin/overflow/zero-fee boundaries, and the malformed,
host-error, transport-error, and unsupported-endpoint failure paths.
Adds tests/config_property_tests.rs, which generates hundreds of internally
consistent configurations and asserts two families of properties:

- preservation: TOML and JSON round trips are exact, crossing formats is
  stable, serialization is deterministic, merging an empty overlay is the
  identity, merging is idempotent, and a merged config still round trips;
- rejection: malformed *combinations* are refused rather than repaired - an
  active network that is not configured, a wallet on an unknown network, a
  duplicate wallet name, a non-HTTP endpoint, a malformed StrKey, an overlay
  that collides with a stored wallet, and an unknown overlay key.

Supporting changes to utils::config:
- pure parse/serialize helpers (parse_config_str, parse_config_json,
  to_toml_string, to_json_string) so round trips are testable without disk;
- ConfigOverlay + merge_configs, with documented precedence: scalars are
  overlay-wins, feature_flags and plugin_trust are replaced wholesale so a
  half-merged policy is impossible, networks merge by key, wallets append and
  reject name collisions, and version/install_id always come from the base.
  The overlay uses deny_unknown_fields so `netwrok = "mainnet"` is a hard
  error rather than a silently ignored typo;
- Config fields reordered so all scalars precede tables, which is what TOML
  serialization requires; deserialization is by key so existing files load
  unchanged;
- PartialEq derives on Config and friends so round-trip equality is assertable;
- validate_config now rejects duplicate wallet names.

Fixes a purity bug: validate_network_exists fell through to validate_network,
which calls load() - so validating an in-memory Config opened, migrated, and
wrote the user's real database, and the same value could validate differently
on two machines. It now consults only the supplied Config and the built-in
network names. Migration note in docs/CONFIGURATION.md.

Also ignores *.proptest-regressions, which are regenerated shrink artifacts.
Every invocation now carries exactly one correlation ID, attached to a root
command span entered in main(). Retries, network requests, plugin calls, and
deployment steps all log underneath it, so a single invocation can be
reconstructed from an aggregated log stream even when several StarForge
processes run concurrently in the same pipeline.

New `utils::correlation`:
- resolve() with precedence --correlation-id > $STARFORGE_CORRELATION_ID >
  generated (UUIDv4). An explicitly supplied value that fails validation is
  fatal (exit 2) rather than a silent fallback, because generating a different
  ID would break exactly the log join the caller asked for;
- validation: 8-64 characters of [A-Za-z0-9_-], trimmed, with a multi-byte
  character reported as an invalid character rather than a length problem;
- span helpers for the four correlated activities: retry_span, network_span,
  plugin_span, deploy_step_span, each tagged with a `kind` field;
- init() is first-call-wins, so a re-entrant context (the REPL, a plugin host)
  cannot fragment one invocation across several IDs.

Not leaking secrets is the point of the exercise:
- a supplied ID that looks like key material (a Stellar S... seed, an encrypted
  salt:nonce:ciphertext bundle, a long mixed-case base64 blob) is rejected
  outright, since the ID lands on every log line;
- span attributes go through sanitize(): secret-looking values become
  [REDACTED], control characters are replaced so a crafted value cannot forge
  an extra log record, and values are truncated at 120 characters;
- URLs go through redact_url(), which keeps scheme, host, and path and drops
  the query string and any user:password@ userinfo.

JSON logging now emits the current span and the full span list, so nested
events carry the ID too. The human formatter already prints span context.

Tests: 18 unit tests plus tests/correlation_logging.rs, which drives a real
tracing subscriber and inspects the emitted JSON - asserting that every nested
event shares one ID, that each span kind appears, that an event outside any
span still logs cleanly, and that a secret key, basic-auth credentials, and an
API key in a query string never reach the log.

Docs: docs/CORRELATION_IDS.md.
`wallet import --file` and `backup restore` read files that came from outside
the tool, so their parsing is a trust boundary. It now lives in
utils::wallet_import - separated from prompting, disk access, and the config
store - where it can be unit-tested, property-tested, and fuzzed.

Enforced on untrusted input:
- size gates run before the JSON parser (4 MiB per document, 8 MiB per
  envelope, 1000 wallets, 64-character names), so an oversized file cannot
  drive a large allocation;
- backup documents must declare version "1", carry at least one wallet, use
  unique names, and hold well-formed StrKeys;
- encrypted envelopes must have 3, 5, or 6 base64 parts, a 16-byte salt, a
  12-byte nonce, a ciphertext of at least one AES-GCM tag, and non-zero KDF
  parameters - all checked before the passphrase prompt, so a corrupt file
  fails fast instead of after an Argon2 derivation;
- wallet names carrying control, bidi, or zero-width characters are rejected;
  non-ASCII names are accepted with a homograph warning rather than refused,
  since earlier releases allowed any Unicode alphanumeric;
- rejections quote the wallet name and reason, never the key material.

Fixes two classification bugs. Encryption detection was
`raw.matches(':').count() == 2`, which only recognises the 3-part bundle: a
backup encrypted with custom Argon2 parameters has 5 or 6 parts and was handed
to the JSON parser, failing with a misleading "Invalid backup JSON format".
And a JSON document containing exactly two colons could trigger a passphrase
prompt for a passphrase that does not exist. Classification now follows the
bundle grammar and never treats a document starting with { or [ as a bundle.

Three fuzz harnesses drive the parsers: fuzz_wallet_backup_parse (bytes),
fuzz_wallet_import_envelope (envelope structure and classification), and
fuzz_wallet_backup_structured (near-valid documents via arbitrary::Arbitrary,
to reach the semantic checks the byte-level harness rarely hits). Each asserts
totality plus the invariants any accepted value must satisfy. Ships with a
dictionary and 13 seed corpus entries covering valid, watch-only, empty,
bad-version, truncated, bidi-name, 3/5/6-part, truncated-ciphertext, and
non-base64 inputs.

cargo fuzz needs nightly, so tests/wallet_import_property_tests.rs asserts the
same invariants with proptest on stable: 16 tests over the primary flow, the
exact ciphertext/salt/nonce/name-length boundaries, and the failure paths
(malformed JSON, malformed StrKeys, deceptive names, oversized input, never
panicking on arbitrary or lossy-UTF-8 bytes, never echoing a secret).

Docs: docs/WALLET_IMPORT_SECURITY.md, plus FUZZING_GUIDE.md, README.md, and
docs/COMMAND_REFERENCE.md updated for all four roadmap issues.
@drips-wave

drips-wave Bot commented Jul 29, 2026

Copy link
Copy Markdown

@nonso7 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Nanle-code
Nanle-code merged commit 0067b47 into Nanle-code:master Jul 29, 2026
2 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants