Motivation
Our mutation-testing gate uses cargo-mutants, which ships a fixed, curated set of structural operators (replace function body with a default, flip binary/relational/logical/bit operators, a few literals). It has no plugin API, so it is structurally blind to the mutations that matter most for a state-commitment system — endianness, field-element chunk widths, codec configuration, canonicalization, and the trie's level/bit-width arithmetic.
universalmutator is regex-rule based (regex ==> replacement), language-agnostic, and auto-discards mutants that don't compile or compile identically. That makes it a good vehicle for a complementary, domain-specific mutation layer: cargo-mutants guards generic code structure; universalmutator guards SALT semantics.
This issue catalogs candidate domain operators to prototype. The selection filter throughout: "what soundness or determinism invariant would a surviving mutant here violate?" We deliberately skip operators that overlap with cargo-mutants (generic arithmetic/relational/logical), and skip the resize load-factor / growth-multiplier (configurable by design, not consensus-critical).
A. Trie-structure operators (highest leverage)
The SALT trie's correctness reduces to "right level-base and right bit-width?" — pure integer arithmetic over a STARTING_NODE_ID[level] table and three distinct bit-widths (TRIE_WIDTH_BITS = 8, MIN_BUCKET_SIZE_BITS = 8, BUCKET_SLOT_BITS = 40). A wrong constant produces a plausible, compiling, wrong structure — exactly what mutation testing exists to catch. See salt/src/trie/node_utils.rs and salt/src/constant.rs.
A1. Level-index off-by-one (the base table) — the sharpest family:
STARTING_NODE_ID\[level \+ 1\] ==> STARTING_NODE_ID[level]
STARTING_NODE_ID\[level - 1\] ==> STARTING_NODE_ID[level]
MAIN_TRIE_LEVELS - 1 ==> MAIN_TRIE_LEVELS
MAX_SUBTREE_LEVELS - 1 ==> MAX_SUBTREE_LEVELS - 2
Expected kills: test_get_child_node, test_get_parent_node (inverse relationship), test_bucket_root_node_id, test_subtree_leaf_for_key.
A2. Bit-width constant confusion (the 24/40/8 split) — swap one named width for another real one; invisible to any generic tool:
<< BUCKET_SLOT_BITS ==> << BUCKET_ID_BITS
>> MIN_BUCKET_SIZE_BITS ==> >> BUCKET_SLOT_BITS
<< BUCKET_SLOT_BITS ==> << (BUCKET_SLOT_BITS - 1)
Expected kills: test_subtree_leaf_for_key, test_vc_position_in_parent (max-bucket case).
A3. Fanout shift direction / amount (the << 8 / >> 8 that is the 256-way branching):
\(parent_relative_position << TRIE_WIDTH_BITS\) ==> (parent_relative_position >> TRIE_WIDTH_BITS)
relative_position >> TRIE_WIDTH_BITS ==> relative_position << TRIE_WIDTH_BITS
<< TRIE_WIDTH_BITS ==> << (TRIE_WIDTH_BITS + 1)
Expected kills: get_child_node/get_parent_node inverse test.
A4. Segment granularity (256 slots per leaf):
>> MIN_BUCKET_SIZE_BITS ==> >> (MIN_BUCKET_SIZE_BITS + 1)
% TRIE_WIDTH ==> % (TRIE_WIDTH - 1)
Expected kills: test_subtree_leaf_for_key_segment_boundaries, test_subtree_leaf_start_key, test_vc_position_in_parent.
A5. Ceil-division rounding in the subtree-root climb — subtree_root_level does (capacity + MIN_BUCKET_SIZE - 1) >> BITS; dropping the - 1 turns ceil into floor, wrong at exact powers of 256. The subtlest one, and a perfect target because the rounding correction is invisible to the eye:
\(capacity \+ MIN_BUCKET_SIZE as u64 - 1\) ==> (capacity + MIN_BUCKET_SIZE as u64)
capacity > MIN_BUCKET_SIZE as u64 ==> capacity >= MIN_BUCKET_SIZE as u64
level -= 1 ==> level += 1
Expected kills: test_subtree_root_level (257→3, 65536→3, 65537→2 boundary cases).
Validation loop: get_child_node/get_parent_node are tested as exact inverses over a parent × child-index matrix, so most A1–A4 mutants should die there. Any survivor points to a coverage gap in that inverse test.
B. Endianness / byte-order
Explicit byte packing in the hasher and codecs; wrong endianness silently breaks determinism and cross-platform reproducibility, and no generic operator flips it.
to_le_bytes ==> to_be_bytes
to_be_bytes ==> to_le_bytes
from_le_bytes ==> from_be_bytes
from_be_bytes ==> from_le_bytes
Expected kills: salt/src/state/hasher.rs pinned-output tests, salt/tests/proof_fixture_codec.rs.
C. Field-element / chunk width (the 31-vs-32 hazard)
Classic commitment bug: a value is 32 bytes but a field element only safely holds 31. Scope the bare 31/32 rules to specific files.
\.chunks\(32\) ==> .chunks(31)
\.chunks\(31\) ==> .chunks(32)
>> 8 ==> >> 4
& 0xff ==> & 0x7f
Expected kills: trie chunking / scalar-encoding tests.
D. Serialization codec config
The legacy() codec choice is the wire format; swapping it should break the byte-for-byte fixture test.
\.legacy\(\) ==> .standard()
with_fixint_encoding ==> with_varint_encoding
with_little_endian ==> with_big_endian
Expected kills: salt/tests/proof_fixture_codec.rs.
E. Canonicalization removal (sort / dedup)
The proof path sorts/dedups to reach canonical form. Deleting those calls is a soundness mutation (duplicates / unordered elements should be rejected or change the root). Line deletion is not in cargo-mutants' repertoire.
^(\s*).*\.sort(_unstable)?\(\); ==> \1
^(\s*).*\.dedup(_by)?\([^)]*\); ==> \1
sort_by\(([^)]*)a\.cmp\(&?b\)([^)]*)\) ==> sort_by(\1b.cmp(&a)\2)
Expected kills: salt/src/proof/prover.rs sort/dedup canonicalization tests.
F. Hash-diffusion ops
Rotates / xors in hash mixing, which cargo-mutants won't touch. Keep xor→or file-scoped to the hasher.
rotate_left ==> rotate_right
rotate_right ==> rotate_left
wrapping_add ==> wrapping_sub
\^ ==> |
Expected kills: salt/src/state/ahash/fallback.rs, salt/src/state/hasher.rs pinned-hash tests.
Explicitly out of scope
- Generic arithmetic / relational / logical / function-body operators — already covered by the
cargo-mutants gate; duplicating them just muddies which tool is authoritative.
- Bucket resize load-factor (
BUCKET_RESIZE_LOAD_FACTOR_PCT) and growth multiplier (BUCKET_RESIZE_MULTIPLIER) — configurable by design, not consensus-critical.
Proposed next steps
Motivation
Our mutation-testing gate uses
cargo-mutants, which ships a fixed, curated set of structural operators (replace function body with a default, flip binary/relational/logical/bit operators, a few literals). It has no plugin API, so it is structurally blind to the mutations that matter most for a state-commitment system — endianness, field-element chunk widths, codec configuration, canonicalization, and the trie's level/bit-width arithmetic.universalmutatoris regex-rule based (regex ==> replacement), language-agnostic, and auto-discards mutants that don't compile or compile identically. That makes it a good vehicle for a complementary, domain-specific mutation layer:cargo-mutantsguards generic code structure;universalmutatorguards SALT semantics.This issue catalogs candidate domain operators to prototype. The selection filter throughout: "what soundness or determinism invariant would a surviving mutant here violate?" We deliberately skip operators that overlap with
cargo-mutants(generic arithmetic/relational/logical), and skip the resize load-factor / growth-multiplier (configurable by design, not consensus-critical).A. Trie-structure operators (highest leverage)
The SALT trie's correctness reduces to "right level-base and right bit-width?" — pure integer arithmetic over a
STARTING_NODE_ID[level]table and three distinct bit-widths (TRIE_WIDTH_BITS = 8,MIN_BUCKET_SIZE_BITS = 8,BUCKET_SLOT_BITS = 40). A wrong constant produces a plausible, compiling, wrong structure — exactly what mutation testing exists to catch. Seesalt/src/trie/node_utils.rsandsalt/src/constant.rs.A1. Level-index off-by-one (the base table) — the sharpest family:
Expected kills:
test_get_child_node,test_get_parent_node(inverse relationship),test_bucket_root_node_id,test_subtree_leaf_for_key.A2. Bit-width constant confusion (the 24/40/8 split) — swap one named width for another real one; invisible to any generic tool:
Expected kills:
test_subtree_leaf_for_key,test_vc_position_in_parent(max-bucket case).A3. Fanout shift direction / amount (the
<< 8/>> 8that is the 256-way branching):Expected kills:
get_child_node/get_parent_nodeinverse test.A4. Segment granularity (256 slots per leaf):
Expected kills:
test_subtree_leaf_for_key_segment_boundaries,test_subtree_leaf_start_key,test_vc_position_in_parent.A5. Ceil-division rounding in the subtree-root climb —
subtree_root_leveldoes(capacity + MIN_BUCKET_SIZE - 1) >> BITS; dropping the- 1turns ceil into floor, wrong at exact powers of 256. The subtlest one, and a perfect target because the rounding correction is invisible to the eye:Expected kills:
test_subtree_root_level(257→3,65536→3,65537→2boundary cases).B. Endianness / byte-order
Explicit byte packing in the hasher and codecs; wrong endianness silently breaks determinism and cross-platform reproducibility, and no generic operator flips it.
Expected kills:
salt/src/state/hasher.rspinned-output tests,salt/tests/proof_fixture_codec.rs.C. Field-element / chunk width (the 31-vs-32 hazard)
Classic commitment bug: a value is 32 bytes but a field element only safely holds 31. Scope the bare
31/32rules to specific files.Expected kills: trie chunking / scalar-encoding tests.
D. Serialization codec config
The
legacy()codec choice is the wire format; swapping it should break the byte-for-byte fixture test.Expected kills:
salt/tests/proof_fixture_codec.rs.E. Canonicalization removal (sort / dedup)
The proof path sorts/dedups to reach canonical form. Deleting those calls is a soundness mutation (duplicates / unordered elements should be rejected or change the root). Line deletion is not in
cargo-mutants' repertoire.Expected kills:
salt/src/proof/prover.rssort/dedup canonicalization tests.F. Hash-diffusion ops
Rotates / xors in hash mixing, which
cargo-mutantswon't touch. Keep xor→or file-scoped to the hasher.Expected kills:
salt/src/state/ahash/fallback.rs,salt/src/state/hasher.rspinned-hash tests.Explicitly out of scope
cargo-mutantsgate; duplicating them just muddies which tool is authoritative.BUCKET_RESIZE_LOAD_FACTOR_PCT) and growth multiplier (BUCKET_RESIZE_MULTIPLIER) — configurable by design, not consensus-critical.Proposed next steps
mutants/salt.rules(Rust isn't a first-classuniversalmutatorlanguage, so run with the generic ruleset + these custom rules).universalmutatoroversalt/src/trie/,salt/src/constant.rs,salt/src/state/, andsalt/src/proof/; diff the survivor set.cargo-mutantssuppression registry).cargo-mutantsrun.