Skip to content

#233: input validation error reported as fatal#1489

Draft
m2ux wants to merge 10 commits into
mainfrom
fix/233-dust-public-key-fatal-validation
Draft

#233: input validation error reported as fatal#1489
m2ux wants to merge 10 commits into
mainfrom
fix/233-dust-public-key-fatal-validation

Conversation

@m2ux
Copy link
Copy Markdown
Contributor

@m2ux m2ux commented May 11, 2026

Summary

Stop pallet-cnight-observation from emitting a "Fatal" log line for Dust registrations whose DustPublicKey byte payload is the wrong scalar value; validate at the inherent boundary (consensus-gated by a CNightObservationApi v2 → v3 bump) and narrow the legacy log severity in the pallet so misleading "Fatal" lines disappear on both new and old runtimes.

🐛 Issue 📐 Engineering

Jira: PM-22301


Motivation

A Dust registration submitted on Cardano with a DustPublicKey whose 33-byte payload is the right length but whose decoded value lies outside the cryptographic field Fr is stored in Mappings::<T> unchanged: the data source's only structural check is a length bound, and the pallet's handle_registration writer never invokes the ledger bridge. The field-range failure surfaces much later, when a subsequent AssetCreate event triggers LedgerApi::construct_cnight_generates_dust_event(...). The ledger bridge logs Error deserializing: "midnight_ledger::dust::DustPublicKey": ... out of range for Fr at error severity, and the pallet then logs Fatal: Unable to construct CNightGeneratesDustEvent: ....

Operationally the chain is unaffected — handle_create returns None, the per-UTXO loop in process_tokens discards None results, block production continues. But the log severity is wrong: the word "Fatal" trains on-call engineers to escalate, and severity-keyed monitoring pipelines do the same. The fix moves field-range validation to the inherent-data-provider boundary so invalid registrations never enter chain state, and narrows the pallet's log line so the legacy path (old runtime on a new binary during the upgrade window) also stops mis-labelling the recoverable case.


Changes

Implementation (landed):

  • Validator — added pub fn dust_public_key_is_valid(bytes: &[u8]) -> bool to ledger/src/versions/common/api/mod.rs. Calls <DustPublicKey as Deserializable>::deserialize directly so no log::error! side effect fires on failure. (commit 39da76b3)
  • Runtime API — bumped CNightObservationApi from #[api_version(2)] to #[api_version(3)] at primitives/cnight-observation/src/lib.rs:397. Doc comment expanded alongside the v1/v2 history to record the IDP-filter rationale. (commit c7cfdf5c)
  • Inherent data providerMidnightCNightObservationInherentDataProvider::new now calls a free function filter_invalid_dust_public_key_registrations against the data-source UTXOs. At api_version >= 3 invalid ObservedUtxoData::Registration entries are dropped with a log::debug! recording the Cardano reward address and hex-encoded bytes; at api_version <= 2 legacy pass-through is preserved. The filter is factored out so unit tests run without a ProvideRuntimeApi<Block> mock. midnight-primitives-mainchain-follower picks up midnight-node-ledger and hex as direct std-feature dependencies. (commit 62d66054)
  • Pallet log overlay — narrowed handle_create (:528-537) and handle_spend (:569-575) Err(e) arms. LedgerApiError::Deserialization(DeserializationError::DustPublicKey) logs at debug with a Skipping AssetCreate/AssetSpend ... prefix; every other variant logs at warn (no error). The Fatal: substring is dropped throughout. (commit fcbf6da6)
  • Change filechanges/runtime/changed/dust-public-key-fatal-validation.md recording the API version bump, the IDP filter gate, the log-downgrade overlay, and the explicit decision not to migrate existing storage entries. (commit 41611b6c)
  • Tests — validator unit tests at ledger/src/versions/common/api/mod.rs (round-trip valid key + three reject paths, commit cb45ebc9); IDP filter unit tests at primitives/mainchain-follower/src/idp/cnight_observation.rs for v2 pass-through, v3 drop, variant scoping, ordering preservation (commit 0f04c992); pallet integration tests at pallets/cnight-observation/tests/tests.rs covering handle_spend invalid-key path and batch tolerance through process_tokens (commit 18527afc).
  • Local validationcargo fmt --check, cargo build, and cargo clippy --no-deps --tests -- -D warnings all clean on midnight-node-ledger, midnight-primitives-cnight-observation, midnight-primitives-mainchain-follower, pallet-cnight-observation. Test execution is operator-only per local convention (cargo test OOMs the agent host). (commit 2b284007)

