Skip to content

feat: support custom transaction extension values - #2273

Open
DenzelPenzel wants to merge 5 commits into
masterfrom
denzelpenzel/custom-transaction-extension-values
Open

feat: support custom transaction extension values#2273
DenzelPenzel wants to merge 5 commits into
masterfrom
denzelpenzel/custom-transaction-extension-values

Conversation

@DenzelPenzel

@DenzelPenzel DenzelPenzel commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Allow callers to provide metadata-aware values for custom transaction extensions without implementing a new Config:

let params = PolkadotExtrinsicParamsBuilder::new()
    .custom_extension("RestrictOrigins", true)
    .build();

The value is encoded against the extension type from runtime metadata. This fixes signing on chains such as Paseo Asset Hub, where RestrictOrigins contains a bare bool.

Fixes #2265.

Implementation

  • Preserve known default extension behavior and parameter access.
  • Encode custom values using runtime metadata.
  • Reject duplicate names, overrides of known extensions, and non-empty implicit data.
  • Restore the output buffer on encoding errors.

Breaking changes

DefaultTransactionExtensions<T> and its Params type changed from public tuple aliases to structs with private fields, so that custom extension values can be carried alongside the known ones. The tuples live on under new names: KnownDefaultTransactionExtensions<T> and KnownDefaultExtrinsicParams<T>.

Code which configures transactions via the builder and passes the params through opaquely is source-compatible and produces byte-identical transactions. Code which relied on the tuple shape of the params migrates as follows:

Before After
constructing the params tuple manually DefaultExtrinsicParams::from_known(tuple)
mutating an element, e.g. params.5 = ... params.known_mut().5 = ...
reading an element, e.g. &params.7 &params.known().7

Given the above, this should land in 0.51 rather than a 0.50.x patch.

Notes

@DenzelPenzel
DenzelPenzel force-pushed the denzelpenzel/custom-transaction-extension-values branch 3 times, most recently from 9d1ea0b to ce9a555 Compare August 14, 2026 14:56
@DenzelPenzel
DenzelPenzel marked this pull request as ready for review August 14, 2026 15:39
@DenzelPenzel
DenzelPenzel requested a review from lexnv August 14, 2026 15:39
/// specific transaction.
pub struct DefaultTransactionExtensions<T: Config> {
known: KnownDefaultTransactionExtensions<T>,
custom: BTreeMap<String, CustomTransactionExtensionValue>,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If we misspell the extension name we can now produce a wrongly singed extrinsic

This takes an arbitrary string, maybe we can easily compare against metadata.extrinsic().transaction_extensions_to_use_for_encoding()?

@DenzelPenzel DenzelPenzel Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch – a typo silently dropped the extension. DefaultTransactionExtensions::new now validates each custom name against metadata.extrinsic().transaction_extensions_to_use_for_encoding() and errors with "Custom transaction extension '{name}' is not present in the runtime metadata" if it is absent. Covered by custom_extension_absent_from_metadata_is_rejected


fn is_authorization_extension(&self, name: &str) -> bool {
frame_decode::extrinsics::TransactionExtensions::is_authorization_extension(
&self.known,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: This forwards only to known entries, so chain specific exntesions return false here? Would be worth having a subxt issue about this one to tackle later

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Filed #2276 for this, and left a comment on is_authorization_extension pointing at it.

Worth noting it is not purely cosmetic: in V5 the signer payload excludes the last authorization extension and everything before it, so a chain declaring its own authorization extension and supplying it via custom_extension would sign over bytes the runtime excludes. Latent for now since VerifyMultiSignature is the only authorization extension Subxt knows about.

custom: Vec<(String, CustomTransactionExtensionValue)>,
}

enum CustomTransactionExtensionValue {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we plan to extend this? Maybe its easier to colapse and use directly Value?

@DenzelPenzel DenzelPenzel Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No plans to extend it, collapsed to Value directly, so the maps are now BTreeMap<String, Value> / Vec<(String, Value)>.

Comment thread subxt/src/config/default_transaction_extensions.rs Outdated
Comment thread subxt/src/config/default_transaction_extensions.rs
Comment thread subxt/src/config/default_transaction_extensions.rs Outdated
Comment thread subxt/src/config/default_transaction_extensions.rs

@lexnv lexnv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The shape looks good, just some tiny bits and pieces!

We could probably add some more V5 tests (ie around authorization_extension_check_is_forwarded and is_authorization_extension and probably some tests with a custom extension positioned before and after VerifyMultiSignature)

@DenzelPenzel

DenzelPenzel commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Added the V5 coverage you asked for. Since polkadot_metadata_small.scale only declares v4 and has no VerifyMultiSignature, there is a new v5_encoding_info() helper laying out [CheckWeight, VerifyMultiSignature, WeightReclaim] – a custom extension either side of the authorization extension:

  • v5_payload_excludes_custom_extension_before_authorization_extension – flipping CheckWeight leaves the signer payload hash unchanged, since it precedes VerifyMultiSignature.
  • v5_payload_includes_custom_extension_after_authorization_extension – flipping WeightReclaim does change it, since it follows.
  • v5_general_extrinsic_includes_custom_extensions_either_side_of_authorization – the extrinsic itself carries every extension value regardless of position, unlike the payload.
  • authorization_extension_check_is_forwarded also now asserts false for a known non-authorization extension (CheckNonce) and for a name not held at all.

The is_authorization_extension gap for chain-specific extensions is tracked in #2276.

@DenzelPenzel
DenzelPenzel requested a review from a team as a code owner September 3, 2026 12:57
Allow callers to provide metadata-aware values for custom transaction
extensions without implementing a new `Config`. The value is encoded
against the extension type from runtime metadata, which fixes signing
on chains such as Paseo Asset Hub.

`DefaultTransactionExtensions<T>` and its `Params` become structs with
private fields, so custom values can travel alongside the known ones.
`known()`, `known_mut()` and `custom()` preserve access to them, and
the tuples live on as `KnownDefaultTransactionExtensions<T>` and
`KnownDefaultExtrinsicParams<T>`.
A custom extension name the runtime does not declare was silently
ignored, so a typo produced a transaction signed without the extension
the caller asked for. Reject it instead, naming the extension.

Tests move to `CheckWeight` and `WeightReclaim`, which the bundled
metadata actually declares.
@DenzelPenzel
DenzelPenzel force-pushed the denzelpenzel/custom-transaction-extension-values branch from f6cd585 to 62050a4 Compare September 3, 2026 13:25
A V5 extrinsic carries every extension value, including custom ones on
either side of the authorization extension. Pin those bytes, and record
why a custom extension can never report itself as an authorization
extension.
@DenzelPenzel
DenzelPenzel force-pushed the denzelpenzel/custom-transaction-extension-values branch from 62050a4 to 882503c Compare September 3, 2026 13:40
@DenzelPenzel
DenzelPenzel requested a review from lexnv September 3, 2026 14:15
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.

Cannot sign for a chain with a custom transaction extension without defining a whole new Config

2 participants