Using metadata hash
Transactions are encoded and readed using the runtime metadata, for offline wallets this implies that they need to have access to a trustable source to provide its metadata.
With the introduction of the frame_metadata V15 and the RFC-78
It was introduced the merkleization of the metadata as an extra measure of security to prevent offline devices from signing transactions from fake metadatas.
Polkadart through the substrate_metadata
package provides a way to generate the merkleized metadata hash and use it to sign transactions.
Make a transfer using metadata hash
import 'package:demo/generated/polkadot/polkadot.dart';import 'package:demo/generated/polkadot/types/sp_runtime/multiaddress/multi_address.dart';import 'package:polkadart/polkadart.dart';import 'package:polkadart/scale_codec.dart';import 'package:polkadart_keyring/polkadart_keyring.dart';import 'package:substrate_metadata/metadata/merkleize.dart';
Future<void> main(List<String> arguments) async { final provider = Provider.fromUri(Uri.parse('wss://rpc.polkadot.io')); final polkadot = Polkadot(provider);
final wallet = await KeyPair.sr25519.fromUri("//Alice"); print('Alice\' wallet: ${wallet.address}');
// Get information necessary to build a proper extrinsic final runtimeVersion = await polkadot.rpc.state.getRuntimeVersion(); final currentBlockNumber = (await polkadot.query.system.number()) - 1; final currentBlockHash = await polkadot.query.system.blockHash(currentBlockNumber); final genesisHash = await polkadot.query.system.blockHash(0); final nonce = await polkadot.rpc.system.accountNextIndex(wallet.address);
// Make the encoded call final multiAddress = $MultiAddress().id(wallet.publicKey.bytes); final transferCall = polkadot.tx.balances.transferKeepAlive(dest: multiAddress, value: BigInt.one); final encodedCall = transferCall.encode();
// Get the MetadataHash final typedMetadata = await state.getTypedMetadata(); final merkleizer = MetadataMerkleizer.fromMetadata(typedMetadata.metadata, decimals: 10, tokenSymbol: 'DOT'); final metadataHash = hex.encode(merkleizer.digest());
// Make the payload final payload = SigningPayload( method: encodedCall, specVersion: runtimeVersion.specVersion, transactionVersion: runtimeVersion.transactionVersion, genesisHash: encodeHex(genesisHash), blockHash: encodeHex(currentBlockHash), blockNumber: currentBlockNumber, eraPeriod: 64, nonce: nonce, metadataHash: metadataHash, tip: 0).encode(polkadot.registry);
// Sign the payload and build the final extrinsic final signature = wallet.sign(payload); final extrinsic = ExtrinsicPayload( signer: wallet.bytes(), method: encodedCall, signature: signature, eraPeriod: 64, blockNumber: currentBlockNumber, nonce: nonce, tip: 0, metadataHash: metadataHash, ).encode(polkadot.registry, SignatureType.sr25519);
// Send the extrinsic to the blockchain final author = AuthorApi(provider); await author.submitAndWatchExtrinsic(extrinsic, (data) { print(data); });}
// Output:// Alice' wallet: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY// ready: null// broadcast: [12D3KooWH6tanw7GDdvspfEpt2gEbn82Na84deJDnMNJptJCfAKU, 12D3KooWN8pzDyuAAZ126uR6WcwYThbMWSPnXf7cimKHCZVn34iB, 12D3KooWDUjbGEP1fExf7LDze8iScY6ycsB8B7K3Gfnb2wwffhxK, 12D3KooWHTuZXi5jfAztC68Wf9wbHk8g3pFs9uUVpw9tib13zQWf, 12D3KooWGGBCu1AwCLt5LWE63RTPorR3PjX7Q3cDZrRku59NPkB5, 12D3KooWB6iwHFUoLpSxp6jNCkExzukVJF1hKWNRBvZWtt34gVXh, 12D3KooWEtK675m8iU8Hpc1E5SLnBMPZgzZfdp4uUGDmyh7LEgR3, 12D3KooW9zVuJRTqsPJ5KoA7QSPzCxesYT8FhAKRDwdLdYXAkim3, 12D3KooWPgHhBcG2zD6uND3M6XWdd8MuXCU2Z6xjZs5typTVqUVd, 12D3KooWGQudnpbyypxqUmQdsmpGCfHDu9ip4oqeMM4M4pPxcSmJ, 12D3KooWSS1FFy4VgJmf5kioR5MotCS3LFNHWM3Z1GxQfmUcKqiy, 12D3KooWQZkoqjez2aJ8GsTf1K5vCY2PV3XxwVf9B3iuuYYxViEE, 12D3KooWHHyDiQXNvgtW2pxNsgGntpz6AuSni3Dgem6EYY7PeMZM, 12D3KooWLJ5DEEcifVAXnJZAyznxd6xWsEvwS6U43nD5JmUxQGPb, 12D3KooWADSbUQbTLHf4dzp8NtWK3DJFj3TSyWxv3v1wooCU77YC, 12D3KooWCmUArt3vWrmNTCtvXf7RCWWhgSgU5jUcqD1cgPiL7w1z, 12D3KooWPe66mj1D4T4KcwsJikeJScxSVNS2iae6oJHGnF99DTLc, 12D3KooWH3toosYujUH3jvZDrVsudJp9CEUc8ajyNGAUeMvRYqYd, 12D3KooWAuKeacpNPdFq7sLjXmgS23SyGu4Us7Z4fRiJoVK3aky2, 12D3KooWFUUj8yJ3sJzXjtPZAvhuK1bAhbn9JPa7KAYCoDN1MLmZ, 12D3KooWCx3GoZeH2MZ4eFmqeYa9RPppC4eZopW8GbEgyYcrrhQA, 12D3KooWPWHUu17HfaPuK7fbszNd6chFDzwHL3HCVw5miNfAKjLN, 12D3KooWPWRX8XUxV3pYeYb1fZpK2RSycTFhrVZ4Su65HxuYzRSh, 12D3KooWH57Fx8xR8FJhkDuvZDg2Jav3ZEJohjMtyLC1CGXVZ1vK, 12D3KooWLc4ieE6EGZ3B8yZ7JnaQqJKU7ocGXRVNufkE9Nh8E3XQ, 12D3KooWCRcoJqxeKVDTcKxqv4kdeUGkoJkWggZJyhrfhh4jAHY4, 12D3KooWAwWmTPHPeUruUrAm53KjUrAGVqdwSKPmRa7RC65yE7aL, 12D3KooWRuAURXtSTkbiz6tccf6QhABKuYmQw2w3wy16ZjhAQEsA, 12D3KooWAgv6KKySREnXRy6V136VM7orMtLTdXifcLkofUTeN76o, 12D3KooWLnf7gQkZVjDcKkoxfxAYuoJZcFukkV3zvkNMvYwvZLV4, 12D3KooWDjsafUnHj8SfCuG8pYBzNFMbq8GuTWQ9AT1SbSa4X3qy, 12D3KooWQj2P4jDanJtktEJnTFqbtv8rqo8s3iqaTi58yE5GMoPa, 12D3KooWJhMu1NSHC2xJgSi73GznghPXBc6oq2VZdUYvJJd8MLU7, 12D3KooWSz85vPmCcQ9fwaHpSrPyaAXRP2fB5phetz6txD444Jmn, 12D3KooWKz8dMRwWnw7UVZrrWegYBPBufBhpxBYSjk4JELyxKrTS, 12D3KooWLcFJj4mvUBD38FckCqafTYavJxZBeQknxVAvkhMFATiB, 12D3KooWNgzmWzP2cdDoinwsCva1vrXdN1bKHbnrzvkz66hQhmtc, 12D3KooWAbPXnoRSng2B5W9rXwz2A3tVrPMy8brnSvmDcyvnJs7X, 12D3KooWRmVgYuBa54Xo225zBAhfesdSPM8TL5JruNmYFUa5m9dP, 12D3KooW9xysa5Q6KuANonoMBp8AgYpCAcpyUk6Z2wxpEbN4ru2N]// inBlock: 0xc3c341788f41a28046b7e0cac16629a2205acf1259ebfb5b52da0f2ae9e09959// finalized: 0xc3c341788f41a28046b7e0cac16629a2205acf1259ebfb5b52da0f2ae9e09959
You will notice that the example above is very similar to the one in Make a balance transfer. The difference here are the following:
import 'package:substrate_metadata/metadata/merkleize.dart';
final typedMetadata = await state.getTypedMetadata();final merkleizer = MetadataMerkleizer.fromMetadata(typedMetadata.metadata, decimals: 10, tokenSymbol: 'DOT');final metadataHash = hex.encode(merkleizer.digest());
- We use
state.getTypedMetadata()
to get the metadata v15 from the blockchain. - We use the
MetadataMerkleizer
to merkleize the metadata. - We use the
digest()
method to generate the metadata hash.
// Make the payload final payload = SigningPayload( method: encodedCall, specVersion: runtimeVersion.specVersion, transactionVersion: runtimeVersion.transactionVersion, genesisHash: encodeHex(genesisHash), blockHash: encodeHex(currentBlockHash), blockNumber: currentBlockNumber, eraPeriod: 64, nonce: nonce, metadataHash: metadataHash, tip: 0).encode(polkadot.registry);
// Sign the payload and build the final extrinsic final signature = wallet.sign(payload); final extrinsic = ExtrinsicPayload( signer: wallet.bytes(), method: encodedCall, signature: signature, eraPeriod: 64, blockNumber: currentBlockNumber, nonce: nonce, tip: 0, metadataHash: metadataHash, ).encode(polkadot.registry, SignatureType.sr25519);
Now, notice that we are providing the metadataHash
in the SigningPayload
and ExtrinsicPayload
objects.
This will ensure that your transaction was signed with the correct metadata and if not it will be rejected by the blockchain.