Out of scope: storage purge of any pre-existing invalid registrations on testnet; extension of IDP-level validation to other DeserializationError variants; throttle pallet rate-limiting for spam.


📌 Submission Checklist

  • Changes are backward-compatible (or flagged if breaking)
  • Pull request description explains why the change is needed
  • Self-reviewed the diff
  • I have included a change file, or skipped for this reason: [reason]
  • If the changes introduce a new feature, I have bumped the node minor version
  • Update documentation (if relevant)
  • No new todos introduced

🔱 Fork Strategy

  • Node Runtime Update
  • Node Client Update
  • Other
  • N/A

🗹 TODO before merging

  • Ready for review

Draft PR for: [node] Input validation error reported as "fatal"
GitHub Issue: shieldedtech/shielded-security-engineering#233

Residual scope: pallet_cnight_observation processing of AssetCreate events
for Dust registrations with invalid DustPublicKey values currently logs a
"Fatal" error for what is recoverable input validation. Implementation
will determine whether to introduce a DustPublicKeyInvalid LedgerApiError
variant (Option A) or perform pre-validation in the inherent data
provider (Option B, fork-safe).

Signed-off-by: Mike Clay <mike.clay@shielded.io>
@m2ux m2ux self-assigned this May 11, 2026
m2ux added 9 commits May 11, 2026 13:44
Expose a thin pub fn that calls <DustPublicKey as Deserializable>::deserialize
directly, returning bool without logging. Intended for upstream filters
(cnight-observation IDP) that treat invalid DustPublicKey registrations as a
per-UTXO non-fatal outcome — calling Api::deserialize would emit an error-level
log line on every drop, which is undesirable here.

Refs: shieldedtech/shielded-security-engineering#233, PM-22301
Signed-off-by: Mike Clay <mike.clay@shielded.io>
v3 marks the consensus-affecting introduction of the IDP-level Fr-range
validation of DustPublicKey payloads on registration UTXOs. The inherent
payload changes once the filter activates, so the new behaviour must gate on
the on-chain API version — mirroring the v1 → v2 over-fetch-factor gate in
the same trait. Doc comment expanded to record the rationale alongside the
v1/v2 history.

Refs: shieldedtech/shielded-security-engineering#233, PM-22301
Signed-off-by: Mike Clay <mike.clay@shielded.io>
…, gated on CNightObservationApi v3

MidnightCNightObservationInherentDataProvider::new now drops registration
UTXOs whose DustPublicKey payload fails the ledger validator
(dust_public_key_is_valid). The check is consensus-affecting — the inherent
payload changes once the filter activates — so it is gated on the on-chain
CNightObservationApi version, mirroring the v1 → v2 over-fetch-factor gate
that lives a few lines above. At v2 and earlier the legacy pass-through is
preserved so old-runtime + new-binary pairings stay consensus-equivalent
across the upgrade window.

Per-drop log line is at debug severity (not error) and records the Cardano
reward address plus the hex-encoded invalid key bytes for downstream
operators.

Refs: shieldedtech/shielded-security-engineering#233, PM-22301
Signed-off-by: Mike Clay <mike.clay@shielded.io>
… fatal to debug

