Polkadot Host implementations use Fast/Warp sync to synchronize missing block headers. They are followed by the state sync. The faster we can synchronize missing block headers and the most recent state, the faster our node will be able to start executing the most recent blocks and create new blocks (if we are running validator). In this post we will explore current strategies to sync the state as well as propose a new approach. **The goal is to come up with state sync protocol that:** **1. Easily parallelizable** **2. To save bandwidth responses do not contain overlapping data** **3. Resistant to DOS attacks** If you are already familiar with how state sync works, just scroll down to [Proposed solution](.Proposed_solution) section. ## State ### Key-value Each block in polkadot blockchain has associated state. State is key-value collection. <pre style="background: #161616; color: #d4d4d4"> state["money:alice"] = "100$" state["money:bob"] = "200$" </pre> ### State hash Block state is immutable. Each block stores hash of state **in a form of merkle root** to ensure state integrity. Forged state will have different hash. <pre style="background: #161616; color: #d4d4d4"> <span style="color: #ff9999;">hash1</span> = hash(state({ "money:alice": <span style="color: #ff9999;">"100$"</span> })) <span style="color: #66b2ff;">hash2</span> = hash(state({ "money:alice": <span style="color: #66b2ff;">"1000$"</span> })) <span style="color: #ff9999;">hash1</span> != <span style="color: #66b2ff;">hash2</span> </pre> ### Polkadot trie Polkadot stores state as merkle trie. #### Trie Trie is prefix tree. Nodes with common prefix are children of parent node with that prefix. <pre style="background: #161616; color: #d4d4d4"> [ "<span style="color: #99ff99;">money:</span>alice", "<span style="color: #99ff99;">money:</span>bob", ] -> node("<span style="color: #99ff99;">money:</span>", [ node("<span style="color: #99ff99;">money:</span>alice"), node("<span style="color: #99ff99;">money:</span>bob"), ]) </pre> Parent node stores children by letter which follows parent prefix. <pre style="background: #161616; color: #d4d4d4"> [ "<span style="color: #99ff99;">money:</span><span style="color: #ff9999;">a</span>lice", "<span style="color: #99ff99;">money:</span><span style="color: #66b2ff;">b</span>ob", ] -> node("<span style="color: #99ff99;">money:</span>", { "<span style="color: #ff9999;">a</span>": node("<span style="color: #99ff99;">money:</span><span style="color: #ff9999;">a</span>lice"), "<span style="color: #66b2ff;">b</span>": node("<span style="color: #99ff99;">money:</span><span style="color: #66b2ff;">b</span>ob"), }) </pre> Node key omits redundant prefix. <pre style="background: #161616; color: #d4d4d4"> [ "<span style="color: #99ff99;">money:</span><span style="color: #ff9999;">a</span><span style="color: #ffcc99;">lice</span>", "<span style="color: #99ff99;">money:</span><span style="color: #66b2ff;">b</span><span style="color: #99ffff;">ob</span>", ] -> node(partial="<span style="color: #99ff99;">money:</span>", { "<span style="color: #ff9999;">a</span>": node(partial="<span style="color: #ffcc99;">lice</span>"), "<span style="color: #66b2ff;">b</span>": node(partial="<span style="color: #99ffff;">ob</span>"), }) </pre> #### Merkle trie Merkle trie is merkle tree. Block stores hash of root trie node. Immutable node is uniquely identified by it's hash. Parent node stores child hash. Polkadot node database stores node by hash. <pre style="background: #161616; color: #d4d4d4"> <span style="color: #ff9999;">trie1 = node("money:"</span>, [ <span style="color: #ffcc99;">node("money:alice"="100$")</span>, <span style="color: #99ff99;">node("money:bob"="200$")</span>, ]) <span style="color: #66b2ff;">trie2 = node("money:"</span>, [ <span style="color: #99ffff;">node("money:alice"="1000$")</span>, <span style="color: #99ff99;">node("money:bob"="200$")</span>, ]) -> { <span style="color: #ff9999;">hash1: node("money:"</span>, [<span style="color: #ffcc99;">hash3</span>, <span style="color: #99ff99;">hash4</span>]), <span style="color: #66b2ff;">hash2: node("money:"</span>, [<span style="color: #99ffff;">hash5</span>, <span style="color: #99ff99;">hash4</span>]), <span style="color: #ffcc99;">hash3: node("money:alice"="100$")</span>, <span style="color: #99ff99;">hash4: node("money:bob"="200$")</span>, <span style="color: #99ffff;">hash5: node("money:alice"="1000$")</span>, } </pre> ## /state/2 Polkadot "/state/2" protocol is used to obtain state from other peers. There are two subprotocols inside "/state/2" protocol. They are distinguished by `request.proof: bool` field. ### /state/2 proof=false This protocol requests next page of key-values. It stops when `response.end: bool` field is `true`. Then it re-creates trie from key-values. <pre style="background: #161616; color: #d4d4d4"> step 1 request(after="") -> response( "money:alice"="100$", <span style="color: #ff9999;">"money:bob"</span>="200$", end=false, ) step 2 request(after=<span style="color: #ff9999;">"money:bob"</span>) -> response( "money:charlie"="300$", "money:dave"="400$", end=<span style="color: #66b2ff;">true</span>, ) </pre> #### Problems We can't check root hash until we receive all key-values. Re-creating trie nodes from key-values may change root hash (inconsistency during trie v1 migration). Waiting for all requests sequentially may be slow. We may want to send parallel requests to different peers. But we don't know range of keys in trie. We may blindly guess keys, but responses may overlap **, increasing bandwidth**. <pre style="background: #161616; color: #d4d4d4"> step 1 (parallel) request(after=<span style="color: #ff9999;">"a"</span>) request(after=<span style="color: #66b2ff;">"b"</span>) request(after=<span style="color: #99ff99;">"c"</span>) -> response([<span style="color: #ff9999;">"a"</span>, "b", "c"]) response([ <span style="color: #66b2ff;">"b"</span>, "c"]) response([ <span style="color: #99ff99;">"c"</span>]) </pre> ### /state/2 proof=true This protocol requests trie nodes (merkle proof) required to get next page of key-values. **That increases response size, especially if trie is getting deeper.** It stops when client receives all trie nodes. <pre style="background: #161616; color: #d4d4d4"> step 1 request(after="") -> response({ <span style="color: #99ff99;">hash1</span>: node("money:", [<span style="color: #66b2ff;">hash2, hash3</span>, hash4, hash5]), <span style="color: #66b2ff;">hash2</span>: node("money:alice"), <span style="color: #66b2ff;">hash3</span>: node(<span style="color: #ff9999;">"money:bob"</span>), }) step 2 request(after=<span style="color: #ff9999;">"money:bob"</span>) -> response({ <span style="color: #99ff99;">hash1</span>: node("money:", [hash2, hash3, <span style="color: #66b2ff;">hash4, hash5</span>]), <span style="color: #66b2ff;">hash4</span>: node("money:charlie"), <span style="color: #66b2ff;">hash5</span>: node("money:dave"), }) </pre> We can check root hash for each request. It receives trie nodes as is, so doesn't need to re-create them. Prefixes from received nodes limit possible keys **for future requests**. But we don't know how many keys exist under prefix, so responses may overlap. <pre style="background: #161616; color: #d4d4d4"> step 1 request(after="") -> response({ <span style="color: #99ff99;">hash1</span>: node(<span style="color: #99ff99;">"money:"</span>, { <span style="color: #ff9999;">"a": hash2</span>, <span style="color: #ffcc99;">"b": hash3</span>, <span style="color: #66b2ff;">"c": hash4</span>, <span style="color: #99ffff;">"d": hash5</span>, }), }) step 2 (parallel) request(after="<span style="color: #99ff99;">money:</span><span style="color: #ff9999;">a</span>") request(after="<span style="color: #99ff99;">money:</span><span style="color: #ffcc99;">b</span>") request(after="<span style="color: #99ff99;">money:</span><span style="color: #66b2ff;">c</span>") request(after="<span style="color: #99ff99;">money:</span><span style="color: #99ffff;">d</span>") -> response([<span style="color: #99ff99;">hash1</span> <span style="color: #ff9999;">hash2</span> hash3]) response([<span style="color: #99ff99;">hash1</span> <span style="color: #ffcc99;">hash3</span> hash4]) response([<span style="color: #99ff99;">hash1</span> <span style="color: #66b2ff;">hash4</span> hash5]) response([<span style="color: #99ff99;">hash1</span> <span style="color: #99ffff;">hash5</span>]) </pre> Even sequential request responses duplicate parent nodes (root, "m", "mo", "mon", "money:"). ## Problems - Responses duplicate parent nodes. - Uses more network bandwith. - Client already knows these nodes. - Parallel responses overlap. - Uses more network bandwith. - Reduces profit of parallel over sequential requests. ## Proposed solution Protocol can request trie nodes by hashes. Response will contain one or more requested nodes. Response may contain children/decendants of requested nodes. Unknown child hashes from response can be used in new requests. Unknown hashes can be requested in parallel from different peers. Response doesn't return keys outside of requested node prefix. Client requests non-overlapping subtrees. So responses don't overlap. Response doesn't contain parent nodes, so it doesn't duplicate them. <pre style="background: #161616; color: #d4d4d4"> tree <span style="color: #99ff99;">hash1 => node</span>([ <span style="color: #ff9999;">hash2 => node</span>([ <span style="color: #ffcc99;">hash4 => node</span>(…) hash5 => node(…) ]) <span style="color: #66b2ff;">hash3 => node</span>([ <span style="color: #99ffff;">hash6 => node</span>(…) hash7 => node(…) ]) ]) step 1 request(<span style="color: #99ff99;">hash1</span>) -> response(<span style="color: #99ff99;">node</span>([<span style="color: #ff9999;">hash2</span>, <span style="color: #66b2ff;">hash3</span>])) step 2 (parallel) request(<span style="color: #ff9999;">hash2</span>) request(<span style="color: #66b2ff;">hash3</span>) -> response(<span style="color: #ff9999;">node</span>([<span style="color: #ffcc99;">hash4</span>, hash5])) response(<span style="color: #66b2ff;">node</span>([<span style="color: #99ffff;">hash6</span>, hash7])) step 3 (parallel) request(<span style="color: #ffcc99;">hash4</span>) request(hash5) request(<span style="color: #99ffff;">hash6</span>) request(hash7) -> response(<span style="color: #ffcc99;">node</span>(…)) response(node(…)) response(<span style="color: #99ffff;">node</span>(…)) response(node(…)) done </pre> If client needs only particular keys (like "/light/2" protocol), or wants to receive them as early as possible, request can contain such keys. Server will return nodes on path along this key, ignoring nodes outside this prefix. ### Spam Malicious client can request many invalid hashes from server. Server may not have all valid nodes, so it can't prove that hash is invalid. Server will process these hashes independently. Such invalid request with N invalid hashes will be handled in O(N) db reads. Client may prove server that requested hashes exist in trie. Simple merkle proof can consist of trie nodes containing requested hashes. Valid node hash must be root, or child of valid node. Server already knows all valid trie nodes which client may request. So proof can use hashes instead of nodes. Proof and request hashes will be organized as tree, to preserve parent-child relation. <pre style="background: #161616; color: #d4d4d4"> request( hashes=[<span style="color: #ff9999;">hash8, hash7</span>], proof={ <span style="color: #66b2ff;">hash1</span>: node(…, [<span style="color: #66b2ff;">hash2</span>, hash3, hash4, …]), <span style="color: #66b2ff;">hash2</span>: node(…, [<span style="color: #66b2ff;">hash5</span>, hash6, <span style="color: #ff9999;">hash7</span>, …]), <span style="color: #66b2ff;">hash5</span>: node(…, [<span style="color: #ff9999;">hash8</span>, hash9, …]), }, ) -> compact(<span style="color: #66b2ff;">hash1</span>, [ compact(<span style="color: #66b2ff;">hash2</span>, [ compact(<span style="color: #66b2ff;">hash5</span>, [ compact(<span style="color: #ff9999;">hash8</span>, []), ]), compact(<span style="color: #ff9999;">hash7</span>, []), ]), ]) </pre> Proof validation bound is same for valid and invalid proof. First db read will check if server has root node. If it doesn't it stops. If it has node, before next db read it will check that next hash is child of current node. So invalid hashes won't be read from db. <pre style="background: #161616; color: #d4d4d4"> <span style="color: #66b2ff;">hash1</span> = node([<span style="color: #99ffff;">hash2</span>, hash3]) valid = compact(<span style="color: #66b2ff;">hash1</span>, [ compact(<span style="color: #99ffff;">hash2</span>, […]), ]) invalid = compact(<span style="color: #66b2ff;">hash1</span>, [ compact(<span style="color: #99ffff;">hash2</span>, […]), compact(<span style="color: #ff9999;">invalid1</span>, […]), compact(<span style="color: #ff9999;">invalid2</span>, […]), … ]) </pre>