Rule

merkle-membership-not-enforced

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

Release v0.1.1View Markdown

Specimen 16 of 96 · Audit-default ordinary rules

Severity
medium
Confidence
low
Maturity
experimental
Selection
Audit default
Policy intent
not required
Release
v0.1.1
Engine facts used
merkle-observations successful-predicates private-provenance state-writes completed-entries

Selection and scope#

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

Shell
./.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 examplecompact-0.34.0-zkir2compiler-validated

Computing and checking a Merkle root does not enforce membership when the boolean result is discarded. From the proof run of the released CLI; the examples page also has the lookalike and the files.

Compactvulnerable.compactline 7 flagged
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.txtexit 1
$ 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. Changed lines are marked.

Compactcorrected.compactrule quiet
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);
}
corrected-output.txtexit 0
$ 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.

The command line is shortened from the recorded run to what you would type; the output below it is verbatim, with absolute file paths shortened to the example folder. Long lines wrap. 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 detectedCorrected quietLookalike quietOther negative quietIncomplete
5/55/55/50/00

No deviations were recorded for this rule.

These are behavior checks on synthetic fixtures, not a precision or recall estimate. See evaluation.

Rule documentation#

From 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 discards a root-check result before storing proof-dependent data. The correction 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 for fingerprint suppressions and baselines.

References#