Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Explicitly note that existing AccountIdConversion is truncating and add fallible try_into... #10719

Merged
merged 13 commits into from
May 17, 2022
4 changes: 2 additions & 2 deletions frame/bounties/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -765,14 +765,14 @@ impl<T: Config> Pallet<T> {
/// This actually does computation. If you need to keep using it, then make sure you cache the
/// value and only call this once.
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account()
T::PalletId::get().into_account_truncating()
}

/// The account ID of a bounty account
pub fn bounty_account_id(id: BountyIndex) -> T::AccountId {
// only use two byte prefix to support 16 byte account id (used by test)
// "modl" ++ "py/trsry" ++ "bt" is 14 bytes, and two bytes remaining for bounty index
T::PalletId::get().into_sub_account(("bt", id))
T::PalletId::get().into_sub_account_truncating(("bt", id))
}

fn create_bounty(
Expand Down
2 changes: 1 addition & 1 deletion frame/child-bounties/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ impl<T: Config> Pallet<T> {
// This function is taken from the parent (bounties) pallet, but the
// prefix is changed to have different AccountId when the index of
// parent and child is same.
T::PalletId::get().into_sub_account(("cb", id))
T::PalletId::get().into_sub_account_truncating(("cb", id))
}

fn create_child_bounty(
Expand Down
2 changes: 1 addition & 1 deletion frame/lottery/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ impl<T: Config> Pallet<T> {
/// This actually does computation. If you need to keep using it, then make sure you cache the
/// value and only call this once.
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account()
T::PalletId::get().into_account_truncating()
}

/// Return the pot account and amount of money in the pot.
Expand Down
4 changes: 2 additions & 2 deletions frame/society/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1732,15 +1732,15 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// This actually does computation. If you need to keep using it, then make sure you cache the
/// value and only call this once.
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account()
T::PalletId::get().into_account_truncating()
}

/// The account ID of the payouts pot. This is where payouts are made from.
///
/// This actually does computation. If you need to keep using it, then make sure you cache the
/// value and only call this once.
pub fn payouts() -> T::AccountId {
T::PalletId::get().into_sub_account(b"payouts")
T::PalletId::get().into_sub_account_truncating(b"payouts")
}

/// Return the duration of the lock, in blocks, with the given number of members.
Expand Down
2 changes: 1 addition & 1 deletion frame/tips/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ impl<T: Config> Pallet<T> {
/// This actually does computation. If you need to keep using it, then make sure you cache the
/// value and only call this once.
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account()
T::PalletId::get().into_account_truncating()
}

/// Given a mutable reference to an `OpenTip`, insert the tip into it and check whether it
Expand Down
2 changes: 1 addition & 1 deletion frame/treasury/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// This actually does computation. If you need to keep using it, then make sure you cache the
/// value and only call this once.
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account()
T::PalletId::get().into_account_truncating()
}

