From 28e068705595678d709cbb5f1e7084c52d987b39 Mon Sep 17 00:00:00 2001 From: Hanh Date: Sat, 19 Jun 2021 20:10:19 +0800 Subject: [PATCH] some doc --- docs/index.md | 90 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/merkle.md | 12 +++++++ docs/report.png | Bin 0 -> 22654 bytes 3 files changed, 102 insertions(+) create mode 100644 docs/merkle.md create mode 100644 docs/report.png diff --git a/docs/index.md b/docs/index.md index cb57220..716d47b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,10 @@ +--- +title: Warp Sync +date: 2021-06-19T19:38:21+08:00 +--- + +{{ page.date | date_to_string }} + ## Abstract The initial synchronization of a wallet with the Zcash blockchain is one of the most often mentioned issues with light clients. @@ -10,6 +17,89 @@ We introduce a new method of synchronization that leverages parallel execution a Our synchronization method does not incrementally update the wallet state block per block. Instead, it directly computes the state at the current block height. Therefore it reduces the amount of calculation immensely. +## Intro +For light wallets, the initial sync time is a big issue. Depending on how many notes a shielded wallet has, it can take several minutes to hours to completely scan the blockchain. +Today, I'd like to introduce a technique that can scan from sapling activation height in **less than 15 seconds** (on a desktop machine) even for big wallets. +Besides the speed benefits, warp sync creates the same outputs as normal sync (byte for byte). It allows you to mix different strategies: warp to recent block then incrementally sync the remaining ones. +Or, Warp sync for the initial sync then incrementally sync afterward. + +Roughly speaking, it calculates the final state (at the current height) without applying every block one by one. Most of the processing is also done in parallel. + +Example of a run: + +```text +[2021-06-19T02:22:34Z INFO sync::scan] Download chain: 5751 ms +[2021-06-19T02:22:40Z INFO sync::scan] Decrypt Notes: 5983 ms +[2021-06-19T02:22:40Z INFO sync::chain] Build CMU list: 53 ms - 675844 nodes +[2021-06-19T02:22:43Z INFO sync::chain] Tree State & Witnesses: 2234 ms +[2021-06-19T02:22:43Z INFO sync::scan] # Witnesses 9 +[2021-06-19T02:22:43Z INFO sync::scan] Total: 14024 ms +``` + +- Downloading the compact blocks from lightwalletd took 5751 ms. It's from a server that runs locally. Network speed matters a lot here. +- Looking for our notes took 5983 ms. It has to trial decrypt every output using our IVK and it is performed in parallel across all CPUs. +- It built the list of note commitments, i.e. the leaves of the commitment tree. It took 53 ms. The tree has 675844 nodes, i.e. that's the total number of spend outputs since activation. +- It calculated the current tree state and the witness for each of our received notes in 2234 ms. This is where the massive gains come from. This step could have taken hours. We will describe how Warp Sync performs this step differently. + +At this point, we are completely synced. There is no additional data or computation done in the background. The wallet is ready. + +Here's a chart of running time with 10 runs. It looks pretty consistent. + +![Report](./report.png) + +As mentioned earlier, the most time-consuming step is the maintenance of the tree state and the witness data. That's required if you want to spend. + +## Commitment Tree + +The commitment tree contains all the output note commitments (basically a hash value). There are 675844 of them and they are 32 bytes long. They form the leaves. Then you hash two of them at a time to build the 2nd layer. And repeat until there is a single value, the root. + +Here's a nice explanation: +https://en.wikipedia.org/wiki/Merkle_tree + +**Hash calculations in zcash are relatively expensive** because it uses Pedersen Hash that involves elliptic curve computation. + +Our goal is to do the minimum number of hash calculations possible. + +The commitment tree is incremental and new nodes are always appended to the end. Nodes are never removed or updated. As such, the insertion of a new note requires *one hash calculation*. + +> A tree of N nodes can be built with about N hash calculations. + +That's very good and it's hard to do better than that. + +`librustzcash` has an object called `CommitmentTree` which supports appending a node in about constant time. It is used to compute the tree state at the end of each block. + +## Witness Data + +Our notes are in the tree as well and we know where they are. When we spend a note, we have to use the Merkle path of the note to build the transaction. The path of a given note remains the same (since a note never moves) but the values of the nodes that the path goes through, can change. For example, the value of the root node will always change. The process of updating the path values is called "updating the incremental witness". `librustzcash` has a class `IncrementalWitness` which keeps care of it. + +In the end, we have a `CommitmentTree` and a bunch of `IncrementalWitness` (one per note we received). On seeing a new transaction output, i.e. a new note, we use `.append` on `CommitmentTree` and `IncrementalWitness` and they update. + +In itself, adding a note is fast. But when we have to do ~675k hashes multiplied by the number of notes we received it becomes very slow. The more notes we receive, the worse it gets. + +(You could avoid updating witnesses if you know that the note has been spent but the scalability issue remains.) + +## Warp Sync + +From a very high level, Warp Sync uses the fact that the Incremental Witnesses are paths of the same tree. When we build the tree state and combine nodes to form each level, we can simultaneously update the Commitment Tree state and all the Witnesses without computing any hashes for the Witnesses. The algorithm reuses the work done when the Merkle Tree is built and is interleaved with its creation. + +The name "Warp Sync" comes from the idea that we don't travel through the path made by each modification to the Tree but we "warp" to the final state. It takes longer than a single incremental change but is much faster than applying them cumulatively. + +> Also, the number of witnesses has little impact on the run time. + +Having 1 000 witnesses only increased the run time by less than 1 sec. In traditional sync, the runtime would be x1000. + +Lastly, another optimization comes from the calculation of the Merkle tree. It combines two nodes independently from other nodes and therefore is a prime candidate for parallelization. + +# Conclusion + +This was an interesting investigation and I'm glad that we could find a method that speeds up the synchronization of a large number of blocks especially when the workflow is straightforward. +Warp Sync doesn't need advanced multitasking. I use `rayon` to do parallel iteration. That's all, no locks, no spawning tasks, workers, etc. The big gain comes from the algorithm itself. + +## More Info + +- [Merkle Tree](./merkle.md) +- Commitment Tree +- Witness Data diff --git a/docs/merkle.md b/docs/merkle.md new file mode 100644 index 0000000..4bff724 --- /dev/null +++ b/docs/merkle.md @@ -0,0 +1,12 @@ +The Zcash Merkle Tree is built by putting note commitments as leaves and then combining nodes using a Pedersen Hash function. + +**Notes never change**. Therefore once a node has both children, its value will remain constant. + +Lastly, notes are always added at the end. + +We are only interested in tracking our notes because we don't +need to validate transactions. We could check that the tree +is valid, i.e. the root matches the value published in the +block header but that's not a requirement. + +> We must be able to compute the Merkle Path of our tracked notes at the current block height. diff --git a/docs/report.png b/docs/report.png new file mode 100644 index 0000000000000000000000000000000000000000..d58da2ff978cf8e1283508fca5fb4170baab1eda GIT binary patch literal 22654 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 literal 0 HcmV?d00001