paritytech / txwrapper

Helper funtions for offline transaction generation.
Apache License 2.0
58 stars 27 forks source link

`@ledgerhq/hw-app-polkadot` `.sign` method throws error: `Txn version not supported` #416

Closed will-yjn closed 3 years ago

will-yjn commented 3 years ago

I use the @ledgerhq/hw-app-polkadot library and the Ledger device to sign payloads for a polkadot transaction. The .sign method throws error: Txn version not supported.

  const { block } = await api.rpc.chain.getBlock();
  const blockHash = await api.rpc.chain.getBlockHash();
  const genesisHash = await api.rpc.chain.getBlockHash(0);
  const metadataRpc = await api.rpc.state.getMetadata();
  const { specVersion, transactionVersion } = await api.rpc.state.getRuntimeVersion();
  const ledgerAddress = (await getLedgerAddress(ledgerInstance)).address;
  const { nonce, } = await api.query.system.account(ledgerAddress);

  log.cyan(transactionVersion.toNumber()). // On Westend the `transactionVersion` is 4. On Polkadot it's 6. 

  const registry = new TypeRegistry();
  const unsigned = methods.balances.transfer(
    {
      value: totalAmount,
      dest,
    },
    {
      address: from,
      blockHash,
      blockNumber: registry
        .createType('BlockNumber', block.header.number)
        .toNumber(),
      eraPeriod: 64,
      genesisHash,
      metadataRpc,
      nonce,
      specVersion,
      tip: 0,
      transactionVersion, // here is the variable
    },
    {
      metadataRpc,
      registry,
    }
  );

  const signingPayload = createSigningPayload(unsigned, { registry });
  log.cyan(signingPayload);

  // Below line throws error: `Txn version not supported`
  const signature = await ledgerInstance.sign(path, signingPayload);

I tried switching between test-net Westend and polkadot main-net. I also tried hardcoding the transactionVersion from 1 to 10. None of the above works. I wonder if some format transformation happens during object construction and createSigningPayload process, so that ledger cannot decode the format of the transaction version. Have you tested the library with Ledger Nano S or X?

emostov commented 3 years ago

The error is likely not the transaction version, but instead the payload is simply not what ledger is expecting - so it tries to decode but finds something different where it is expecting the transactionVersion.

I don't have experience using ledger with txwrapper, but it should be able to be supported. I made an example of some the changes I am guessing that you need.

import * as pApi from '@polkadot/api';
import * as util from '@polkadot/util';
import * as txwrapper from '@substrate/txwrapper';

async function main() {
    const provider = new pApi.WsProvider('wss://rpc.polkadot.io');
    const api = await pApi.ApiPromise.create({ provider });

    const blockHash = await api.rpc.chain.getBlockHash();
    const { block: { header } } = await api.rpc.chain.getBlock(blockHash);
    const genesisHash = await api.rpc.chain.getBlockHash(0);
    // Use hex string for metadataRPC
    const metadataRpc = (await api.rpc.state.getMetadata()).toHex();
    const { specVersion, transactionVersion } = await api.rpc.state.getRuntimeVersion();
    const keyring = new pApi.Keyring({ ss58Format: 42, type: 'sr25519' });
    const alice = keyring.createFromUri('//Alice', { name: 'Alice' });
    const destBob = keyring.createFromUri('//Bob', { name: 'Bob' });

    // You need to use getRegistry to create the registry in order to ensure you
    // have the correct types for the chain and specific runtime.
    const registry = txwrapper.getRegistry(
        'Polkadot',
        'polkadot',
        specVersion.toNumber(),
        metadataRpc
    );

    const unsigned = txwrapper.methods.balances.transferKeepAlive(
        {
            dest: destBob.address,
            value: '1234567890'
        },
        {
            address: alice.address,
            // Convert everything to string or numbers
            blockHash: blockHash.toHex(),
            blockNumber: header.number.unwrap().toNumber(),
            genesisHash: genesisHash.toHex(),
            metadataRpc,
            nonce: 0,
            specVersion: specVersion.toNumber(),
            tip: 0,
            eraPeriod: 64,
            transactionVersion: transactionVersion.toNumber(),
        },
        {
            registry: registry,
            metadataRpc: metadataRpc,
        }
    );

    const extrinsicPayload = registry.createType(
        'ExtrinsicPayload',
        unsigned,
        {
            version: unsigned.version,
        }
    );
    const extrinsicPayloadU8a = extrinsicPayload.toU8a({ method: true });
    // `SignedPayloads` bigger than 256 bits get hashed with blake2_256
    // ref: https://substrate.dev/rustdocs/v3.0.0/src/sp_runtime/generic/unchecked_extrinsic.rs.html#201-209
    const signingPayloadU8a = extrinsicPayloadU8a.length > 256
        ? registry.hash(extrinsicPayloadU8a)
        : extrinsicPayloadU8a;

    const signingPayload = util.u8aToHex(signingPayloadU8a);

    // It looks like the ledger sign function just signs any message by turning it into a buffer, but
    // does not do any specific formatting, so we need to format it like we do above to make sure
    // big payloads are hashed etc.
    // You might need to do something like `signingPayload.slice(2)` in order to remove the leading
    // `0x` - I am not sure what assumptions are made here:
    // https://github.com/LedgerHQ/ledgerjs/blob/a5c2ea85a37e00fc805c878bd428a20ab0c0206a/packages/hw-app-polkadot/src/Polkadot.js#L131
    console.log('Payload to sign: ', signingPayload);
    // const signature = await ledgerInstance.sign(path, signingPayload);
}

main().catch(console.log);
emostov commented 3 years ago

Relates to https://github.com/LedgerHQ/ledgerjs/issues/583

will-yjn commented 3 years ago

Thank you very much! It saves my day!

You don't need to transform the signingPayloadU8a to hex though. No need to omit 0x either otherwise it breaks too.