In the last few months (Dec’21, Jan, Feb) our team had been working on adopting wallet core of Aditya’с fantastic Zecwallet Lite for usage with the Zephyr wallet. This task turned out to be very different from the work that we have done for the earlier milestone in 2021. This time we had to shift our focus from high-level tasks like:
- produce UI designs and high-fidelity wireframes for the Zephyr wallet
- create PoC that a Zcash wallet can live within the sandbox of Chrome/Firefox extension
to low-level stuff:
- Wasm bindings
- gRPC / RPC protocols
- protobuf & formats
- async runtimes
- Transport layer with Envoy <> lightwalletd
Our last year’s status report was very visual (as it contained a lot of UI screenshots). This status report instead of much more technical. Thus in order to explain what we worked on, I am required to provide a high-level view of the crypto wallet architecture.
We can start our architectural refresher with the simplest crypto wallet there is - a CLI wallet. It takes input directly from user via command line arguments
A:\> wallet send -to t123456 -value 42zec
The CLI wallet is compiled to native code (x86 instructions) and has full access to the Operating System and underlying primitives (eg. network sockets).
Here’s a diagram which represents CLI wallet architecture at glance.
- user interacts with CLI wallet via command line: issuing a Rescan command
- CLI commands are eventually handled by the module called
LightClientwhich encapsulates most of the operational logic
LightClientneeds to communicate with the
lightwalletdserver, it goes through number of layers:
- RPC module which is based on Tokio runtime
- Operating System which provides network sockets for communication
- The last (orange) step of the diagram is TCP/IP connection to the
lightwalledserver to do RPC over
Having access to the operating system means that architectural diagram of an Electron based wallet would be conceptually similar to the architectural diagram of a CLI app. There will be few small changes though:
- There can be a rich interactive UI
- UI logic is written using JS/HTML
- UI code can communicate with NodeJS runtime via Electron built in mechanisms
- NodeJS runtime can execute native code (eg. C-libraries or Rust-libraries) via Node’s NAPI (the green rectangle)
You can spot some differences in the upper part of the diagram, but if you look at the bottom parts of the diagram, you will see that everything from zecwalletlite-lib, “Operating System” and “lightwalletd” components are presented in exactly same way as on the CLI App diagram and are using native code (eg. x86).
The architecture of a web-wallet has very significant differences vs CLI or Desktop wallets. Zephyr, which is a wallet delivered to the user as a Chrome/Firefox extension is a subset of a web wallet.
So let’s have a look at the architecture diagram to and spot those significant differences.
The biggest changes are represented by the two large red rectangles. They represent “bridges” (between JS and WASM world) which are sandwiching
You can notice that this diagram does not have anymore any native code blocks (all “x86” icons are replaced by “JS” or “WA”-WebAssembly icons). All the code is either JS or pure WASM.
There is no access to the underlying Operating System anymore. And that means that there is no access to networking primitives like sockets neither.
Without access to the OS and sockets we would need to find an alternative way to perform RPC calls, as RPC and networking mechanisms from CLI Wallet or Electron Desktop App Wallet do not apply anymore. Our web-stack can only perform HTTP requests via
fetch()/XHR browser API. As our host is browser, not OS.
To bridge HTTP requests and the
lightwalletd server and protocol, an Envoy proxy is introduced. Also our wallet-core, executed as WASM module is has to use
to ask browser to perform RPC calls destined at lightwalletd. This results in slightly cumbersome multi-step architecture with lots of intermediaries and moving parts. And that’s where most of our work and research effort was put in the last few months.
Now that you have had a chance to get introduced to the architecture of the wallet, let’s have a glimpse into some of the technical things we’ve been working on.
Our main focus was adopting
zecwallet-cli/lib code to work in WASM environment (as per diagram #3).
The fantastic codebase of Aditya’s from zecwallet-lite#wasm (https://github.com/adityapk00/zecwallet-lite/tree/wasm) was used as a reference of the changes required to be ported to
However the latest versions of
zecwallet-cli/lib have moved forward significantly since the #wasm branch (eg. Aditya added Blaze sync implementation).
Our intention was to be able to maintain a fork of
zecwallet-cli/lib code which is WASM compilable and can accept upstream changes from Aditya’s commits.
The challenges and tasks we had to tackle:
- implementation of async operations within wasm code and brining in alternative async runtime into our code (wasm-compatible instead of Tokio)
- sorting out usage of c-library within the zecwallet-lib core (when dealing with c-library secp256k1, we tried making it work as WASM module using emscripten, but eventually realized that Aditya’s approach from 2021 of using pure-Rust implementation of libsecp256k1 is a much simpler and cleaner)
- Preparing internal documentation and research notes re: gRPC, protobuf, wasm<>js interop, Envoy RPC, etc
- Fixing issues with Envoy proxy. (there were subtle differences in gRPC generated code produced by different versions of
protobuf-codegen); fixing timing, handling empty payloads, CORS
In order to finish our current milestone, in the next few weeks we will continue working on:
- removing Rust’s Tokio dependency from wallet code (Tokio async runtime does not work in WASM)
- porting Blaze sync code for WASM (mostly removing Tokio runtime dependencies)
- wasm—>js bridge related work (handling gRPC calls)
- debugging RPC calls and Envoy Proxy interaction