handle_create and handle_spend in pallet-cnight-observation both fired
log::error!("Fatal: ...") when LedgerApi::construct_cnight_generates_dust_event
returned any error. For the DustPublicKey deserialize variant the call site
is not fatal — invalid registrations are filtered upstream at the IDP from
CNightObservationApi v3, and at legacy api versions the pallet still ingests
them but cannot construct the downstream event. Narrowing the match routes
the DustPublicKey variant at debug severity; other ledger errors continue to
be reported, but at warn severity (not error) and without the misleading
"Fatal:" prefix.

This preserves operator visibility into genuine bridge failures while
silencing the per-block noise that the upgrade window would otherwise
produce on legacy runtimes paired with a new binary.

Refs: shieldedtech/shielded-security-engineering#233, PM-22301
Signed-off-by: Mike Clay <mike.clay@shielded.io>
…lidation

Records the CNightObservationApi v2 → v3 bump, the IDP-level validation gate,
and the variant-narrowed log-severity downgrade on the pallet call sites.
Mirrors the shape of granular-ledger-error-codes.md and
audit-motion-close-weight-bound.md.

Refs: shieldedtech/shielded-security-engineering#233, PM-22301
Signed-off-by: Mike Clay <mike.clay@shielded.io>
Adds a small test module alongside dust_public_key_is_valid covering the
happy path (deterministic derive_secret_key seed → serialize → validate) and
three reject paths: 33-byte all-0xff (out-of-Fr-range), empty, and length-1.
The helper avoids pulling rand into the ledger crate's dev-dependencies by
using DustSecretKey::derive_secret_key with a fixed seed.

Refs: shieldedtech/shielded-security-engineering#233, PM-22301
Signed-off-by: Mike Clay <mike.clay@shielded.io>
…version 2 and 3

Factors the filter into a free fn filter_invalid_dust_public_key_registrations
so it can be tested without instantiating a ProvideRuntimeApi<Block> mock.
Four cases cover (a) v2 pass-through, (b) v3 drop of invalid registration,
(c) per-utxo / variant-scoped behaviour, and (d) ordering preservation
under Vec::retain.

The "valid" fixture is asserted against the production validator so any
future encoding change surfaces as a loud test failure rather than silently
flipping the test's truth condition.

Refs: shieldedtech/shielded-security-engineering#233, PM-22301
Signed-off-by: Mike Clay <mike.clay@shielded.io>
…ch tolerance

Adds two pallet integration tests around the variant-narrowed log downgrade:

- handle_spend_returns_none_for_invalid_dust_public_key — drives an invalid
  DustPublicKey through handle_spend via a seeded UtxoOwners entry; confirms
  no SystemTransactionApplied event is emitted.
- process_tokens_preserves_batch_tolerance_with_invalid_registration — mixes
  one valid registration + create with one invalid registration + create;
  asserts exactly one SystemTransactionApplied event is emitted (for the
  valid-key create) without poisoning the batch.

Log-severity expectations are documented inline as comments because the
pallet mock does not install a tracing subscriber; behavioural assertions
(return value, event presence) are the primary correctness signals.

Refs: shieldedtech/shielded-security-engineering#233, PM-22301
Signed-off-by: Mike Clay <mike.clay@shielded.io>
…validation

Three formatter passes and two clippy lints from --no-deps -D warnings:

- ledger/src/versions/common/api/mod.rs: hoist dust_public_key_is_valid
  above its #[cfg(test)] module to satisfy clippy::items-after-test-module;
  reflow assert! call to fit standard rustfmt column budget.
- primitives/mainchain-follower/src/idp/cnight_observation.rs: move test
  module to end of file (items-after-test-module); drop redundant & on
  hex::encode args (clippy::needless-borrows-for-generic-args); use
  .as_slice() on BoundedVec for the same lint; reflow vec! literal.
- Cargo.lock: record the new midnight-node-ledger and hex direct deps in
  midnight-primitives-mainchain-follower from task 3.

No functional change.

Refs: shieldedtech/shielded-security-engineering#233, PM-22301
Signed-off-by: Mike Clay <mike.clay@shielded.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant