Skip to content

[PM-19896]: reduce panic risk in cnight-observation genesis construction#1466

Merged
m2ux merged 9 commits into
mainfrom
mike/pm-19896-cnight-genesis-panic-hardening
May 20, 2026
Merged

[PM-19896]: reduce panic risk in cnight-observation genesis construction#1466
m2ux merged 9 commits into
mainfrom
mike/pm-19896-cnight-genesis-panic-hardening

Conversation

@m2ux

@m2ux m2ux commented May 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Reduce panic risk in cnight-observation genesis construction by replacing the four remaining expect() calls in BuildGenesisConfig::build() with diagnostic panics whose messages name the chain-spec field path, the supplied byte length, and the maximum permitted length. The Substrate fail-fast genesis convention (build() returns (); node halts on invalid genesis) is preserved — only the diagnostic prose changes.

🎫 Ticket 📐 Engineering

GitHub Issue: shieldedtech/shielded-security-engineering#365


Motivation

External audit flagged three panic sites in the cnight-observation pallet (formerly native-token-observation):

  1. get_data_from_inherent_data — already remediated upstream via PM-21799 / PR [PM-21799] (fix): replace panic with error handling in get_data_from_inherent_data #1234.
  2. set_mapping_validator_contract_address — already remediated via the existing try_into().map_err(...) pattern.
  3. BuildGenesisConfig::build()residual scope for this work package.

The four remaining expect() calls in build() panic with four-word messages ("Mapping Validator address longer than expected", "Policy ID too long", "Asset name too long", "Auth Token asset name longer than expected") that don't say which field was wrong, what length was supplied, or what the cap was. Replacing them with descriptive errors that name the offending field and the maximum permitted length turns a noisy crash into a one-line, self-explanatory diagnostic, shortening operational recovery and closing the audit finding.


Planned Changes

Round 1 (landed — 8b3b8976, 6bd01131). Named length constants in primitives/cnight-observation; four expect()unwrap_or_else(|_| panic!(...)) in BuildGenesisConfig::build() naming the field, supplied length, and cap; three panic-shape tests; mock helpers; change fragment.

Round 2 (landed — e0f64900). Correct the field path in panic messages to the runtime's actual chain-spec key (cNightObservation.config.addresses.<field>); replace the four near-duplicate panic blocks with a single generic helper bounded_or_panic_with_field_path<S: Get<u32>>(field_path, bytes) that reads the cap via BoundedVec::<u8, S>::bound(); update three round-1 tests; add two helper-level tests.

Round 3 (landed — 134b4da8, on top of merge 15e310b3). Addresses Klapeyron's overcomplication critique on e0f64900: remove the helper and inline the four panics, restore the mock crate to its pre-PR shape, reduce the five panic-message tests to one representative regression guard on mapping_validator_address, slim the change file. Net diff vs PR base shrinks from +288/−46 to +112/−21 (≈176 LOC reduction). Panic strings, field paths, and the operator-facing contract are unchanged.

Out of scope: sibling pallets' genesis builds; serde_valid::Validate at the genesis boundary; typing CNightAddresses fields as BoundedVec directly (deferred — captured as follow-up issue per LGLO's suggestion); asset-name minimum-length check (CDDL allows zero-length); storage migrations; runtime metadata regeneration.

Detailed plan: 05-work-package-plan.md. Test plan: 05-test-plan.md.


📌 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] triage audit findings reduce panic risk in inherent and genesis construction
Jira: https://shielded.atlassian.net/browse/PM-19896
GitHub Issue: shieldedtech/shielded-security-engineering#365

Residual scope: four expect() calls in BuildGenesisConfig::build() at
pallets/cnight-observation/src/lib.rs:307,316,323,333

Signed-off-by: Mike Clay <mike.clay@shielded.io>
@m2ux m2ux self-assigned this May 6, 2026
@Klapeyron

Copy link
Copy Markdown
Contributor

