Files
pezkuwi-fellows/approved/0078-merkleized-metadata.html
T
2025-11-18 01:15:24 +00:00

607 lines
55 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE HTML>
<html lang="en" class="polkadot" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>RFC-0078: Merkleized Metadata - Polkadot Fellowship RFCs</title>
<!-- Custom HTML head -->
<meta name="description" content="An online book of RFCs approved or proposed within the Polkadot Fellowship.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="icon" href="../favicon.svg">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
<link rel="stylesheet" href="../theme/polkadot.css">
</head>
<body class="sidebar-visible no-js">
<div id="body-container">
<!-- Provide site root to javascript -->
<script>
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "polkadot" : "polkadot";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('polkadot')
html.classList.add(theme);
var body = document.querySelector('body');
body.classList.remove('no-js')
body.classList.add('js');
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var body = document.querySelector('body');
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
body.classList.remove('sidebar-visible');
body.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded affix "><a href="../introduction.html">Introduction</a></li><li class="spacer"></li><li class="chapter-item expanded affix "><li class="part-title">Newly Proposed</li><li class="spacer"></li><li class="chapter-item expanded affix "><li class="part-title">Proposed</li><li class="chapter-item expanded "><a href="../proposed/0150-voting-while-delegating.html">RFC-150: Allow Voting While Delegating</a></li><li class="chapter-item expanded "><a href="../proposed/0154-multi-slot-aura.html">RFC-0154: AURA Multi-Slot Collation </a></li><li class="chapter-item expanded "><a href="../proposed/0155-pUSD.html">RFC-0155: pUSD (Polkadot USD over-collateralised debt token)</a></li><li class="chapter-item expanded "><a href="../proposed/0156-bls-signatures.html">RFC-0156: Add BLS12-381 Host Functions</a></li><li class="spacer"></li><li class="chapter-item expanded affix "><li class="part-title">Approved</li><li class="chapter-item expanded "><a href="../approved/0001-agile-coretime.html">RFC-1: Agile Coretime</a></li><li class="chapter-item expanded "><a href="../approved/0005-coretime-interface.html">RFC-5: Coretime Interface</a></li><li class="chapter-item expanded "><a href="../approved/0007-system-collator-selection.html">RFC-0007: System Collator Selection</a></li><li class="chapter-item expanded "><a href="../approved/0008-parachain-bootnodes-dht.html">RFC-0008: Store parachain bootnodes in relay chain DHT</a></li><li class="chapter-item expanded "><a href="../approved/0009-improved-net-light-client-requests.html">RFC-0009: Improved light client requests networking protocol</a></li><li class="chapter-item expanded "><a href="../approved/0010-burn-coretime-revenue.html">RFC-0010: Burn Coretime Revenue</a></li><li class="chapter-item expanded "><a href="../approved/0012-process-for-adding-new-collectives.html">RFC-0012: Process for Adding New System Collectives</a></li><li class="chapter-item expanded "><a href="../approved/0013-prepare-blockbuilder-and-core-runtime-apis-for-mbms.html">RFC-0013: Prepare Core runtime API for MBMs</a></li><li class="chapter-item expanded "><a href="../approved/0014-improve-locking-mechanism-for-parachains.html">RFC-0014: Improve locking mechanism for parachains</a></li><li class="chapter-item expanded "><a href="../approved/0017-coretime-market-redesign.html">RFC-0017: Coretime Market Redesign</a></li><li class="chapter-item expanded "><a href="../approved/0022-adopt-encointer-runtime.html">RFC-0022: Adopt Encointer Runtime</a></li><li class="chapter-item expanded "><a href="../approved/0026-sassafras-consensus.html">RFC-0026: Sassafras Consensus Protocol</a></li><li class="chapter-item expanded "><a href="../approved/0032-minimal-relay.html">RFC-0032: Minimal Relay</a></li><li class="chapter-item expanded "><a href="../approved/0042-extrinsics-state-version.html">RFC-0042: Add System version that replaces StateVersion on RuntimeVersion</a></li><li class="chapter-item expanded "><a href="../approved/0043-storage-proof-size-hostfunction.html">RFC-0043: Introduce storage_proof_size Host Function for Improved Parachain Block Utilization</a></li><li class="chapter-item expanded "><a href="../approved/0045-nft-deposits-asset-hub.html">RFC-0045: Lowering NFT Deposits on Asset Hub</a></li><li class="chapter-item expanded "><a href="../approved/0047-assignment-of-availability-chunks.html">RFC-0047: Assignment of availability chunks to validators</a></li><li class="chapter-item expanded "><a href="../approved/0048-session-keys-runtime-api.html">RFC-0048: Generate ownership proof for SessionKeys</a></li><li class="chapter-item expanded "><a href="../approved/0050-fellowship-salaries.html">RFC-0050: Fellowship Salaries</a></li><li class="chapter-item expanded "><a href="../approved/0056-one-transaction-per-notification.html">RFC-0056: Enforce only one transaction per notification</a></li><li class="chapter-item expanded "><a href="../approved/0059-nodes-capabilities-discovery.html">RFC-0059: Add a discovery mechanism for nodes based on their capabilities</a></li><li class="chapter-item expanded "><a href="../approved/0078-merkleized-metadata.html" class="active">RFC-0078: Merkleized Metadata</a></li><li class="chapter-item expanded "><a href="../approved/0084-general-transaction-extrinsic-format.html">RFC-0084: General transactions in extrinsic format</a></li><li class="chapter-item expanded "><a href="../approved/0091-dht-record-creation-time.html">RFC-0091: DHT Authority discovery record creation time</a></li><li class="chapter-item expanded "><a href="../approved/0097-unbonding_queue.html">RFC-0097: Unbonding Queue</a></li><li class="chapter-item expanded "><a href="../approved/0099-transaction-extension-version.html">RFC-0099: Introduce a transaction extension version</a></li><li class="chapter-item expanded "><a href="../approved/0100-xcm-multi-type-asset-transfer.html">RFC-0100: New XCM instruction: InitiateAssetsTransfer</a></li><li class="chapter-item expanded "><a href="../approved/0101-xcm-transact-remove-max-weight-param.html">RFC-0101: XCM Transact remove require_weight_at_most parameter</a></li><li class="chapter-item expanded "><a href="../approved/0103-introduce-core-index-commitment.html">RFC-0103: Introduce a CoreIndex commitment and a SessionIndex field in candidate receipts</a></li><li class="chapter-item expanded "><a href="../approved/0105-xcm-improved-fee-mechanism.html">RFC-0105: XCM improved fee mechanism</a></li><li class="chapter-item expanded "><a href="../approved/0107-xcm-execution-hints.html">RFC-0107: XCM Execution hints</a></li><li class="chapter-item expanded "><a href="../approved/0108-xcm-remove-testnet-ids.html">RFC-0108: Remove XCM testnet NetworkIds</a></li><li class="chapter-item expanded "><a href="../approved/0122-alias-origin-on-asset-transfers.html">RFC-0122: Asset transfers can alias XCM origin on destination to original origin</a></li><li class="chapter-item expanded "><a href="../approved/0123-pending-code-as-storage-location-for-runtime-upgrades.html">RFC-0123: Introduce :pending_code as intermediate storage key for the runtime code</a></li><li class="chapter-item expanded "><a href="../approved/0125-xcm-asset-metadata.html">RFC-0125: XCM Asset Metadata</a></li><li class="chapter-item expanded "><a href="../approved/0126-introduce-pvq.html">RFC-0126: Introduce PVQ (PolkaVM Query)</a></li><li class="chapter-item expanded "><a href="../approved/0135-compressed-blob-prefixes.html">RFC-0135: Compressed Blob Prefixes</a></li><li class="chapter-item expanded "><a href="../approved/0139-faster-erasure-coding.html">RFC-0139: Faster Erasure Coding</a></li><li class="chapter-item expanded "><a href="../approved/0146-deflationary-fee-proposal.html">RFC-0146: Deflationary Transaction Fee Model for the Relay Chain and its System Parachains</a></li><li class="chapter-item expanded "><a href="../approved/0149-rfc-1-renewal-adjustment.html">RFC-0149: Renewal Adjustment</a></li><li class="spacer"></li><li class="chapter-item expanded affix "><li class="part-title">Stale</li><li class="chapter-item expanded "><a href="../stale/0000-pre-elves_soft.html">RFC-0000: Pre-ELVES soft concensus</a></li><li class="chapter-item expanded "><a href="../stale/0000-rewards.html">RFC-0000: Validator Rewards</a></li><li class="chapter-item expanded "><a href="../stale/0004-remove-unnecessary-allocator-usage.html">RFC-0004: Remove the host-side runtime memory allocator</a></li><li class="chapter-item expanded "><a href="../stale/0006-dynamic-pricing-for-bulk-coretime-sales.html">RFC-0006: Dynamic Pricing for Bulk Coretime Sales</a></li><li class="chapter-item expanded "><a href="../stale/0034-xcm-absolute-location-account-derivation.html">RFC-34: XCM Absolute Location Account Derivation</a></li><li class="chapter-item expanded "><a href="../stale/0035-conviction-voting-delegation-modifications.html"> RFC-0035: Conviction Voting Delegation Modifications</a></li><li class="chapter-item expanded "><a href="../stale/0044-rent-based-registration.html">RFC-0044: Rent based registration model</a></li><li class="chapter-item expanded "><a href="../stale/0054-remove-heap-pages.html">RFC-0054: Remove the concept of "heap pages" from the client</a></li><li class="chapter-item expanded "><a href="../stale/0070-x-track-kusamanetwork.html">RFC-0070: X Track for @kusamanetwork</a></li><li class="chapter-item expanded "><a href="../stale/0073-referedum-deposit-track.html">RFC-0073: Decision Deposit Referendum Track</a></li><li class="chapter-item expanded "><a href="../stale/0074-stateful-multisig-pallet.html">RFC-0074: Stateful Multisig Pallet</a></li><li class="chapter-item expanded "><a href="../stale/0077-increase-max-length-of-identity-pgp-fingerprint-value.html">RFC-0077: Increase maximum length of identity PGP fingerprint values from 20 bytes</a></li><li class="chapter-item expanded "><a href="../stale/0088-broker-pallet-slashable-deposit-purchaser-reputation-reserved-cores.html">RFC-0088: Add slashable locked deposit, purchaser reputation, and reserved cores for on-chain identities to broker pallet</a></li><li class="chapter-item expanded "><a href="../stale/00xx-secondary-marketplace-for-regions.html">RFC-0001: Secondary Market for Regions</a></li><li class="chapter-item expanded "><a href="../stale/00xx-smart-contracts-coretime-chain.html">RFC-0002: Smart Contracts on the Coretime Chain</a></li><li class="chapter-item expanded "><a href="../stale/0102-offchain-parachain-runtime-upgrades.html">RFC-0000: Feature Name Here</a></li><li class="chapter-item expanded "><a href="../stale/0106-xcm-remove-fees-mode.html">RFC-0106: Remove XCM fees mode</a></li><li class="chapter-item expanded "><a href="../stale/0111-pure-proxy-replication.html">RFC-0111: Pure Proxy Replication</a></li><li class="chapter-item expanded "><a href="../stale/0112-compress-state-response-message-in-state-sync.html">RFC-0112: Compress the State Response Message in State Sync</a></li><li class="chapter-item expanded "><a href="../stale/0114-secp256r1-hostfunction.html">RFC-0114: Introduce secp256r1_ecdsa_verify_prehashed Host Function to verify NIST-P256 elliptic curve signatures</a></li><li class="chapter-item expanded "><a href="../stale/0117-unbrick-collective.html">RFC-0117: The Unbrick Collective</a></li><li class="chapter-item expanded "><a href="../stale/0120-referenda-confirmation-by-candle-mechanism.html">RFC-0120: Referenda Confirmation by Candle Mechanism</a></li><li class="chapter-item expanded "><a href="../stale/0124-extrinsic-version-5.html">RFC-0124: Extrinsic version 5</a></li><li class="chapter-item expanded "><a href="../stale/0138-invulnerable-collator-election.html">RFC-0138: Election mechanism for invulnerable collators on system chains</a></li><li class="chapter-item expanded "><a href="../stale/0152-decentralized-convex-preference-coretime-market-for-polkadot.html">RFC-0152: Decentralized Convex-Preference Coretime Market for Polkadot</a></li><li class="chapter-item expanded "><a href="../stale/RFC-114 Adjust Tipper Track Confirmation Periods.html">RFC-114: Adjust Tipper Track Confirmation Periods</a></li><li class="chapter-item expanded "><a href="../stale/TODO-stale-nomination-reward-curve.html">RFC-TODO: Stale Nomination Reward Curve</a></li><li class="chapter-item expanded "><a href="../stale/xxxx-improve-the-security-of-proof-of-possession.html">RFC-XXXX: Adding customized mandatory context to proof of possession statement</a></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="polkadot">Polkadot</button></li>
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Polkadot Fellowship RFCs</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<p><a href="https://github.com/polkadot-fellows/RFCs/blob/main/text/0078-merkleized-metadata.md">(source)</a></p>
<p><strong>Table of Contents</strong></p>
<ul>
<li><a href="#rfc-0078-merkleized-metadata">RFC-0078: Merkleized Metadata</a>
<ul>
<li><a href="#summary">Summary</a></li>
<li><a href="#motivation">Motivation</a></li>
<li><a href="#requirements">Requirements</a>
<ul>
<li><a href="#reduce-metadata-size">Reduce metadata size</a></li>
</ul>
</li>
<li><a href="#stakeholders">Stakeholders</a></li>
<li><a href="#explanation">Explanation</a>
<ul>
<li><a href="#metadata-digest">Metadata digest</a></li>
<li><a href="#extrinsic-metadata">Extrinsic metadata</a></li>
<li><a href="#type-information">Type Information</a></li>
<li><a href="#prune-unrelated-types">Prune unrelated Types</a></li>
<li><a href="#generating-typeref">Generating <code>TypeRef</code></a></li>
<li><a href="#building-the-merkle-tree-root">Building the Merkle Tree Root</a></li>
<li><a href="#inclusion-in-an-extrinsic">Inclusion in an Extrinsic</a></li>
</ul>
</li>
<li><a href="#drawbacks">Drawbacks</a></li>
<li><a href="#testing-security-and-privacy">Testing, Security, and Privacy</a></li>
<li><a href="#performance-ergonomics-and-compatibility">Performance, Ergonomics, and Compatibility</a>
<ul>
<li><a href="#performance">Performance</a></li>
<li><a href="#ergonomics--compatibility">Ergonomics &amp; Compatibility</a></li>
</ul>
</li>
<li><a href="#prior-art-and-references">Prior Art and References</a></li>
<li><a href="#unresolved-questions">Unresolved Questions</a></li>
<li><a href="#future-directions-and-related-material">Future Directions and Related Material</a></li>
</ul>
</li>
</ul>
<h1 id="rfc-0078-merkleized-metadata"><a class="header" href="#rfc-0078-merkleized-metadata">RFC-0078: Merkleized Metadata</a></h1>
<div class="table-wrapper"><table><thead><tr><th></th><th></th></tr></thead><tbody>
<tr><td><strong>Start Date</strong></td><td>22 February 2024</td></tr>
<tr><td><strong>Description</strong></td><td>Include merkleized metadata hash in extrinsic signature for trust-less metadata verification.</td></tr>
<tr><td><strong>Authors</strong></td><td>Zondax AG, Parity Technologies</td></tr>
</tbody></table>
</div>
<h2 id="summary"><a class="header" href="#summary">Summary</a></h2>
<p>To interact with chains in the Polkadot ecosystem it is required to know how transactions are encoded and how to read state. For doing this, Polkadot-SDK, the framework used by most of the chains in the Polkadot ecosystem, exposes metadata about the runtime to the outside. UIs, wallets, and others can use this metadata to interact with these chains. This makes the metadata a crucial piece of the transaction encoding as users are relying on the interacting software to encode the transactions in the correct format.</p>
<p>It gets even more important when the user signs the transaction in an offline wallet, as the device by its nature cannot get access to the metadata without relying on the online wallet to provide it. This makes it so that the offline wallet needs to <em>trust</em> an online party, deeming the security assumptions of the offline devices, mute. </p>
<p>This RFC proposes a way for offline wallets to leverage metadata, within the constraints of these. The design idea is that the metadata is chunked and these chunks are put into a merkle tree. The root hash of this merkle tree represents the metadata. The offline wallets can use the root hash to decode transactions by getting proofs for the individual chunks of the metadata. This root hash is also included in the signed data of the transaction (but not sent as part of the transaction). The runtime is then including its known metadata root hash when verifying the transaction. If the metadata root hash known by the runtime differs from the one that the offline wallet used, it very likely means that the online wallet provided some fake data and the verification of the transaction fails.</p>
<p>Users depend on offline wallets to correctly display decoded transactions before signing. With merkleized metadata, they can be assured of the transaction's legitimacy, as incorrect transactions will be rejected by the runtime.</p>
<h2 id="motivation"><a class="header" href="#motivation">Motivation</a></h2>
<p>Polkadot's innovative design (both relay chain and parachains) present the ability to developers to upgrade their network as frequently as they need. These systems manage to have integrations working after the upgrades with the help of FRAME Metadata. This Metadata, which is in the order of half a MiB for most Polkadot-SDK chains, completely describes chain interfaces and properties. Securing this metadata is key for users to be able to interact with the Polkadot-SDK chain in the expected way.</p>
<p>On the other hand, offline wallets provide a secure way for Blockchain users to hold their own keys (some do a better job than others). These devices seldomly get upgraded, usually account for one particular network and hold very small internal memories. Currently in the Polkadot ecosystem there is no secure way of having these offline devices know the latest Metadata of the Polkadot-SDK chain they are interacting with. This results in a plethora of similar yet slightly different offline wallets for all different Polkadot-SDK chains, as well as the impediment of keeping these regularly updated, thus not fully leveraging Polkadot-SDKs unique forkless upgrade feature.</p>
<p>The two main reasons why this is not possible today are:</p>
<ol>
<li><strong>Metadata is too large for offline devices</strong>. Currently Polkadot-SDK metadata is on average 500 KiB, which is more than what the mostly adopted offline devices can hold.</li>
<li><strong>Metadata is not authenticated</strong>. Even if there was enough space on offline devices to hold the metadata, the user would be trusting the entity providing this metadata to the hardware wallet. In the Polkadot ecosystem, this is how currently Polkadot Vault works.</li>
</ol>
<p><strong>This RFC proposes a solution to make FRAME Metadata compatible with offline signers in a secure way.</strong> As it leverages FRAME Metadata, it does not only ensure that offline devices can always keep up to date with every FRAME based chain, but also that every offline wallet will be compatible with all FRAME based chains, avoiding the need of per-chain implementations.</p>
<h2 id="requirements"><a class="header" href="#requirements">Requirements</a></h2>
<ol>
<li>Metadata's integrity MUST be preserved. If any compromise were to happen, extrinsics sent with compromised metadata SHOULD fail.</li>
<li>Metadata information that could be used in signable extrinsic decoding MAY be included in digest, yet its inclusion MUST be indicated in signed extensions.</li>
<li>Digest MUST be deterministic with respect to metadata.</li>
<li>Digest MUST be cryptographically strong against pre-image, both first (finding an input that results in given digest) and second (finding an input that results in same digest as some other input given).</li>
<li>Extra-metadata information necessary for extrinsic decoding and constant within runtime version MUST be included in digest.</li>
<li>It SHOULD be possible to quickly withdraw offline signing mechanism without access to cold signing devices.</li>
<li>Digest format SHOULD be versioned.</li>
<li>Work necessary for proving metadata authenticity MAY be omitted at discretion of signer device design (to support automation tools).</li>
</ol>
<h3 id="reduce-metadata-size"><a class="header" href="#reduce-metadata-size">Reduce metadata size</a></h3>
<p>Metadata should be stripped from parts that are not necessary to parse a signable extrinsic, then it should be separated into a finite set of self-descriptive chunks. Thus, a subset of chunks necessary for signable extrinsic decoding and rendering could be sent, possibly in small portions (ultimately, one at a time), to cold devices together with the proof.</p>
<ol>
<li>Single chunk with proof payload size SHOULD fit within few kB;</li>
<li>Chunks handling mechanism SHOULD support chunks being sent in any order without memory utilization overhead;</li>
<li>Unused enum variants MUST be stripped (this has great impact on transmitted metadata size; examples: era enum, enum with all calls for call batching).</li>
</ol>
<h2 id="stakeholders"><a class="header" href="#stakeholders">Stakeholders</a></h2>
<ul>
<li>Runtime implementors</li>
<li>UI/wallet implementors</li>
<li>Offline wallet implementors</li>
</ul>
<p>The idea for this RFC was brought up by runtime implementors and was extensively discussed with offline wallet implementors. It was designed in such a way that it can work easily with the existing offline wallet solutions in the Polkadot ecosystem.</p>
<h2 id="explanation"><a class="header" href="#explanation">Explanation</a></h2>
<p>The FRAME metadata provides a wide range of information about a FRAME based runtime. It contains information about the pallets, the calls per pallet, the storage entries per pallet, runtime APIs, and type information about most of the types that are used in the runtime. For decoding extrinsics on an offline wallet, what is mainly required is type information. Most of the other information in the FRAME metadata is actually not required for decoding extrinsics and thus it can be removed. Therefore, the following is a proposal on a custom representation of the metadata and how this custom metadata is chunked, ensuring that only the needed chunks required for decoding a particular extrinsic are sent to the offline wallet. The necessary information to transform the FRAME metadata type information into the type information presented in this RFC will be provided. However, not every single detail on how to convert from FRAME metadata into the RFC type information is described.</p>
<p>First, the <code>MetadataDigest</code> is introduced. After that, <code>ExtrinsicMetadata</code> is covered and finally the actual format of the type information. Then pruning of unrelated type information is covered and how to generate the <code>TypeRef</code>s. In the latest step, merkle tree calculation is explained.</p>
<h3 id="metadata-digest"><a class="header" href="#metadata-digest">Metadata digest</a></h3>
<p>The metadata digest is the compact representation of the metadata. The hash of this digest is the <em>metadata hash</em>. Below the type declaration of the <code>Hash</code> type and the <code>MetadataDigest</code> itself can be found:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>type Hash = [u8; 32];
enum MetadataDigest {
#[index = 1]
V1 {
type_information_tree_root: Hash,
extrinsic_metadata_hash: Hash,
spec_version: u32,
spec_name: String,
base58_prefix: u16,
decimals: u8,
token_symbol: String,
},
}
<span class="boring">}</span></code></pre></pre>
<p>The <code>Hash</code> is 32 bytes long and <code>blake3</code> is used for calculating it. The hash of the <code>MetadataDigest</code> is calculated by <code>blake3(SCALE(MetadataDigest))</code>. Therefore, <code>MetadataDigest</code> is at first <code>SCALE</code> encoded, and then those bytes are hashed.</p>
<p>The <code>MetadataDigest</code> itself is represented as an <code>enum</code>. This is done to make it future proof, because a <code>SCALE</code> encoded <code>enum</code> is prefixed by the <code>index</code> of the variant. This <code>index</code> represents the version of the digest. As seen above, there is no <code>index</code> zero and it starts directly with one. Version one of the digest contains the following elements:</p>
<ul>
<li><code>type_information_tree_root</code>: The root of the <a href="#type-information">merkleized type information</a> tree.</li>
<li><code>extrinsic_metadata_hash</code>: The hash of the <a href="#extrinsic-metadata">extrinsic metadata</a>.</li>
<li><code>spec_version</code>: The <code>spec_version</code> of the runtime as found in the <code>RuntimeVersion</code> when generating the metadata. While this information can also be found in the metadata, it is hidden in a big blob of data. To avoid transferring this big blob of data, we directly add this information here.</li>
<li><code>spec_name</code>: Similar to <code>spec_version</code>, but being the <code>spec_name</code> found in the <code>RuntimeVersion</code>.</li>
<li><code>ss58_prefix</code>: The <code>SS58</code> prefix used for address encoding.</li>
<li><code>decimals</code>: The number of decimals for the token.</li>
<li><code>token_symbol</code>: The symbol of the token.</li>
</ul>
<h3 id="extrinsic-metadata"><a class="header" href="#extrinsic-metadata">Extrinsic metadata</a></h3>
<p>For decoding an extrinsic, more information on what types are being used is required. The actual format of the extrinsic is the format as described in the <a href="https://spec.polkadot.network/id-extrinsics">Polkadot specification</a>. The metadata for an extrinsic is as follows:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>struct ExtrinsicMetadata {
version: u8,
address_ty: TypeRef,
call_ty: TypeRef,
signature_ty: TypeRef,
signed_extensions: Vec&lt;SignedExtensionMetadata&gt;,
}
struct SignedExtensionMetadata {
identifier: String,
included_in_extrinsic: TypeRef,
included_in_signed_data: TypeRef,
}
<span class="boring">}</span></code></pre></pre>
<p>To begin with, <code>TypeRef</code>. This is a unique identifier for a type as found in the type information. Using this <code>TypeRef</code>, it is possible to look up the type in the type information tree. More details on this process can be found in the section <a href="#generating-typeref">Generating <code>TypeRef</code></a>.</p>
<p>The actual <code>ExtrinsicMetadata</code> contains the following information:</p>
<ul>
<li><code>version</code>: The version of the extrinsic format. As of writing this, the latest version is <code>4</code>.</li>
<li><code>address_ty</code>: The address type used by the chain.</li>
<li><code>call_ty</code>: The <code>call</code> type used by the chain. The <code>call</code> in FRAME based runtimes represents the type of transaction being executed on chain. It references the actual function to execute and the parameters of this function.</li>
<li><code>signature_ty</code>: The signature type used by the chain.</li>
<li><code>signed_extensions</code>: FRAME based runtimes can extend the base extrinsic with extra information. This extra information that is put into an extrinsic is called &quot;signed extensions&quot;. These extensions offer the runtime developer the possibility to include data directly into the extrinsic, like <code>nonce</code>, <code>tip</code>, amongst others. This means that the this data is sent alongside the extrinsic to the runtime. The other possibility these extensions offer is to include extra information only in the signed data that is signed by the sender. This means that this data needs to be known by both sides, the signing side and the verification side. An example for this kind of data is the <em>genesis hash</em> that ensures that extrinsics are unique per chain. Another example is the <em>metadata hash</em> itself that will also be included in the signed data. The offline wallets need to know which signed extensions are present in the chain and this is communicated to them using this field.</li>
</ul>
<p>The <code>SignedExtensionMetadata</code> provides information about a signed extension:</p>
<ul>
<li><code>identifier</code>: The <code>identifier</code> of the signed extension. An <code>identifier</code> is required to be unique in the Polkadot ecosystem as otherwise extrinsics are maybe built incorrectly.</li>
<li><code>included_in_extrinsic</code>: The type that will be included in the extrinsic by this signed extension.</li>
<li><code>included_in_signed_data</code>: The type that will be included in the signed data by this signed extension.</li>
</ul>
<h3 id="type-information"><a class="header" href="#type-information">Type Information</a></h3>
<p>As SCALE is not self descriptive like JSON, a decoder always needs to know the format of the type to decode it properly. This is where the type information comes into play. The format of the extrinsic is fixed as described above and <code>ExtrinsicMetadata</code> provides information on which type information is required for which part of the extrinsic. So, offline wallets only need access to the actual type information. It is a requirement that the type information can be chunked into logical pieces to reduce the amount of data that is sent to the offline wallets for decoding the extrinsics. So, the type information is structured in the following way:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>struct Type {
path: Vec&lt;String&gt;,
type_def: TypeDef,
type_id: Compact&lt;u32&gt;,
}
enum TypeDef {
Composite(Vec&lt;Field&gt;),
Enumeration(EnumerationVariant),
Sequence(TypeRef),
Array(Array),
Tuple(Vec&lt;TypeRef&gt;),
BitSequence(BitSequence),
}
struct Field {
name: Option&lt;String&gt;,
ty: TypeRef,
type_name: Option&lt;String&gt;,
}
struct Array {
len: u32,
type_param: TypeRef,
}
struct BitSequence {
num_bytes: u8,
least_significant_bit_first: bool,
}
struct EnumerationVariant {
name: String,
fields: Vec&lt;Field&gt;,
index: Compact&lt;u32&gt;,
}
enum TypeRef {
Bool,
Char,
Str,
U8,
U16,
U32,
U64,
U128,
U256,
I8,
I16,
I32,
I64,
I128,
I256,
CompactU8,
CompactU16,
CompactU32,
CompactU64,
CompactU128,
CompactU256,
Void,
PerId(Compact&lt;u32&gt;),
}
<span class="boring">}</span></code></pre></pre>
<p>The <code>Type</code> declares the structure of a type. The <code>type</code> has the following fields:</p>
<ul>
<li><code>path</code>: A <code>path</code> declares the position of a type locally to the place where it is defined. The <code>path</code> is not globally unique, this means that there can be multiple types with the same <code>path</code>.</li>
<li><code>type_def</code>: The high-level type definition, e.g. the type is a composition of fields where each field has a type, the type is a composition of different types as <code>tuple</code> etc.</li>
<li><code>type_id</code>: The unique identifier of this type.</li>
</ul>
<p>Every <code>Type</code> is composed of multiple different types. Each of these &quot;sub types&quot; can reference either a full <code>Type</code> again or reference one of the primitive types. This is where <code>TypeRef</code> becomes relevant as the type referencing information. To reference a <code>Type</code> in the type information, a unique identifier is used. As primitive types can be represented using a single byte, they are not put as separate types into the type information. Instead the primitive types are directly part of <code>TypeRef</code> to not require the overhead of referencing them in an extra <code>Type</code>. The special primitive type <code>Void</code> represents a type that encodes to nothing and can be decoded from nothing. As FRAME doesn't support <code>Compact</code> as primitive type it requires a more involved implementation to convert a FRAME type to a <code>Compact</code> primitive type. SCALE only supports <code>u8</code>, <code>u16</code>, <code>u32</code>, <code>u64</code> and <code>u128</code> as <code>Compact</code> which maps onto the primitive type declaration in the RFC. One special case is a <code>Compact</code> that wraps an empty <code>Tuple</code> which is expressed as primitive type <code>Void</code>.</p>
<p>The <code>TypeDef</code> variants have the following meaning:</p>
<ul>
<li><code>Composite</code>: A <code>struct</code> like type that is composed of multiple different fields. Each <code>Field</code> can have its own type. The order of the fields is significant. A <code>Composite</code> with no fields is expressed as primitive type <code>Void</code>.</li>
<li><code>Enumeration</code>: Stores a <code>EnumerationVariant</code>. A <code>EnumerationVariant</code> is a struct that is described by a name, an index and a vector of <code>Field</code>s, each of which can have it's own type. Typically <code>Enumeration</code>s have more than just one variant, and in those cases <code>Enumeration</code> will appear multiple times, each time with a different variant, in the type information. <code>Enumeration</code>s can become quite large, yet usually for decoding a type only one variant is required, therefore this design brings optimizations and helps reduce the size of the proof. An <code>Enumeration</code> with no variants is expressed as primitive type <code>Void</code>.</li>
<li><code>Sequence</code>: A <code>vector</code> like type wrapping the given type.</li>
<li><code>BitSequence</code>: A <code>vector</code> storing bits. <code>num_bytes</code> represents the size in bytes of the internal storage. If <code>least_significant_bit_first</code> is <code>true</code> the least significant bit is first, otherwise the most significant bit is first.</li>
<li><code>Array</code>: A fixed-length array of a specific type.</li>
<li><code>Tuple</code>: A composition of multiple types. A <code>Tuple</code> that is composed of no types is expressed as primitive type <code>Void</code>.</li>
</ul>
<p>Using the type information together with the <a href="https://spec.polkadot.network/id-cryptography-encoding#sect-scale-codec">SCALE specification</a> provides enough information on how to decode types.</p>
<h3 id="prune-unrelated-types"><a class="header" href="#prune-unrelated-types">Prune unrelated Types</a></h3>
<p>The FRAME metadata contains not only the type information for decoding extrinsics, but it also contains type information about storage types. The scope of the RFC is only about decoding transactions on offline wallets. Thus, a lot of type information can be pruned. To know which type information are required to decode all possible extrinsics, <code>ExtrinsicMetadata</code> has been defined. The extrinsic metadata contains all the types that define the layout of an extrinsic. Therefore, all the types that are accessible from the types declared in the extrinsic metadata can be collected. To collect all accessible types, it requires to recursively iterate over all types starting from the types in <code>ExtrinsicMetadata</code>. Note that some types are accessible, but they don't appear in the final type information and thus, can be pruned as well. These are for example inner types of <code>Compact</code> or the types referenced by <code>BitSequence</code>. The result of collecting these accessible types is a list of all the types that are required to decode each possible extrinsic.</p>
<h3 id="generating-typeref"><a class="header" href="#generating-typeref">Generating <code>TypeRef</code></a></h3>
<p>Each <code>TypeRef</code> basically references one of the following types:</p>
<ul>
<li>One of the primitive types. All primitive types can be represented by 1 byte and thus, they are directly part of the <code>TypeRef</code> itself to remove an extra level of indirection.</li>
<li>A <code>Type</code> using its unique identifier.</li>
</ul>
<p>In FRAME metadata a primitive type is represented like any other type. So, the first step is to remove all the primitive only types from the list of types that were generated in the previous section. The resulting list of types is sorted using the <code>id</code> provided by FRAME metadata. In the last step the <code>TypeRef</code>s are created. Each reference to a primitive type is replaced by one of the corresponding <code>TypeRef</code> primitive type variants and every other reference is replaced by the type's unique identifier. The unique identifier of a type is the index of the type in our sorted list. For <code>Enumeration</code>s all variants have the same unique identifier, while they are represented as multiple type information. All variants need to have the same unique identifier as the reference doesn't know which variant will appear in the actual encoded data.</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>let pruned_types = get_pruned_types();
for ty in pruned_types {
if ty.is_primitive_type() {
pruned_types.remove(ty);
}
}
pruned_types.sort(|(left, right)|
if left.frame_metadata_id() == right.frame_metadata_id() {
left.variant_index() &lt; right.variant_index()
} else {
left.frame_metadata_id() &lt; right.frame_metadata_id()
}
);
fn generate_type_ref(ty, ty_list) -&gt; TypeRef {
if ty.is_primitive_type() {
TypeRef::primtive_from_ty(ty)
}
TypeRef::from_id(
// Determine the id by using the position of the type in the
// list of unique frame metadata ids.
ty_list.position_by_frame_metadata_id(ty.frame_metadata_id())
)
}
fn replace_all_sub_types_with_type_refs(ty, ty_list) -&gt; Type {
for sub_ty in ty.sub_types() {
replace_all_sub_types_with_type_refs(sub_ty, ty_list);
sub_ty = generate_type_ref(sub_ty, ty_list)
}
ty
}
let final_ty_list = Vec::new();
for ty in pruned_types {
final_ty_list.push(replace_all_sub_types_with_type_refs(ty, ty_list))
}
<span class="boring">}</span></code></pre></pre>
<h3 id="building-the-merkle-tree-root"><a class="header" href="#building-the-merkle-tree-root">Building the Merkle Tree Root</a></h3>
<p>A complete binary merkle tree with <code>blake3</code> as the hashing function is proposed. For building the merkle tree root, the initial data has to be hashed as a first step. This initial data is referred to as the <em>leaves</em> of the merkle tree. The leaves need to be sorted to make the tree root deterministic. The type information is sorted using their unique identifiers and for the <code>Enumeration</code>, variants are sort using their <code>index</code>. After sorting and hashing all leaves, two leaves have to be combined to one hash. The combination of these of two hashes is referred to as a <em>node</em>.</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>let nodes = leaves;
while nodes.len() &gt; 1 {
let right = nodes.pop_back();
let left = nodes.pop_back();
nodes.push_front(blake3::hash(scale::encode((left, right))));
}
let merkle_tree_root = if nodes.is_empty() { [0u8; 32] } else { nodes.back() };
<span class="boring">}</span></code></pre></pre>
<p>The <code>merkle_tree_root</code> in the end is the last node left in the list of nodes. If there are no nodes in the list left, it means that the initial data set was empty. In this case, all zeros hash is used to represent the empty tree. </p>
<p>Building a tree with 5 leaves (numbered 0 to 4):</p>
<pre><code>nodes: 0 1 2 3 4
nodes: [3, 4] 0 1 2
nodes: [1, 2] [3, 4] 0
nodes: [[3, 4], 0] [1, 2]
nodes: [[[3, 4], 0], [1, 2]]
</code></pre>
<p>The resulting tree visualized:</p>
<pre><code> [root]
/ \
* *
/ \ / \
* 0 1 2
/ \
3 4
</code></pre>
<p>Building a tree with 6 leaves (numbered 0 to 5):</p>
<pre><code>nodes: 0 1 2 3 4 5
nodes: [4, 5] 0 1 2 3
nodes: [2, 3] [4, 5] 0 1
nodes: [0, 1] [2, 3] [4, 5]
nodes: [[2, 3], [4, 5]] [0, 1]
nodes: [[[2, 3], [4, 5]], [0, 1]]
</code></pre>
<p>The resulting tree visualized:</p>
<pre><code> [root]
/ \
* *
/ \ / \
* * 0 1
/ \ / \
2 3 4 5
</code></pre>
<h3 id="inclusion-in-an-extrinsic"><a class="header" href="#inclusion-in-an-extrinsic">Inclusion in an Extrinsic</a></h3>
<p>To ensure that the offline wallet used the correct metadata to show the extrinsic to the user the metadata hash needs to be included in the extrinsic. The metadata hash is generated by hashing the SCALE encoded <code>MetadataDigest</code>:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>blake3::hash(SCALE::encode(MetadataDigest::V1 { .. }))
<span class="boring">}</span></code></pre></pre>
<p>For the runtime the metadata hash is generated at compile time. Wallets will have to generate the hash using the FRAME metadata. </p>
<p>The signing side should control whether it wants to add the metadata hash or if it wants to omit it. To accomplish this it is required to add one extra byte to the extrinsic itself. If this byte is <code>0</code> the metadata hash is not required and if the byte is <code>1</code> the metadata hash is added using <code>V1</code> of the <code>MetadataDigest</code>. This leaves room for future versions of the <code>MetadataDigest</code> format. When the metadata hash should be included, it is only added to the data that is signed. This brings the advantage of not requiring to include 32 bytes into the extrinsic itself, because the runtime knows the metadata hash as well and can add it to the signed data as well if required. This is similar to the genesis hash, while this isn't added conditionally to the signed data. So, to recap:</p>
<ul>
<li>Included in the extrinsic is <code>u8</code>, the &quot;mode&quot;. The mode is either <code>0</code> which means to not include the metadata hash in the signed data or the mode is <code>1</code> to include the metadata hash in <code>V1</code>.</li>
<li>Included in the signed data is an <code>Option&lt;[u8; 32]&gt;</code>. Depending on the mode the value is either <code>None</code> or <code>Some(metadata_hash)</code>.</li>
</ul>
<h2 id="drawbacks"><a class="header" href="#drawbacks">Drawbacks</a></h2>
<p>The chunking may not be the optimal case for every kind of offline wallet.</p>
<h2 id="testing-security-and-privacy"><a class="header" href="#testing-security-and-privacy">Testing, Security, and Privacy</a></h2>
<p>All implementations are required to strictly follow the RFC to generate the metadata hash. This includes which hash function to use and how to construct the metadata types tree. So, all implementations are following the same security criteria. As the chains will calculate the metadata hash at compile time, the build process needs to be trusted. However, this is already a solved problem in the Polkadot ecosystem by using reproducible builds. So, anyone can rebuild a chain runtime to ensure that a proposal is actually containing the changes as advertised.</p>
<p>Implementations can also be tested easily against each other by taking some metadata and ensuring that they all come to the same metadata hash.</p>
<p>Privacy of users should also not be impacted. This assumes that wallets will generate the metadata hash locally and don't leak any information to third party services about which chunks a user will send to their offline wallet. Besides that, there is no leak of private information as getting the raw metadata from the chain is an operation that is done by almost everyone.</p>
<h2 id="performance-ergonomics-and-compatibility"><a class="header" href="#performance-ergonomics-and-compatibility">Performance, Ergonomics, and Compatibility</a></h2>
<h3 id="performance"><a class="header" href="#performance">Performance</a></h3>
<p>There should be no measurable impact on performance to Polkadot or any other chain using this feature. The metadata root hash is calculated at compile time and at runtime it is optionally used when checking the signature of a transaction. This means that at runtime no performance heavy operations are done. </p>
<h3 id="ergonomics--compatibility"><a class="header" href="#ergonomics--compatibility">Ergonomics &amp; Compatibility</a></h3>
<p>The proposal alters the way a transaction is built, signed, and verified. So, this imposes some required changes to any kind of developer who wants to construct transactions for Polkadot or any chain using this feature. As the developer can pass <code>0</code> for disabling the verification of the metadata root hash, it can be easily ignored.</p>
<h2 id="prior-art-and-references"><a class="header" href="#prior-art-and-references">Prior Art and References</a></h2>
<p><a href="https://github.com/polkadot-fellows/RFCs/pull/46">RFC 46</a> produced by the Alzymologist team is a previous work reference that goes in this direction as well.</p>
<p>On other ecosystems, there are other solutions to the problem of trusted signing. Cosmos for example has a standardized way of transforming a transaction into some textual representation and this textual representation is included in the signed data. Basically achieving the same as what the RFC proposes, but it requires that for every transaction applied in a block, every node in the network always has to generate this textual representation to ensure the transaction signature is valid.</p>
<h2 id="unresolved-questions"><a class="header" href="#unresolved-questions">Unresolved Questions</a></h2>
<p>None.</p>
<h2 id="future-directions-and-related-material"><a class="header" href="#future-directions-and-related-material">Future Directions and Related Material</a></h2>
<ul>
<li>Does it work with all kind of offline wallets?</li>
<li>Generic types currently appear multiple times in the metadata with each instantiation. It could be may be useful to have generic type only once in the metadata and declare the generic parameters at their instantiation. </li>
<li>The metadata doesn't contain any kind of semantic information. This means that the offline wallet for example doesn't know what is a balance etc. The current solution for this problem is to match on the type name, but this isn't a sustainable solution.</li>
<li><code>MetadataDigest</code> only provides one <code>token</code> and <code>decimal</code>. However, chains support a lot of chains support multiple tokens for paying fees etc. Probably more a question of having semantic information as mentioned above.</li>
</ul>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../approved/0059-nodes-capabilities-discovery.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../approved/0084-general-transaction-extrinsic-format.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../approved/0059-nodes-capabilities-discovery.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../approved/0084-general-transaction-extrinsic-format.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script>
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js"></script>
<script src="../mark.min.js"></script>
<script src="../searcher.js"></script>
<script src="../clipboard.min.js"></script>
<script src="../highlight.js"></script>
<script src="../book.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>