-
Notifications
You must be signed in to change notification settings - Fork 248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Static Decoding of Signed Extensions: Simple Approach #1235
Changes from 23 commits
c0226dc
ffec0b1
b0fb96a
c87f165
4ed554d
460bfaf
ea59e6c
3dad997
fe708e7
0eabab9
58fd654
1dd8f53
64b5c90
7d8447e
fe1db8c
8c93d5b
bba2c4b
2692882
0d52317
7ff7552
f200d0f
5afe150
42a0423
b7d9411
13e3f2d
39f1a30
cb65208
d50519d
1dc9976
3a79e70
0876c28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,8 @@ use crate::{ | |
Metadata, | ||
}; | ||
|
||
use crate::config::signed_extensions::{ChargeAssetTxPayment, ChargeTransactionPayment}; | ||
use crate::config::SignedExtension; | ||
use crate::dynamic::DecodedValue; | ||
use crate::utils::strip_compact_prefix; | ||
use codec::{Compact, Decode}; | ||
|
@@ -366,12 +368,13 @@ where | |
} | ||
|
||
/// Returns `None` if the extrinsic is not signed. | ||
pub fn signed_extensions(&self) -> Option<ExtrinsicSignedExtensions<'_>> { | ||
pub fn signed_extensions(&self) -> Option<ExtrinsicSignedExtensions<'_, T>> { | ||
let signed = self.signed_details.as_ref()?; | ||
let extra_bytes = &self.bytes[signed.signature_end_idx..signed.extra_end_idx]; | ||
Some(ExtrinsicSignedExtensions { | ||
bytes: extra_bytes, | ||
metadata: &self.metadata, | ||
_marker: std::marker::PhantomData, | ||
}) | ||
} | ||
|
||
|
@@ -605,21 +608,22 @@ impl<T: Config> ExtrinsicEvents<T> { | |
|
||
/// The signed extensions of an extrinsic. | ||
#[derive(Debug, Clone)] | ||
pub struct ExtrinsicSignedExtensions<'a> { | ||
pub struct ExtrinsicSignedExtensions<'a, T: Config> { | ||
bytes: &'a [u8], | ||
metadata: &'a Metadata, | ||
_marker: std::marker::PhantomData<T>, | ||
} | ||
|
||
impl<'a> ExtrinsicSignedExtensions<'a> { | ||
impl<'a, T: Config> ExtrinsicSignedExtensions<'a, T> { | ||
/// Returns an iterator over each of the signed extension details of the extrinsic. | ||
/// If the decoding of any signed extension fails, an error item is yielded and the iterator stops. | ||
pub fn iter(&self) -> impl Iterator<Item = Result<ExtrinsicSignedExtension<'a>, Error>> { | ||
pub fn iter(&self) -> impl Iterator<Item = Result<ExtrinsicSignedExtension<T>, Error>> { | ||
let signed_extension_types = self.metadata.extrinsic().signed_extensions(); | ||
let num_signed_extensions = signed_extension_types.len(); | ||
let bytes = self.bytes; | ||
let metadata = self.metadata; | ||
let mut index = 0; | ||
let mut byte_start_idx = 0; | ||
let metadata = &self.metadata; | ||
|
||
std::iter::from_fn(move || { | ||
if index == num_signed_extensions { | ||
|
@@ -649,25 +653,38 @@ impl<'a> ExtrinsicSignedExtensions<'a> { | |
ty_id, | ||
identifier: extension.identifier(), | ||
metadata, | ||
_marker: std::marker::PhantomData, | ||
})) | ||
}) | ||
} | ||
|
||
fn find_by_name(&self, name: &str) -> Option<ExtrinsicSignedExtension<'_, T>> { | ||
let signed_extension = self | ||
.iter() | ||
.find_map(|e| e.ok().filter(|e| e.name() == name))?; | ||
Some(signed_extension) | ||
} | ||
|
||
/// Searches through all signed extensions to find a specific one. | ||
/// If the Signed Extension is found, but decoding failed, `Some(Err(err))` is returned. | ||
pub fn find<S: SignedExtension<T>>(&self) -> Option<Result<S::Decoded, Error>> { | ||
let signed_extension = self.find_by_name(S::NAME)?; | ||
signed_extension.as_signed_extra::<S>().transpose() | ||
} | ||
|
||
/// The tip of an extrinsic, extracted from the ChargeTransactionPayment or ChargeAssetTxPayment | ||
/// signed extension, depending on which is present. | ||
/// | ||
/// Returns `None` if `tip` was not found or decoding failed. | ||
pub fn tip(&self) -> Option<u128> { | ||
let tip = self.iter().find_map(|e| { | ||
e.ok().filter(|e| { | ||
e.name() == "ChargeTransactionPayment" || e.name() == "ChargeAssetTxPayment" | ||
}) | ||
})?; | ||
|
||
// Note: ChargeAssetTxPayment might have addition information in it (asset_id). | ||
// But both should start with a compact encoded u128, so this decoding is fine. | ||
let tip = Compact::<u128>::decode(&mut tip.bytes()).ok()?.0; | ||
Some(tip) | ||
// Note: the overhead of iterating twice should be negligible. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could probably modify the |
||
self.find::<ChargeTransactionPayment>() | ||
.map(|e| e.map(|e| e.tip())) | ||
.or_else(|| { | ||
self.find::<ChargeAssetTxPayment>() | ||
.map(|e| e.map(|e| e.tip())) | ||
})? | ||
.ok() | ||
} | ||
|
||
/// The nonce of the account that submitted the extrinsic, extracted from the CheckNonce signed extension. | ||
|
@@ -684,14 +701,15 @@ impl<'a> ExtrinsicSignedExtensions<'a> { | |
|
||
/// A single signed extension | ||
#[derive(Debug, Clone)] | ||
pub struct ExtrinsicSignedExtension<'a> { | ||
pub struct ExtrinsicSignedExtension<'a, T: Config> { | ||
bytes: &'a [u8], | ||
ty_id: u32, | ||
identifier: &'a str, | ||
metadata: &'a Metadata, | ||
_marker: std::marker::PhantomData<T>, | ||
} | ||
|
||
impl<'a> ExtrinsicSignedExtension<'a> { | ||
impl<'a, T: Config> ExtrinsicSignedExtension<'a, T> { | ||
/// The bytes representing this signed extension. | ||
pub fn bytes(&self) -> &'a [u8] { | ||
self.bytes | ||
|
@@ -709,11 +727,23 @@ impl<'a> ExtrinsicSignedExtension<'a> { | |
|
||
/// Signed Extension as a [`scale_value::Value`] | ||
pub fn value(&self) -> Result<DecodedValue, Error> { | ||
let value = | ||
DecodedValue::decode_as_type(&mut &self.bytes[..], self.ty_id, self.metadata.types())?; | ||
self.as_type() | ||
} | ||
|
||
/// Decodes the `extra` bytes of this Signed Extension into a static type. | ||
fn as_type<E: DecodeAsType>(&self) -> Result<E, Error> { | ||
let value = E::decode_as_type(&mut &self.bytes[..], self.ty_id, self.metadata.types())?; | ||
Ok(value) | ||
} | ||
|
||
/// Decodes the `extra` bytes of this Signed Extension into its associated `Decoded` type. | ||
/// Returns `Ok(None)` if the identitfier of this Signed Extension object does not line up with the `NAME` constant of the provided Signed Extension type. | ||
pub fn as_signed_extra<S: SignedExtension<T>>(&self) -> Result<Option<S::Decoded>, Error> { | ||
if self.identifier != S::NAME { | ||
return Ok(None); | ||
} | ||
self.as_type::<S::Decoded>().map(Some) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,8 +5,10 @@ | |
use crate::{test_context, utils::node_runtime}; | ||
use codec::{Compact, Encode}; | ||
use futures::StreamExt; | ||
|
||
use subxt::config::signed_extensions::{ChargeAssetTxPayment, CheckMortality, CheckNonce}; | ||
use subxt::config::DefaultExtrinsicParamsBuilder; | ||
use subxt::config::SubstrateConfig; | ||
use subxt::utils::Era; | ||
use subxt_metadata::Metadata; | ||
use subxt_signer::sr25519::dev; | ||
|
||
|
@@ -272,38 +274,64 @@ async fn decode_signed_extensions_from_blocks() { | |
}}; | ||
} | ||
|
||
let expected_signed_extensions = [ | ||
"CheckNonZeroSender", | ||
"CheckSpecVersion", | ||
"CheckTxVersion", | ||
"CheckGenesis", | ||
"CheckMortality", | ||
"CheckNonce", | ||
"CheckWeight", | ||
"ChargeAssetTxPayment", | ||
]; | ||
|
||
let transaction1 = submit_transfer_extrinsic_and_get_it_back!(1234); | ||
let extensions1 = transaction1.signed_extensions().unwrap(); | ||
let nonce1 = extensions1.nonce().unwrap(); | ||
let nonce1_static = extensions1.find::<CheckNonce>().unwrap().unwrap().0; | ||
let tip1 = extensions1.tip().unwrap(); | ||
let tip1_static: u128 = extensions1 | ||
.find::<ChargeAssetTxPayment>() | ||
.unwrap() | ||
.unwrap() | ||
.tip(); | ||
|
||
let transaction2 = submit_transfer_extrinsic_and_get_it_back!(5678); | ||
let extensions2 = transaction2.signed_extensions().unwrap(); | ||
let nonce2 = extensions2.nonce().unwrap(); | ||
let nonce2_static = extensions2.find::<CheckNonce>().unwrap().unwrap().0; | ||
let tip2 = extensions2.tip().unwrap(); | ||
let tip2_static: u128 = extensions2 | ||
.find::<ChargeAssetTxPayment>() | ||
.unwrap() | ||
.unwrap() | ||
.tip(); | ||
|
||
assert_eq!(nonce1, 0); | ||
assert_eq!(nonce1, nonce1_static); | ||
assert_eq!(tip1, 1234); | ||
assert_eq!(tip1, tip1_static); | ||
assert_eq!(nonce2, 1); | ||
assert_eq!(nonce2, nonce2_static); | ||
assert_eq!(tip2, 5678); | ||
assert_eq!(tip2, tip2_static); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it'd be really good I think to also test getting the mortality (just because that returns an enum and would be good to check that it decodes OK! I'm not sure off the top of my head whether the encoded impl is consistent with this or not)) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a check at the end of the test to verify Mortality decodes and is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, thank you! That's reassuring :) |
||
|
||
let expected_signed_extensions = [ | ||
"CheckNonZeroSender", | ||
"CheckSpecVersion", | ||
"CheckTxVersion", | ||
"CheckGenesis", | ||
"CheckMortality", | ||
"CheckNonce", | ||
"CheckWeight", | ||
"ChargeAssetTxPayment", | ||
]; | ||
|
||
assert_eq!(extensions1.iter().count(), expected_signed_extensions.len()); | ||
for (e, expected_name) in extensions1.iter().zip(expected_signed_extensions.iter()) { | ||
assert_eq!(e.unwrap().name(), *expected_name); | ||
} | ||
|
||
assert_eq!(extensions2.iter().count(), expected_signed_extensions.len()); | ||
for (e, expected_name) in extensions2.iter().zip(expected_signed_extensions.iter()) { | ||
assert_eq!(e.unwrap().name(), *expected_name); | ||
} | ||
|
||
// check that era decodes: | ||
for extensions in [&extensions1, &extensions2] { | ||
let era: Era = extensions | ||
.find::<CheckMortality<SubstrateConfig>>() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a shame to have to write the (We can probably come up with something to avoid this at some point, but all good for now!) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. |
||
.unwrap() | ||
.unwrap(); | ||
assert_eq!(era, Era::Immortal) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Maybe this should be
Result<Option<..>
instead, actually? I'm never sure; just looking at some of the otherfind
methods we have around :)