I am not sure if we can make significant improvement on that, be design build genesis calls are meant to fail and existing expects are quite descriptive, do you plan to provide the length details for Policy ID too long or Asset name too long?

Replace the four `expect(...)` calls in `pallet-cnight-observation`'s
`BuildGenesisConfig::build()` with `unwrap_or_else(|_| panic!(...))` whose
messages name the chain-spec field path, the supplied byte length, and the
maximum permitted length expressed via a named constant. An operator reading
a startup-failure log can now locate and correct the offending field
without inspecting the source.

Introduce two named bound constants in `midnight-primitives-cnight-observation`
so the panic messages and the bounded-vector storage type parameters share a
single source of truth:

- `CNIGHT_POLICY_ID_LENGTH = 28` (Cardano native-asset policy ID width)
- `CARDANO_ASSET_NAME_MAX_LENGTH = 32` (Cardano native-asset name CDDL cap)

Update `MainChainAuthTokenAssetName` and `CNightIdentifier` storage types to
use the new constants. `ConstU32<28>` and `ConstU32<CNIGHT_POLICY_ID_LENGTH>`
produce identical monomorphisations, so the Substrate metadata, storage
layout, and wire format are unchanged.

Add three unit tests in `pallets/cnight-observation/tests/tests.rs` that
drive `BuildGenesisConfig::build` through `build_storage()` with an
over-length value for `mapping_validator_address`, `auth_token_asset_name`,
and `cnight_asset_name`, capture the panic via `std::panic::catch_unwind`,
and assert the panic message contains both the dotted chain-spec field path
and the cap-constant name. `cnight_policy_id` is not covered by a unit test
because its source type is `[u8; 28]` and the panic site is unreachable from
chain-spec deserialization (serde rejects mismatched lengths first); the
defence-in-depth panic remains so a future change to the source type does
not silently lose the diagnostic.

Expose `runtime_genesis_with_cnight_addresses` and `midnight_test_config`
helpers in the cnight-observation mock crate so the new tests reuse the
existing `MidnightConfig` initializer rather than duplicating it.

Behaviour for valid chain specs is byte-identical: `try_into` still produces
the `BoundedVec` and `set` is called as before. The only observable change
is the panic string for over-length input.

Closes audit finding for the four cnight-observation genesis sites.

JIRA: PM-19896
Signed-off-by: Mike Clay <mike.clay@shielded.io>
@m2ux

m2ux commented May 6, 2026

Copy link
Copy Markdown
Contributor Author

I am not sure if we can make significant improvement on that, be design build genesis calls are meant to fail and existing expects are quite descriptive, do you plan to provide the length details for Policy ID too long or Asset name too long?

yes

Adds the GitHub issue reference required by the .github changes-check
workflow (regex 'github.com/.../issues/[0-9]+').

Issue: shieldedtech/shielded-security-engineering#365
Signed-off-by: Mike Clay <mike.clay@shielded.io>
@m2ux m2ux changed the title PM-19896: reduce panic risk in cnight-observation genesis construction [PM-19896]: reduce panic risk in cnight-observation genesis construction May 7, 2026
Comment thread pallets/cnight-observation/src/lib.rs Outdated
Comment thread pallets/cnight-observation/src/lib.rs Outdated
Comment thread pallets/cnight-observation/src/lib.rs Outdated

@LGLO LGLO left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH I would prefer passing BoundedVecs in CNightAddresses. Now we have some parsing there and some parsing in genesis build.

… bounded_or_panic helper

Round-2 review feedback on PR #1466 (commit 6bd0113) flagged two issues
with the genesis-build panic diagnostics introduced in 8b3b897:

- LGLO (R3): the chain-spec field-path strings used the snake_case pallet
  segment `cnight_observation.addresses.<field>` and omitted the `config.`
  segment. The runtime declares the pallet as `CNightObservation` and the
  Substrate v2 #[runtime::*] form renders it as `cNightObservation` in
  RuntimeGenesisConfig JSON; the GenesisConfig<T>'s Rust `config` field
  serialises as `config`. Operator-facing chain specs under res/ use
  `cNightObservation.config.addresses.<field>`. Correct the four panic
  messages and the corresponding test substrings.

