# merkle-membership-not-enforced

> Proof-related state changes lack an enforced relationship between the exact digest and trusted ledger roots.

Canonical: https://nightshade.dicksonwu.xyz/rules/merkle-membership-not-enforced.html
Release: Nightshade 0.1.1 (v0.1.1)

| Field | Value |
| --- | --- |
| Rule ID | `merkle-membership-not-enforced` |
| Severity | medium |
| Confidence | low |
| Maturity | experimental |
| Selection | Audit default |
| Policy intent | not required |
| Default enabled | yes |
| Category | Audit-default ordinary rules |
| Engine facts used | merkle-observations, successful-predicates, private-provenance, state-writes, completed-entries |
| Release | v0.1.1 |

## Selection and scope

Selected by the default audit profile, so an ordinary scan runs it.

```sh
./.nightshade/node_modules/.bin/nightshade ./contracts --detectors merkle-membership-not-enforced
./.nightshade/node_modules/.bin/nightshade detector show merkle-membership-not-enforced
```

The first command runs only this rule; `--detectors` takes a comma-separated list. The second prints the rule's metadata from your installed version.

Severity is review priority. It does not express exploitability or impact. Confidence is detector metadata, not a probability. All built-in rules are experimental.

Compiler scope: the engine supports the pinned profiles 0.28.0, 0.29.0, 0.30.0, 0.31.1, 0.33.0-rc.2, 0.34.0, 0.35.0-rc.1 and 0.35.0, with ZKIR2 by default. Support for this rule can be narrower; the rule documentation below states any narrower scope.

## Verified example

Synthetic example from the proof run of the released CLI. Computing and checking a Merkle root does not enforce membership when the boolean result is discarded. Full example: https://nightshade.dicksonwu.xyz/examples.md#merkle-membership-not-enforced

```compact
import CompactStandardLibrary;
ledger tree: MerkleTree<2,Field>;
export ledger accepted: Field;
witness suppliedPath(): MerkleTreePath<2,Field>;
export circuit accept(): [] {
  const proof = suppliedPath();
  const digest = merkleTreePathRoot<2,Field>(proof);
  tree.checkRoot(disclose(digest));
  accepted = disclose(proof.leaf);
}
```

Output (exit code 1):

```text
$ nightshade vulnerable.compact --config config.json --compiler /path/to/compactc

MEDIUM — Proof-related effect lacks enforced trusted-root membership [merkle-membership-not-enforced]
vulnerable.compact:7:18
  7 |   const digest = merkleTreePathRoot<2,Field>(proof);
    |                  ^
accept has a modeled successful history where proof-related data influences accepted, without a required check of that digest against definite preexisting ledger root data. Computing or ignoring a membership result does not enforce it. Review whether this effect requires membership; public deposits and root-management flows may be intentional.
Source: vulnerable.compact:7:18
Call path: accept → merkleTreePathRoot
Remediation: If membership is required, constrain successful execution with the exact digest's ledger-tree checkRoot result or equality with the intended trusted root. Preserve valid alternate checks and intentional public behavior.

Semantic profile: compact-0.34.0-zkir2.
Baseline analysis: complete; selected rules: complete.
Rule coverage: 1 selected rule(s); 0 with incomplete entries.
1 finding(s), 0 suppressed, 0 baseline; 1/1 entrypoint(s) completed; analysis complete. Compiler validation passed.
```

Corrected: Assert the result of checkRoot.

```compact
import CompactStandardLibrary;
ledger tree: MerkleTree<2,Field>;
export ledger accepted: Field;
witness suppliedPath(): MerkleTreePath<2,Field>;
export circuit accept(): [] {
  const proof = suppliedPath();
  const digest = merkleTreePathRoot<2,Field>(proof);
  assert(tree.checkRoot(disclose(digest)), "member");
  accepted = disclose(proof.leaf);
}
```

Output (exit code 0):

```text
$ nightshade corrected.compact --config config.json --compiler /path/to/compactc

Semantic profile: compact-0.34.0-zkir2.
Baseline analysis: complete; selected rules: complete.
Rule coverage: 1 selected rule(s); 0 with incomplete entries.
0 finding(s), 0 suppressed, 0 baseline; 1/1 entrypoint(s) completed; analysis complete. Compiler validation passed.
```

Paths are shortened to the example folder. A quiet result shows that this rule stopped firing, not that the contract is secure.

## Corpus checks

Synthetic corpus checks for this rule in the 0.1.1 proof run. A check counts only with compiler acceptance and complete analysis.

| Positive detected | Corrected quiet | Lookalike quiet | Other negative quiet | Incomplete |
| ---: | ---: | ---: | ---: | ---: |
| 5/5 | 5/5 | 5/5 | 0/0 | 0 |

No deviations were recorded for this rule.

