# ledger-bound-preimage-publication

> A private preimage checked against a ledger hash is also published on a potential successful path.

Canonical: https://nightshade.dicksonwu.xyz/rules/ledger-bound-preimage-publication.html
Release: Nightshade 0.1.1 (v0.1.1)

| Field | Value |
| --- | --- |
| Rule ID | `ledger-bound-preimage-publication` |
| Severity | medium |
| Confidence | medium |
| Maturity | experimental |
| Selection | Audit default |
| Policy intent | not required |
| Default enabled | yes |
| Category | Audit-default ordinary rules |
| Engine facts used | resolved-calls, cfg, private-provenance, hash-bindings, public-sinks |
| 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 ledger-bound-preimage-publication
./.nightshade/node_modules/.bin/nightshade detector show ledger-bound-preimage-publication
```

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. A secret checked against a stored hash is then written directly to public state. Full example: https://nightshade.dicksonwu.xyz/examples.md#ledger-bound-preimage-publication

```compact
pragma language_version >= 0.26.0;
import CompactStandardLibrary;
export ledger ownerHash: Bytes<32>;
export ledger published: Field;
witness ownerSecret(): Field;
constructor(initialHash: Bytes<32>) {
  ownerHash = disclose(initialHash);
}
export circuit update(): [] {
  const secret = ownerSecret();
  assert(persistentHash<Field>(secret) == ownerHash, "not authorized");
  published = disclose(secret);
}
```

Output (exit code 1):

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

MEDIUM — Ledger-bound private preimage is published [ledger-bound-preimage-publication]
vulnerable.compact:12:3
  12 |   published = disclose(secret);
     |   ^
Private value from ownerSecret reaches ledger-write published without a hiding transformation. A hash of this value is asserted equal to ledger ownerHash on the same potential successful path. Ledger operation data is public. If an untrusted observer obtains a reusable authentication secret through this boundary, secrecy is lost. An intentional reveal protocol may permit this flow.
Source: vulnerable.compact:10:18
Constraint: vulnerable.compact:11:3
Call path: update
Remediation: Keep reusable authentication preimages private; publish a suitable commitment or non-secret result. If this is intentional reveal, confirm that the revealed value grants no remaining authority and document that policy.

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; 2/2 entrypoint(s) completed; analysis complete. Compiler validation passed.
```

Corrected: Publish the hash instead of the secret.

```compact
pragma language_version >= 0.26.0;
import CompactStandardLibrary;
export ledger ownerHash: Bytes<32>;
export ledger published: Bytes<32>;
witness ownerSecret(): Field;
constructor(initialHash: Bytes<32>) {
  ownerHash = disclose(initialHash);
}
export circuit update(): [] {
  const secret = ownerSecret();
  assert(persistentHash<Field>(secret) == ownerHash, "not authorized");
  published = disclose(persistentHash<Field>(secret));
}
```

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; 2/2 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 |
| ---: | ---: | ---: | ---: | ---: |
| 7/7 | 12/12 | 1/1 | 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/ledger-bound-preimage-publication.md at v0.1.1.

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

Reports the same raw private source reaching a public sink and having a hash
containing that exact source asserted equal to preexisting ledger data on one
modeled successful path. The hash may include constants or public context such
as a ledger sequence; a second private ingredient is outside this contract.
Ledger writes and ledger-operation arguments are public sinks; exported returns
are application outputs and are not necessarily published on the ledger.

### Example and review scenario

The compiler-qualified owned [direct fixture](#example-1)
checks a witness preimage against `ownerHash` and also publishes that preimage.
Review whether this is a reusable authentication secret or an intentional reveal.
The [correction](#example-2) publishes only the digest:

```compact
assert(persistentHash<Field>(secret) == ownerHash, "not authorized");
published = disclose(persistentHash<Field>(secret));
```

Recommendation: keep reusable preimages private and publish an appropriate
non-secret result. For a reveal protocol, confirm that disclosure grants no
remaining authority and document the intended boundary. A digest alone does not
guarantee secrecy for low-entropy input.

### False positives and limitations

Intentional commit/reveal can match. The rule does not prove concrete feasibility,
reusability, intended authentication, or deployed exploitability. Transformed
preimages, hashes with multiple private sources and cryptographic equivalence
are outside the evidence contract. Identity-preserving numeric casts and full-width
Bytes slices retain raw provenance. `disclose` alone is permission, not a sink;
discarded disclosure is quiet. Unknown effects and exhausted budgets remain
incomplete. Fixture labels concern this detector only; see the
[corpus qualification scope](https://github.com/DicksonWu654/nightshade/blob/v0.1.1/docs/evaluation.md).

Real-source regression: the recorded bulletin-board mutation publishes a key bound
by `hash([domain, sequence, secret])`. It is now detected; the hash-only correction
and separate-witness lookalike remain quiet. Private-controlled context, transformed
secrets and may-alias source unions do not establish this single-preimage evidence.

### Source examples

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

#### Example 1

Direct fixture (source: `tests/corpus/vulnerable/direct.compact`).

```compact
pragma language_version >= 0.26.0;
import CompactStandardLibrary;
export ledger ownerHash: Bytes<32>;
export ledger published: Field;
witness ownerSecret(): Field;
constructor(initialHash: Bytes<32>) {
  ownerHash = disclose(initialHash);
}
export circuit update(): [] {
  const secret = ownerSecret();
  assert(persistentHash<Field>(secret) == ownerHash, "not authorized");
  published = disclose(secret);
}
```

#### Example 2

Correction (source: `tests/corpus/safe/hash-only.compact`).

```compact
pragma language_version >= 0.26.0;
import CompactStandardLibrary;
export ledger ownerHash: Bytes<32>;
export ledger published: Bytes<32>;
witness ownerSecret(): Field;
constructor(initialHash: Bytes<32>) {
  ownerHash = disclose(initialHash);
}
export circuit update(): [] {
  const secret = ownerSecret();
  assert(persistentHash<Field>(secret) == ownerHash, "not authorized");
  published = disclose(persistentHash<Field>(secret));
}
```

## 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 ledger-bound-preimage-publication -- 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/ledger-bound-preimage-publication.md)
- [Catalog entry in rules.json](https://nightshade.dicksonwu.xyz/rules.json)
- [Verified example](https://nightshade.dicksonwu.xyz/examples.md#ledger-bound-preimage-publication)
- [All rules](https://nightshade.dicksonwu.xyz/rules.md#audit-default-ordinary-rules)
- [Limitations](https://nightshade.dicksonwu.xyz/limitations.md)