- Klapeyron (R2): four near-duplicate `unwrap_or_else(|_| panic!(...))`
  blocks can be replaced with a single helper that reads the bound off
  the destination type via `BoundedVec::<u8, S>::bound()`, removing
  the duplicate plumbing and ensuring the message and the storage type
  cannot drift apart on a future bound change.

Add `pub fn bounded_or_panic_with_field_path<S: Get<u32>>(...)` and
route all four genesis-build BoundedVec conversions through it. The
named bound constants `CARDANO_BECH32_ADDRESS_MAX_LENGTH`,
`CNIGHT_POLICY_ID_LENGTH`, `CARDANO_ASSET_NAME_MAX_LENGTH` remain in
primitives — they are still used as `ConstU32<…>` parameters at storage
type definitions and helper call sites — but they no longer appear in
panic prose. Tests assert `maximum <N>` (the numeric bound returned by
`bound()`) instead.

Update the change file with a Future-hardening footnote noting the
deferred CNightAddresses → BoundedVec refactor (LGLO R4), and add two
helper-level unit tests pinning the helper's panic shape (field path,
supplied length, `maximum N`) and its happy path independent of the
pallet call sites.

Round-2 plan: §11–§19 of the work-package plan.
JIRA: https://shielded.atlassian.net/browse/PM-19896

Signed-off-by: Mike Clay <mike.clay@shielded.io>
@m2ux

m2ux commented May 9, 2026

Copy link
Copy Markdown
Contributor Author

@LGLO re your summary suggestion to type CNightAddresses fields as BoundedVec directly: agreed in principle and now listed under 'Out of scope' in the PR description as a follow-up. Pulling parsing into the deserialiser is the right end state, but it changes the chain-spec JSON encoding (operators currently pass a hex string for the policy ID and ASCII for the bech32 / asset names), so it deserves its own work package. Round 2 (e0f64900) keeps parsing in BuildGenesisConfig::build() but compresses the four sites through a single helper.

@m2ux m2ux requested review from Klapeyron and LGLO May 9, 2026 07:38
@m2ux m2ux marked this pull request as ready for review May 11, 2026 09:23
@m2ux m2ux requested a review from a team as a code owner May 11, 2026 09:23
Comment thread pallets/cnight-observation/src/lib.rs Outdated
Comment thread pallets/cnight-observation/src/lib.rs Outdated
@LGLO

LGLO commented May 13, 2026

Copy link
Copy Markdown
Contributor

@LGLO re your summary suggestion to type CNightAddresses fields as BoundedVec directly: agreed in principle and now listed under 'Out of scope' in the PR description as a follow-up. Pulling parsing into the deserialiser is the right end state, but it changes the chain-spec JSON encoding (operators currently pass a hex string for the policy ID and ASCII for the bech32 / asset names), so it deserves its own work package. Round 2 (e0f64900) keeps parsing in BuildGenesisConfig::build() but compresses the four sites through a single helper.

Culprit of my comment was parsing in two places, we could replace

	#[serde(with = "hex")]
	pub cnight_policy_id: [u8; 28],

with

	#[serde(with = "our_function_that_outputs_bounded_vec_of_exactly_28_bytes")]
	pub cnight_policy_id: BoundedVec<...>,

and/or

	#[cfg_attr(feature = "std", validate(pattern = r"^(addr|addr_test)1[0-9a-z]{1,108}$"))]
	pub mapping_validator_address: String,

with

	#[serde(with = "proper_serde_for_cardano_address")]
	pub mapping_validator_address: BoundedCardanoAddress,

(BTW validate pattern above allows for total 9+1+108=118 ascii chars, but runtime allows only 108 bytes) => again problem with having this logic in two places.

Anyway, I don't see a bug introduced in this PR. I think that if we touch this code (and add substantial number of lines), we can do it better.

