api: [Bug] Runtime API Call Decoding incorrect with Custom Types

  • I’m submitting a …

    • Bug report
    • Feature request
    • Support request
    • Other
  • What is the current behavior and expected behavior?

Current: In our case, calling a custom RPC and its runtime API clone with the same input value does not result in the same return value: One field is decoded incorrectly. The RPC returns the right value and encodes the amount to a number, while the runtime API returns a different account (probably a double encoding), and a HEX-encoded value for the deposit amount.

struct Deposit {
   owner: AccountId,
   amount: u128
}

❌ Runtime Api: didApi.queryByW3n("williamfreude")

owner: 4nv4phaiCRnr6JSfhawcDZp7LzmvtWvabyCxkML7NPrr8eHV
amount: 8,589,934,592,000,000,154,110,983

✅ DID RPC: did.queryByW3n("williamfreude")

owner: 4qTuS5ymocwsPJ3uK5FKG7d2nVhV3CawDMwhWyKz2FBXq2gX
amount: 2,000,000,000,000,000

We tried out numerous varations, with and without custom serde (de-) serialization. Nothing worked until we used minimal custom type declarations, see below.

Expected

When calling the exact same method with the same input and same custom type declarations, it should lead to the same result.

We fixed the issue for the runtime API call by referring to augmented api types, see this PR for changing our custom type declaration However, this augmented type definition matches exactly the custom one which we provided:

Augmented

  interface DidDidDetails extends Struct {
    readonly authenticationKey: H256;
    readonly keyAgreementKeys: BTreeSet<H256>;
    readonly delegationKey: Option<H256>;
    readonly attestationKey: Option<H256>;
    readonly publicKeys: BTreeMap<H256, DidDidDetailsDidPublicKeyDetails>;
    readonly lastTxCounter: u64;
    readonly deposit: KiltSupportDeposit;
  }

Custom

export interface RawDidDetails extends Struct {
  readonly authenticationKey: Hash;
  readonly keyAgreementKeys: BTreeSet<Hash>;
  readonly delegationKey: Option<Hash>;
  readonly attestationKey: Option<Hash>;
  readonly publicKeys: BTreeMap<Hash, DidDidDetailsDidPublicKeyDetails>;
  readonly lastTxCounter: BlockNumber;
  readonly deposit: KiltSupportDeposit;
}
  • What is the motivation for changing the behavior?

Others shouldn’t need to spend time debugging this unexpected behavior in the future.

  • Please tell us about your environment:

I tested both in the Polkadot Apps as well as a custom (Type)script.

  • Version: 9.4.1 and 9.6.1

  • Environment:

    • Node.js (node v16.13.2 (npm v8.3.1))
    • Browser (Polkadot Apps apps v0.121.2-118-x and Chrome 106.0.5249.119. Both also with earlier versions, ca. 2 week range)
    • Other (limited support for other environments)
  • Language:

    • JavaScript
    • TypeScript 4.8.3
    • Other: Polkadot Apps

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 23 (13 by maintainers)

Commits related to this issue

Most upvoted comments

Only runtimes 9 & 10 have BlockNumber: 'u64' overrides in https://github.com/KILTprotocol/sdk-js/tree/develop/packages/type-definitions/src

(Latest, looking at the imports and following the trail, never apply any such override)

TL;DR We are still looking at mismatched types here as supplied - the API will decode based on what it is fed and for anything RPC or runtime call related, it needs to have all the types, as used, correctly specified (at least for runtime calls that pain will go away at some point in the future once it gets into the metadata… hopefully…)

Let’s focus on the topic at-hand after my little outbursts 😃

Anyway, looking at Augmented/Custom above -

interface DidDidDetails extends Struct {
    readonly authenticationKey: H256;
    readonly keyAgreementKeys: BTreeSet<H256>;
    readonly delegationKey: Option<H256>;
    readonly attestationKey: Option<H256>;
    readonly publicKeys: BTreeMap<H256, DidDidDetailsDidPublicKeyDetails>;
    readonly lastTxCounter: u64;
    readonly deposit: KiltSupportDeposit;
  }

export interface RawDidDetails extends Struct {
  readonly authenticationKey: Hash;
  readonly keyAgreementKeys: BTreeSet<Hash>;
  readonly delegationKey: Option<Hash>;
  readonly attestationKey: Option<Hash>;
  readonly publicKeys: BTreeMap<Hash, DidDidDetailsDidPublicKeyDetails>;
  readonly lastTxCounter: BlockNumber;
  readonly deposit: KiltSupportDeposit;
}
  • Hash === H256 so all ok
  • u64 !== BlockNumber (lastTxCounter field) the latter is u32, see

image

Obviously is decoding via runtime, it really needs to match - in JSON you can get away with mismatched types since the input comes in a text, not binary

So it would seem that if you swapped your runtime to the first version that the field type was actually incorrect, and correct with the replaced version.

And indeed, looking at your Rust types, it needs to be u64, not BlockNumber, aka

pub struct DidDetails<T: Config> {
	/// The ID of the authentication key, used to authenticate DID-related
	/// operations.
	pub authentication_key: KeyIdOf<T>,
	/// The set of the key agreement key IDs, which can be used to encrypt
	/// data addressed to the DID subject.
	pub key_agreement_keys: DidKeyAgreementKeySet<T>,
	/// \[OPTIONAL\] The ID of the delegation key, used to verify the
	/// signatures of the delegations created by the DID subject.
	pub delegation_key: Option<KeyIdOf<T>>,
	/// \[OPTIONAL\] The ID of the attestation key, used to verify the
	/// signatures of the attestations created by the DID subject.
	pub attestation_key: Option<KeyIdOf<T>>,
	/// The map of public keys, with the key label as
	/// the key map and the tuple (key, addition_block_number) as the map
	/// value.
	/// The map includes all the keys under the control of the DID subject,
	/// including the ones currently used for authentication, key agreement,
	/// attestation, and delegation. Other than those, the map also contains
	/// the old attestation keys that have been rotated, i.e., they cannot
	/// be used to create new attestations but can still be used to verify
	/// previously issued attestations.
	pub public_keys: DidPublicKeyMap<T>,
	/// The counter used to avoid replay attacks, which is checked and
	/// updated upon each DID operation involving with the subject as the
	/// creator.
	pub last_tx_counter: u64,
	/// The deposit that was taken to incentivise fair use of the on chain
	/// storage.
	pub deposit: Deposit<AccountIdOf<T>, BalanceOf<T>>,
}

TL;DR Looking at the struct above, it make sense why it fails on lastTxCounter: 'BlockNumber' and works on lastTxCounter: 'u64', so would say that it operates as expected with the supplied definitions, i.e. yielding invalid results when incorrect.

(At least for the runtime a future Substrate update will start making all the APIs and types available in the metadata - so unlike RPCs which will remain manual forever, when we get that update there would be no need to supply manual types for runtime calls)

What’s the point of substrate if custom apis don’t work (JS side, which is the 90% or use cases)… shocked to see this issue still open… everyone still using old deprecated RPCs? Or?

i’ll be keeping this active as long as it is not resolved 😃

i’ll be keeping this active as long as it is not resolved 😃

this is still an actual issue; we are seeing it too