These are behavior checks on synthetic fixtures, not a precision or recall estimate. See [evaluation](https://nightshade.dicksonwu.xyz/evaluation.md#synthetic-corpus).

## Rule documentation

From https://github.com/DicksonWu654/nightshade/blob/v0.1.1/docs/detectors/merkle-membership-not-enforced.md at v0.1.1.

Availability: **default**; no policy required. Severity **medium**, confidence **low**.

Owned example: the compiler-qualified [fixture](#example-1)
discards a root-check result before storing proof-dependent data. The
[correction](#example-2) asserts
that exact digest's check. Review a write that is intended to require tree
membership; recommendation: enforce the relationship to the trusted preexisting
root on every successful relevant history.

A default-enabled **medium severity, low confidence** structural review rule. It
reports a successful history where proof-related data influences persistent state
or a canonical token operation without enforcing that exact digest's relationship
to a preexisting ledger root. Whether membership is intended needs application
review; this does not establish an authorization defect or exploitability.

For example, this produces a review finding:

```compact
const proof = suppliedPath();
const digest = merkleTreePathRoot<2,Field>(proof);
tree.checkRoot(disclose(digest));
accepted = disclose(proof.leaf);
```

When membership is required, constrain successful execution:

```compact
assert(tree.checkRoot(disclose(digest)), "member");
accepted = disclose(proof.leaf);
```

A missing constraint can leave a state-changing operation independent of the
intended tree membership. A check of a different digest, an ignored Boolean or an
equality with a newly self-written root does not establish this relationship.
The severity reflects the potential state/token effect; low confidence reflects
unknown application intent and bounded abstract feasibility.

The candidate must be a canonical computed root or private supplied
`MerkleTreeDigest`, including nested wrapper/parameter fields, whose data or control
dependencies reach a persistent write or canonical mint/send operation. Computed
roots retain dependencies on the full path, including private nodes with a public
leaf. Auxiliary path nodes do not independently become supplied-root candidates.
Constructors, digest-only helpers and namesake user functions/types are outside
this rule. An arbitrary Field witness is not presumed to be a digest.

Dependencies also follow a required same-type equality of an exact private input
before the effect. A separately supplied output constrained to equal a
proof-related commitment's hash can therefore retain that relationship when it
is stored. Sharing flow origins, wildcard storage addresses or witness declarations
does not establish equality. Derived identities and cross-type comparisons do
not identify a private input. Late equality cannot create an earlier dependency;
late atomic root checks are assessed separately. This closure and every supplied
digest candidate consume the existing matching allowance; exhaustion exposes no
partial findings. Each immutable history decodes equality once; each effect visits
only reachable dependency edges, filtering both write and operation positions.
Histories without native operations retain ordinary flow provenance without new
equality inference. Missing nonzero operation positions remain incomplete.

Accepted evidence is a required positive result of the exact digest's check against
a definite preexisting ledger tree, or transitive required equality with definite
preexisting ledger root data. Query observations preserve their receiver snapshot,
so dynamic-key checks and saved Booleans work without equating may-alias regions.
Alternate checks, Boolean aliases, checks later in an atomic circuit, repeated
computations and nested calls are supported. A prior required check stays valid
after a later write. A check first made after insertion/reset does not establish
membership in the original tree; equality with a retained original root can.

Proof-identity traversal, predicate decoding, observation matching and path/effect
storage have explicit bounds. Missing evidence or exhaustion emits an incomplete
diagnostic. Detector-query exhaustion discards all partial findings for this rule.
Caller summaries are interpreted while Merkle observations are requested because
existing caller summaries do not replay these observations; leaf summaries remain
available. This does not expand correlated-engine admission or default budgets.

Some state updates intentionally do not require tree membership. The rule reports
only the documented structural relationship and cannot infer whether the
application requires membership, prove a feasible bypass, or establish deployed
impact.

### Source examples

These short regression examples are included here for installed-package readers.
Rule-specific policy requirements and limits above still apply.

#### Example 1

Fixture (source: `tests/corpus/vulnerable/merkle-membership-not-enforced.compact`).

```compact
import CompactStandardLibrary;
ledger tree: MerkleTree<2,Field>;
export ledger accepted: Field;
witness suppliedPath(): MerkleTreePath<2,Field>;
export circuit accept(): [] {
  const proof = suppliedPath();
  const digest = merkleTreePathRoot<2,Field>(proof);
  tree.checkRoot(disclose(digest));
  accepted = disclose(proof.leaf);
}
```

#### Example 2

Correction (source: `tests/corpus/safe/merkle-membership-not-enforced.compact`).

```compact
import CompactStandardLibrary;
ledger tree: MerkleTree<2,Field>;
export ledger accepted: Field;
witness suppliedPath(): MerkleTreePath<2,Field>;
export circuit accept(): [] {
  const proof = suppliedPath();
  const digest = merkleTreePathRoot<2,Field>(proof);
  assert(tree.checkRoot(disclose(digest)), "member");
  accepted = disclose(proof.leaf);
}
```

## Suppress a match

If a match is intentional, suppress it on the line before with the exact rule ID and a reason of at least eight characters:

```compact
// nightshade-disable-next-line merkle-membership-not-enforced -- Reason this match is intentional
```

Diagnostics cannot be suppressed, and an unmatched directive for a selected rule makes analysis incomplete. See [configuration](https://nightshade.dicksonwu.xyz/configuration.md#suppress-an-intentional-match) for fingerprint suppressions and baselines.

## References

- [Rule source document at v0.1.1](https://github.com/DicksonWu654/nightshade/blob/v0.1.1/docs/detectors/merkle-membership-not-enforced.md)
- [Catalog entry in rules.json](https://nightshade.dicksonwu.xyz/rules.json)
- [Verified example](https://nightshade.dicksonwu.xyz/examples.md#merkle-membership-not-enforced)
- [All rules](https://nightshade.dicksonwu.xyz/rules.md#audit-default-ordinary-rules)
- [Limitations](https://nightshade.dicksonwu.xyz/limitations.md)
