#233: input validation error reported as fatal#1489
Draft
m2ux wants to merge 10 commits into
Draft
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stop
pallet-cnight-observationfrom emitting a "Fatal" log line for Dust registrations whoseDustPublicKeybyte payload is the wrong scalar value; validate at the inherent boundary (consensus-gated by aCNightObservationApiv2 → 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
DustPublicKeywhose 33-byte payload is the right length but whose decoded value lies outside the cryptographic fieldFris stored inMappings::<T>unchanged: the data source's only structural check is a length bound, and the pallet'shandle_registrationwriter never invokes the ledger bridge. The field-range failure surfaces much later, when a subsequentAssetCreateevent triggersLedgerApi::construct_cnight_generates_dust_event(...). The ledger bridge logsError deserializing: "midnight_ledger::dust::DustPublicKey": ... out of range for Fraterrorseverity, and the pallet then logsFatal: Unable to construct CNightGeneratesDustEvent: ....Operationally the chain is unaffected —
handle_createreturnsNone, the per-UTXO loop inprocess_tokensdiscardsNoneresults, 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):
pub fn dust_public_key_is_valid(bytes: &[u8]) -> booltoledger/src/versions/common/api/mod.rs. Calls<DustPublicKey as Deserializable>::deserializedirectly so nolog::error!side effect fires on failure. (commit39da76b3)CNightObservationApifrom#[api_version(2)]to#[api_version(3)]atprimitives/cnight-observation/src/lib.rs:397. Doc comment expanded alongside the v1/v2 history to record the IDP-filter rationale. (commitc7cfdf5c)MidnightCNightObservationInherentDataProvider::newnow calls a free functionfilter_invalid_dust_public_key_registrationsagainst the data-source UTXOs. Atapi_version >= 3invalidObservedUtxoData::Registrationentries are dropped with alog::debug!recording the Cardano reward address and hex-encoded bytes; atapi_version <= 2legacy pass-through is preserved. The filter is factored out so unit tests run without aProvideRuntimeApi<Block>mock.midnight-primitives-mainchain-followerpicks upmidnight-node-ledgerandhexas direct std-feature dependencies. (commit62d66054)handle_create(:528-537) andhandle_spend(:569-575)Err(e)arms.LedgerApiError::Deserialization(DeserializationError::DustPublicKey)logs atdebugwith aSkipping AssetCreate/AssetSpend ...prefix; every other variant logs atwarn(noerror). TheFatal:substring is dropped throughout. (commitfcbf6da6)changes/runtime/changed/dust-public-key-fatal-validation.mdrecording the API version bump, the IDP filter gate, the log-downgrade overlay, and the explicit decision not to migrate existing storage entries. (commit41611b6c)ledger/src/versions/common/api/mod.rs(round-trip valid key + three reject paths, commitcb45ebc9); IDP filter unit tests atprimitives/mainchain-follower/src/idp/cnight_observation.rsfor v2 pass-through, v3 drop, variant scoping, ordering preservation (commit0f04c992); pallet integration tests atpallets/cnight-observation/tests/tests.rscoveringhandle_spendinvalid-key path and batch tolerance throughprocess_tokens(commit18527afc).cargo fmt --check,cargo build, andcargo clippy --no-deps --tests -- -D warningsall clean onmidnight-node-ledger,midnight-primitives-cnight-observation,midnight-primitives-mainchain-follower,pallet-cnight-observation. Test execution is operator-only per local convention (cargo testOOMs the agent host). (commit2b284007)Out of scope: storage purge of any pre-existing invalid registrations on testnet; extension of IDP-level validation to other
DeserializationErrorvariants; throttle pallet rate-limiting for spam.📌 Submission Checklist
🔱 Fork Strategy
🗹 TODO before merging