/// The needed bond for a proposal whose spend is `value`.
Expand Down
91 changes: 77 additions & 14 deletions primitives/runtime/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1252,38 +1252,71 @@ impl<'a> codec::Input for TrailingZeroInput<'a> {

/// This type can be converted into and possibly from an AccountId (which itself is generic).
pub trait AccountIdConversion<AccountId>: Sized {
/// Convert into an account ID. This is infallible.
fn into_account(&self) -> AccountId {
self.into_sub_account(&())
/// Convert into an account ID. This is infallible, and may truncate bytes to provide a result.
/// This may lead to duplicate accounts if the size of `AccountId` is less than the seed.
fn into_account_truncating(&self) -> AccountId {
self.into_sub_account_truncating(&())
}

/// Convert into an account ID, checking that all bytes of the seed are being used in the final
/// `AccountId` generated. If any bytes are dropped, this returns `None`.
fn try_into_account(&self) -> Option<AccountId> {
self.try_into_sub_account(&())
}

/// Try to convert an account ID into this type. Might not succeed.
fn try_from_account(a: &AccountId) -> Option<Self> {
Self::try_from_sub_account::<()>(a).map(|x| x.0)
}

/// Convert this value amalgamated with the a secondary "sub" value into an account ID. This is
/// infallible.
/// Convert this value amalgamated with the a secondary "sub" value into an account ID,
/// truncating any unused bytes. This is infallible.
///
/// NOTE: The account IDs from this and from `into_account` are *not* guaranteed to be distinct
/// for any given value of `self`, nor are different invocations to this with different types
/// `T`. For example, the following will all encode to the same account ID value:
/// - `self.into_sub_account(0u32)`
/// - `self.into_sub_account(vec![0u8; 0])`
/// - `self.into_account()`
fn into_sub_account<S: Encode>(&self, sub: S) -> AccountId;
///
/// Also, if the seed provided to this function is greater than the number of bytes which fit
/// into this `AccountId` type, then it will lead to truncation of the seed, and potentially
/// non-unique accounts.
fn into_sub_account_truncating<S: Encode>(&self, sub: S) -> AccountId;

/// Same as `into_sub_account`, but ensuring that all bytes of the account's seed are used when
/// generating an account. This can help guarantee that different accounts are unique, besides
/// types which encode the same as noted above.
fn try_into_sub_account<S: Encode>(&self, sub: S) -> Option<AccountId>;

/// Try to convert an account ID into this type. Might not succeed.
fn try_from_sub_account<S: Decode>(x: &AccountId) -> Option<(Self, S)>;
}

/// Format is TYPE_ID ++ encode(parachain ID) ++ 00.... where 00... is indefinite trailing zeroes to
/// Format is TYPE_ID ++ encode(sub-seed) ++ 00.... where 00... is indefinite trailing zeroes to
/// fill AccountId.
impl<T: Encode + Decode, Id: Encode + Decode + TypeId> AccountIdConversion<T> for Id {
fn into_sub_account<S: Encode>(&self, sub: S) -> T {
impl<T: Encode + Decode + MaxEncodedLen, Id: Encode + Decode + TypeId> AccountIdConversion<T>
for Id
{
// Take the `sub` seed, and put as much of it as possible into the generated account, but
// allowing truncation of the seed if it would not fit into the account id in full. This can
// lead to two different `sub` seeds with the same account generated.
fn into_sub_account_truncating<S: Encode>(&self, sub: S) -> T {
(Id::TYPE_ID, self, sub)
.using_encoded(|b| T::decode(&mut TrailingZeroInput(b)))
.expect("`AccountId` type is never greater than 32 bytes; qed")
.expect("All byte sequences are valid `AccountIds`; qed")
}

// Same as `into_sub_account_truncating`, but returns `None` if any bytes would be truncated.
fn try_into_sub_account<S: Encode>(&self, sub: S) -> Option<T> {
(Id::TYPE_ID, self, sub).using_encoded(|b| {
if b.len() > T::max_encoded_len() {
return None
};
let account = T::decode(&mut TrailingZeroInput(b))
Copy link
Member

Choose a reason for hiding this comment

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

IMO the check above is wrong, because if you for example have an enum max_encoded_len returns you the length of the biggest enum variant. This is wrong here, just imagine you are decoding the shortest variant and then you would not have consumed all bytes.

If you want to make it correct, you need to extend TrailingZeroInput to be able to check if the given input was consumed completely after having the type decoded.

Copy link
Member

Choose a reason for hiding this comment

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

The AccountId type has constant length, but in general I see that it does not work for all T.
So my proposal of adding a const_encoded_len could also fix this, right?

Copy link
Member

Choose a reason for hiding this comment

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

How do you know that the AccountId has a constant length? AccountId is some generic value that could be anything (one of the reasons for this pr). Just because Polkadot uses AccountId32, it doesn't mean that everyone uses this or is required to use this.

Copy link
Member

@ggwpez ggwpez Feb 21, 2022

Choose a reason for hiding this comment

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

Yes therefore we could add a FixEncodedLen trait analogous to MaxEncodedLen that requires a type to always have the same encoding len.
PS: Probably still better to make it work even for non FixEncodedLen types 😄

Copy link
Contributor

@kianenigma kianenigma May 3, 2022

Choose a reason for hiding this comment

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

+1 using MaxEncodedLen is IMO also wrong. Ideally, you just decode, and check how many bytes where actually used for decoding.

Maybe one trick here could be to decode, then re-encode, and see if we go back to the same bytes. A bit hacky and also assumes reversible encode/decode.

Copy link
Member Author

Choose a reason for hiding this comment

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

I did something like what @kianenigma suggested. I check the length of the bytes used for the seed, and check that the length of bytes of the account is greater or equal to, otherwise, we know some bytes were truncated.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks. I also tried my luck on this but could not create a test that the function actually returns Some(). The AccountId type is u64, so an overflow is very likely anyway.

Copy link
Member Author

Choose a reason for hiding this comment

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

You can just create a mock environment where account id is 32 bytes?

.expect("All byte sequences are valid `AccountIds`; qed");
return Some(account)
})
}

fn try_from_sub_account<S: Decode>(x: &T) -> Option<(Self, S)> {
Expand Down Expand Up @@ -1655,6 +1688,13 @@ mod tests {
let _ = s.verify(&[0u8; 100][..], &Public::unchecked_from([0; 32]));
}

#[derive(Encode, Decode, Default, PartialEq, Debug)]
struct U128Value(u128);
impl super::TypeId for U128Value {
const TYPE_ID: [u8; 4] = [0x0d, 0xf0, 0x0d, 0xf0];
}
// f00df00d

#[derive(Encode, Decode, Default, PartialEq, Debug)]
struct U32Value(u32);
impl super::TypeId for U32Value {
Expand All @@ -1672,23 +1712,46 @@ mod tests {
type AccountId = u64;

#[test]
fn into_account_should_work() {
let r: AccountId = U32Value::into_account(&U32Value(0xdeadbeef));
fn into_account_truncating_should_work() {
let r: AccountId = U32Value::into_account_truncating(&U32Value(0xdeadbeef));
assert_eq!(r, 0x_deadbeef_cafef00d);
}

#[test]
fn try_into_account_should_work() {
let r: AccountId = U32Value::try_into_account(&U32Value(0xdeadbeef)).unwrap();
assert_eq!(r, 0x_deadbeef_cafef00d);

// u128 is bigger than u64 would fit
let maybe: Option<AccountId> = U128Value::try_into_account(&U128Value(u128::MAX));
assert!(maybe.is_none());
}

#[test]
fn try_from_account_should_work() {
let r = U32Value::try_from_account(&0x_deadbeef_cafef00d_u64);
assert_eq!(r.unwrap(), U32Value(0xdeadbeef));
}

#[test]
fn into_account_with_fill_should_work() {
let r: AccountId = U16Value::into_account(&U16Value(0xc0da));
fn into_account_truncating_with_fill_should_work() {
let r: AccountId = U16Value::into_account_truncating(&U16Value(0xc0da));
assert_eq!(r, 0x_0000_c0da_f00dcafe);
}

#[test]
fn try_into_sub_account_should_work() {
let r: AccountId = U16Value::try_into_account(&U16Value(0xc0da)).unwrap();
assert_eq!(r, 0x_0000_c0da_f00dcafe);

let maybe: Option<AccountId> = U16Value::try_into_sub_account(
&U16Value(0xc0da),
"a really large amount of additional encoded information which will certainly overflow the account id type ;)"
);

assert!(maybe.is_none())
}

#[test]
fn try_from_account_with_fill_should_work() {
let r = U16Value::try_from_account(&0x0000_c0da_f00dcafe_u64);
Expand Down
2 changes: 1 addition & 1 deletion utils/frame/frame-utilities-cli/src/pallet_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ impl PalletIdCmd {
"Cannot convert argument to palletid: argument should be 8-character string"
})?;

let account_id: R::AccountId = PalletId(id_fixed_array).into_account();
let account_id: R::AccountId = PalletId(id_fixed_array).into_account_truncating();

with_crypto_scheme!(
self.crypto_scheme.scheme,
Expand Down