m2ux added 2 commits May 14, 2026 10:26
…ke/pm-19896-cnight-genesis-panic-hardening

Signed-off-by: Mike Clay <mike.clay@shielded.io>
Round-3 fix-cycle response to external review on commit `e0f64900`. Klapeyron
(2026-05-13) observed that the round-1+2 solution shipped roughly 300 LOC of
helper, mock helpers, constants, comments, and tests to enrich four fail-fast
panic messages — a surface disproportionate to the underlying audit finding.
This commit accepts the critique and shrinks the change.

Changes:

- Remove `pub fn bounded_or_panic_with_field_path<S: Get<u32>>(...)` from the
  pallet. The four genesis-build call sites now use inline `unwrap_or_else(|v:
  Vec<u8>| panic!(...))` blocks, reading the supplied byte length from the
  `TryFrom<Vec<u8>>` error path and the cap from the destination `BoundedVec`
  type via `bound()`. The dotted chain-spec field-path strings introduced in
  round 2 to satisfy LGLO R3 are preserved verbatim
  (`cNightObservation.config.addresses.<field>`).

- Revert the two mock-crate helpers (`runtime_genesis_with_cnight_addresses`,
  `midnight_test_config`) and the corresponding `mock/Cargo.toml` dep entries.
  The `midnight` field literal is inlined back into `new_test_ext` exactly as
  it was pre-PR. The mock crate is now byte-identical to the PR base.

- Reduce the five round-1+2 tests to one representative regression guard
  (`build_panics_with_field_path_and_bound_when_mapping_validator_address_too_long`).
  The retained test exercises the genesis-build path directly via
  `GenesisConfig::<Test>::build()` on a fresh `TestExternalities`, without the
  removed mock helpers. The four call sites use a byte-identical message
  template, so a regression to the format would affect all four simultaneously.

- Slim the change-file prose to one paragraph; drop the round-2 "future
  hardening" footnote (LGLO R4 — `CNightAddresses → BoundedVec` refactor — is
  a separate follow-up issue).

Round-3 net delta vs PR base (`19f8b0f9`): +112 / -21 across 4 files, down
from round-2's +288 / -46 across 7 files. Behaviour for valid chain specs is
unchanged; the operator-facing panic strings are unchanged for over-length
input.

Round-3 plan: §20–§28 of the work-package plan.
JIRA: https://shielded.atlassian.net/browse/PM-19896

Signed-off-by: Mike Clay <mike.clay@shielded.io>
@m2ux

m2ux commented May 14, 2026

Copy link
Copy Markdown
Contributor Author

The "parsing in two places" framing makes sense — a serde-side BoundedVec with a custom serde(with = "…") adapter would resolve both the policy-id and address sites the same way, and the 118-vs-108 regex/runtime mismatch would fall out naturally once the regex is replaced by a typed parse.

Round 3 (134b4da8) took the smaller-footprint path: the round-2 +288/−46 is now +112/−21, with the mock crate byte-identical to the PR base. The deeper CNightAddresses → BoundedVec rework is captured in the work package's deferred-scope list as a follow-up.

@m2ux m2ux requested a review from Klapeyron May 14, 2026 10:47
LGLO
LGLO previously approved these changes May 18, 2026
m2ux added 2 commits May 18, 2026 14:13
…ke/pm-19896-cnight-genesis-panic-hardening

Signed-off-by: Mike Clay <mike.clay@shielded.io>
Signed-off-by: Mike Clay <mike.clay@shielded.io>
@m2ux

m2ux commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

Blocked on Docker login issue

@m2ux m2ux requested a review from LGLO May 20, 2026 10:30
@m2ux m2ux added this pull request to the merge queue May 20, 2026
Merged via the queue into main with commit 9b9ad26 May 20, 2026
53 of 57 checks passed
@m2ux m2ux deleted the mike/pm-19896-cnight-genesis-panic-hardening branch May 20, 2026 10:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants