Improvments to runtime configuration#265
Conversation
| RuntimeCall::TransactionStorage( | ||
| pallet_transaction_storage::Call::store { .. } | | ||
| pallet_transaction_storage::Call::store_with_cid_config { .. } | | ||
| pallet_transaction_storage::Call::renew { .. } |
There was a problem hiding this comment.
hmm, not sure about this now, maybe we want to allow SC to renew in the future (most probably for renew we will also remove authorization check), but ok we can keep it like this
| RuntimeCall::Utility(utility_call) => { | ||
| for inner in utility_inner_calls(utility_call) { | ||
| validate_storage_calls(who, inner, depth + 1, consume)?; | ||
| } | ||
| Ok(()) | ||
| }, |
There was a problem hiding this comment.
I meant this, the Utility is not storage call and it is handled by validate_inner_calls
| RuntimeCall::Utility(utility_call) => { | |
| for inner in utility_inner_calls(utility_call) { | |
| validate_storage_calls(who, inner, depth + 1, consume)?; | |
| } | |
| Ok(()) | |
| }, |
There was a problem hiding this comment.
I didn't understand the comment ? could you explain a bit more?
Co-authored-by: Branislav Kontur <bkontur@gmail.com>
Co-authored-by: Branislav Kontur <bkontur@gmail.com>
* replacing ipfs-http-client usage * remove unused const
* added ValidTransaction tag for check_signed * Apply suggestion from @bkontur * Fix call to renamed parameter in check_store_renew_unsigned The `hash` parameter was renamed to `content_hash` but the call site was not updated, causing a compilation error. * Unify preimage ValidTransaction tag between signed and unsigned paths Extract preimage_store_renew_valid_transaction helper so both check_store_renew_unsigned and check_signed use the same tag prefix and provides, ensuring the tx pool deduplicates across signed and unsigned submissions of the same pre-authorized content. * Test that signed preimage tags match unsigned preimage tags Extend validate_signed_account_authorization_has_provides_tag to verify that the preimage-authorized signed path produces the same ValidTransaction provides tag as the unsigned path, ensuring the transaction pool deduplicates across both submission types. * Test cross-signer dedup for preimage-authorized content Verify that different signers submitting the same preimage-authorized content produce identical provides tags, confirming dedup is content-based and not signer-based. --------- Co-authored-by: Branislav Kontur <bkontur@gmail.com>
… npm grouping (#276) * Pass scope by reference in check_authorization and check_authorization_expired * Removed unused * ci: add Dependabot npm monitoring for console-ui and examples Group version and security updates per directory so each produces a single PR instead of one per dependency. * fmt :D
* feat: add /format skill for pre-commit formatting checks Add a new Claude skill that runs all formatting and cleaning tasks: - Rust formatting (cargo +nightly fmt) - TOML formatting (taplo) - Zepter feature propagation checks - Clippy linting Update CLAUDE.md to instruct Claude to automatically run /format after generating code and before creating commits. * Update SKILL.md description to include linting * Update .claude/skills/format/SKILL.md Co-authored-by: Rohit Sarpotdar <rohit.sarpotdar@parity.io> --------- Co-authored-by: Andrii <ndk@parity.io> Co-authored-by: Rohit Sarpotdar <rohit.sarpotdar@parity.io>
bulletin-polkadot does not support remark without sudo. Revert the remark to use Sudo.sudo wrapping while keeping authorizationSigner for the store call. Add TODO to revisit with PR #265.
Co-authored-by: Branislav Kontur <bkontur@gmail.com>
| RuntimeCallOf<T>: IsSubType<Call<T>>, | ||
| { | ||
| if depth >= MAX_WRAPPER_DEPTH { | ||
| return true; |
There was a problem hiding this comment.
hmm, this is strange, we reached the max_depth and returning true?
Which means "Returns true if call is a storage-mutating TransactionStorage call" ?
There was a problem hiding this comment.
adding a comment would be fine? or some more explanatory return type? if someone tries to wrap with too many depth levels then returning true will eventually cause the call to be blocked in such case
| } | ||
|
|
||
| /// Maximum recursion depth for inspecting wrapper calls. | ||
| pub const MAX_WRAPPER_DEPTH: u32 = 8; |
There was a problem hiding this comment.
for example for recursion depth/count for xcm we are using something like this https://github.com/paritytech/polkadot-sdk/blob/d5c5fbe6b7752779f75e2bc8db4fea354aefc01a/polkadot/xcm/xcm-builder/src/barriers.rs#L593
There was a problem hiding this comment.
I see, lookint at how environmental! works, its using thread-local global state to track recursions - I believe that's mostly not a good thing unless you must do it for some reason. For this use case here - too complicated
…method (#327) Refactor the standalone `is_storage_mutating_call` function into a provided method on the `CallInspector` trait. This changes the trait parameter from `CallInspector<Call>` to `CallInspector<T: Config>`, enabling the cleaner `Self::is_storage_mutating_call(call, 0)` syntax in runtime `Contains` impls instead of the verbose turbofish `pallet_transaction_storage::is_storage_mutating_call::<Runtime, Self>(call, 0)`.
* Reject wrapped store/renew, keep management call validation Storage-mutating calls (store, store_with_cid_config, renew) must now be submitted as direct extrinsics only — wrapping them in batch, sudo, proxy, etc. is rejected at validation time with InvalidTransaction::Call. Authorization management calls (authorize_*, refresh_*, remove_expired_*) can still be wrapped and are validated via validate_signed as before. This simplifies the extension logic by removing: - preserves_origin tracking from TraverseResult and inspect_wrapper - Mixed-batch rejection logic (store + management in same batch) - Origin transformation for wrappers (no longer needed) - authorized_scope accumulation in the wrapper path The visitor now skips validate_signed for storage-mutating calls found in wrappers (they'll be rejected by the has_storage_mutating check), ensuring the correct InvalidTransaction::Call error is returned regardless of whether the caller has authorization. * simplify
…337) * Avoid cloning RuntimeOrigin in extract_signer Use origin.caller() to match on OriginCaller by reference instead of cloning the entire RuntimeOrigin via into_caller(). Only the AccountId is cloned, which is needed for the return value. * Avoid unnecessary clone of scope in ValidateStorageCalls Move scope into Authorized origin instead of cloning it, since maybe_scope is owned and not used after this point. * Allow sudo(store) on bulletin-westend Remove Sudo from StorageCallInspector on Westend so the sudo key holder can store data via sudo(store) without authorization. Root origin is already accepted by ensure_authorized. Update existing sudo_as tests to reflect that sudo calls now pass validation (failing at dispatch instead when no sudo key is configured). * nit * nit * Remove stale test note about Westend call allowlist
|
@karolk91 my assistant sees (I will check later also): |
1 Seems just doc is outdated - fixed in: 863d37b 4 I think TraverseResult may stay, at is a bit more self-explanatory |
#338) traverse_storage_calls already handles both direct pallet calls (via is_sub_type) and wrapper calls (via inspect_wrapper), so the separate code paths in prepare() were unnecessary.
Summary
Add recursive wrapper call inspection to
ValidateStorageCallsso that storage authorization is enforced even whenstore/store_with_cid_config/reneware nested insideUtility::batch,Proxy::proxy,Sudo::sudo_as, etc. Also harden XCM configuration to block storage-mutating calls over XCM, refactorAllowedSignedCallsinto a single-source-of-truth allowlist, and add extensive test coverage for both runtimes.What changed
1.
CallInspectortrait +TraverseResult(pallets/transaction-storage/src/extension.rs)New trait that lets the runtime tell the pallet extension how to unwrap wrapper calls. The extension recursively walks the call tree and rejects any wrapper containing storage-mutating calls (store/renew must be direct extrinsics). For wrappers containing only management calls (authorize_, refresh_, remove_expired_*), it validates authorization and accumulates
ValidTransactionmetadata for correct mempool deduplication.TraverseResulttracks:found_storage— whether any TransactionStorage pallet calls were found2. Wrapper inspection helpers (
pallets/common/src/lib.rs)Generic helper functions for
pallet-utility,pallet-sudo, andpallet-proxythat extract inner calls from wrapper call variants. ReturnsNonefor non-wrapper variants (e.g.set_key,add_proxy). Exhaustively matches all variants (including__Ignore) to ensure new upstream variants cause compile errors.3.
StorageCallInspector(both runtimes)Runtime-specific
CallInspectorimplementation. Also implementsContains<RuntimeCall>for use as XCMSafeCallFilter.sudo(store)without authorization (Root origin is accepted byensure_authorized)4.
AllowedSignedCallsrefactor (Polkadot runtime)Split into
validate_signed_call()(recursive allowlist + auth checks) and a priority/longevity assignment block. Adds dedicated priority/longevity constants for Sudo, Proxy, Utility, and Upgrade calls (previously they all borrowedBridgeTxLongevity).5. XCM
SafeCallFilter(both runtimes)Changed from
EverythingtoEverythingBut<StorageCallInspector>, blockingstore/store_with_cid_config/renewover XCM (including when wrapped in batch). Authorization management calls (authorize_account, etc.) remain allowed over XCM.What is allowed / rejected
Signed transactions — storage calls
store/renewwith authorizationAuthorizedstore/renewwithout authorizationPayment)batch([store, ...])with or without authCall)sudo(store)(Westend only, sudo key holder)ensure_authorizedsudo(store)/sudo_as(store)(Polkadot)Call)proxy(store)(Polkadot)Call)batch_all([authorize_account])by Authorizerbatch([authorize_account])by non-AuthorizerMAX_WRAPPER_DEPTH(8)ExhaustsResources)XCM Transact
store/store_with_cid_config/renewSafeCallFilterbatch([store])authorize_account/authorize_preimagePolkadot solochain signed call allowlist
Allowed:
TransactionStorage::*,Session::set_keys/purge_keys, bridge calls (submit_finality_proof,submit_parachain_heads,receive_messages_proof/delivery_proof),Sudo::*,Proxy::*,Utility::*(inner calls recursively validated),System::apply_authorized_upgrade.Everything else (e.g.,
System::remark) is rejected withInvalidTransaction::Call, both direct and wrapped.Design: store/renew must be direct extrinsics
The
ValidateStorageCallsextension rejects store/renew inside any wrapper. This is because:Origin::Authorized { who, scope }so thatstore/renewdispatches passensure_authorizedAuthorized, but mixing storage + non-storage calls in a batch creates an origin conflict (AuthorizedvsSigned)Authorizedorigin at dispatchException: On Westend, sudo is not inspected.
sudo(store)works because sudo dispatches with Root origin, whichensure_authorizedaccepts directly (noAuthorizedorigin needed).Test coverage
~1250 new lines of tests across both runtimes covering:
sudo(store)works for sudo key holder on Westend