Revised 2021-02-23.
Cryptocurrencies have a problem. I mean, there’s many problems, some of them are really substantial.
But one that I think is a pretty big blocker to ecosystem health is the lack of good wallet infrastructure. In some places this is less of a problem than others, but I’ll get into that later.
Here’s a list of requirements that I think a good crypto wallet should have, in no particular order:
hardware wallet support: this one is a no-brainer, in some cases this isn’t possible (a lightning node needs some hot privkeys)
core portability: the internal heavy lifting should compile on any platform, to native code
remote control: I should be able to use nodes embedded into the program or remotely, or I should be able to use it the interface as a remote control of a “wallet node” living elsewhere, maybe we should even be able to communicate over SSH tunnels
no web bullshit: related to the above, see below for details (// TODO extend this more)
extensible: you should be able to use the internal libraries to build tools for whatever protocol you’re building on top of a network
Certain crypto ecosystems have a reliance on web technology.
Web browsers are bad. Google essentially controls the standardization process and Mozilla, while not as evil as Google, is woefully mismanaged and does Firefox does not have the market share to challenge Google’s near-hegemony. Google has an incentive to prevent adoption of decentralized technology, and if they become threatened by it they will absolutely take steps to disrupt it. Reliance on web is a liability, even ignoring the security risks.
Javascript (and HTML) is a bad language/environment for many reasons:
wibbly wobbly types make it impossible to trust your program
wibbly wobbly types put a lot more emphasis on using tests to ensure correctness, which is bad beacuse
tests can’t cover all cases, cases that a rich type system would handle trivially
programmers don’t like writing tests, so they inevitably skip writing them
linters can’t handle every mistake
the lack of a sufficient standard library makes heavy reliance on upstream packages for most functionality a necessity
the low barrier to entry means that NPM is filled with low quality and under-maintained packages
the DOM is for documents, its layout engines are not as well-suited interactive applications
the lack of types make runtimes have to do much more work to make programs run at reasonable speeds
the extra work from runtimes means programs use much more memory, and are still slower and less responsive
the dominant web frameworks add more layers of abstractions to solve problems and introduce new ones, which exacerbates the above points
More generally, the web is a bad platform for distributing applications used in decentralized networks because browsers are fundamentally designed around a model of “clients that request pages from servers”. Breaking out of this model while using the same technology, as some do try, is difficult. When it goes wrong we see disasters like Mist.
The sad part is Mist was actually a neat idea. Instead of shipping the wallet as an extension that injects functionality into pages, the idea is that we’d distribute dapps as the pages themselves and they’d run in a kind of dapp browser. But the execution of this idea was flawed and insecure, as relying on Electron and the web stack is what doomed it.
The narrative that it’s a good platform for due to its native sandboxing and ease of distribution is flawed in this space. Sure, we have hardware wallets, but with more complexity in the applications we interact with it’s hard to ensure that the transactions you’re signing are actually doing as they behave. So we have to do some level of auditing on the software, making the need for sandboxing it less strong. But regardless, once we audit what we get from the site we’re using it’s trivial for the maintainer to push a new version without our knowledge. There’s ways around this (hosting on Skynet/IPFS/etc.), but these have their own limitations, don’t solve all the issues with the web, and aren’t as well-understood by users as desktop applications.
So if you’re afraid of your application being malicious your only choice is to audit the code and then build it from source before you use it. And then all the arguments about the web being an ideal distribution platform are moot! Obviously most people don’t build their software from soruce. On Windows it’s even a pretty big pain in the ass. But most platforms at this point are moving to a system with package managers and a more sensible distribution model. System package repository maintainers ideally are a lot more trustworthy, and third parties to manage downstream patches make it a lot more difficult for the authors to sneak in a malicious update without it being noticed.
So yeah, the web is great for application distibution if you want to blindly trust things. Which most users do, even if that’s bad, which leads us to our next point.
The strict client-server model is also bad because it doesn’t map into how distributed protocols are usually architected. You can try, there’s WebTorrent and such, but it’s a ton of work and involving web protocols under the hood infects the architecture of the program and increase the work you have to do.
Just using the web prompts developers to just rely on centralized third party services like Infura to provide data about what’s happening on the chain.
If the way most users interact with the network isn’t in a decentralized fashion, then what’s the point of using cryptocurrencies in general? If we can make running nodes that improve the health of the network as easier to do, then it’s a lot easier to justify the sensible default being that every user runs a node without them having to worry about it.
As developers, we have a set of skills that most don’t. We have an understanding of technology that most users shouldn’t have to learn. We can’t just say “users will choose the wallet that suits their needs”, since most users will just do the thing that’s easiest and cheapest for them. We have to build good wallets that are:
easy to understand, according to the conventions of the platforms they’re on
cheap, relying on secure L2s where possible
fast and efficient, using languages that compile to native code to do heavy lifting
safe, not relying on centralized third parties and avoiding information leakage
We have a responsibility to build good wallets and put the needs of users above our own, as users are dependent on us to do due diligence and not take shortcuts when developing software just because it makes our lives easier.
This doctrine applies to user-facing software more generally, not just wallets.
This document isn’t really supposed to be concerned with what the wallets do specifically, more about how they do it. But there’s a few higher level features that I think should be mentioned.
multisigs, obviously
timelocks, vaults, etc.
Not every wallet needs to support these features, but they’re important considerations to have. Privacy features especially are important because often it’s the case that the more people that use them, the more effective they are.
This article is primarily aimed at Ethereum wallets since it’s a major network and most of the wallets really suck.
Functionally they do a lot, and a lot of people get a lot of benefit from them. Which is great. But we, as an industry, can do a lot better.
// TODO
// TODO
// TODO
Overall pretty good but being in C# makes it less portable.
// TODO
To use their Lightning integration you have to either rely on their custodial service or use a Node.js server that relies on deprecated Bitcoin Core features, which is less than ideal.
Most of them are really great, and do support Lightning.
But I wish wallets like Phoenix gave you the option to run the node separately from the phone and remote control it. That way recovery can be easier if you lose your phone. But that use case is reasonably well-suited by Zap at the moment.
So what do we do about this situation?
Just build a wallet that checks all the boxes in that list at the top. But how?
First of all, Rust is the only really suitable choice to build this. You could argue that you can use Go, since you can kinda use Go on every platform you could want to. But there’s extra effort required in calling into Go from higher level languages. But also Go sucks, don’t use it. C/C++ might also be suitable but Rust just has better tooling for this kinda stuff and is just nicer and more productive overall.
I can’t think of any other language that would be suitable. Something that’s necessary is that it must be reasonably easy to call into it from any other language and not have strong runtime requirements (like a GC or something), and only languages that I know of that provide this that are popular are C, C++, and Rust. There’s also things like D and Zig that you might be able to get away with but like who knows those?
I haven’t completely sorted out the nomenclature but there’s a general idea.
Our wallet library is constructed out of various, fairly general, pieces.
component: some library or external connection that provides one or more services, which may be constructed in terms of other services
services: standardized piece of functionality that can be provided by different components to accomplish some kind of behavior, to share intent logic across different kinds of systems
intent: an action taken by the user effecting the outside world, interacting with services and optionally depending on some resources
resources: provided by services, something that exists conceptually outside of the wallet
An example of a component would be an RPC connection to a bitcoind
or geth
node. It has a certain amount of configuration to initialize and some properties about its state that it exposes (like if it’s running, booting, shutting down, etc.). Components can expose services. Services are more standardized and give us access to more general behaviors. A goal is that we can assemble (“virtual”?) components that rely on some services, which we would use to expose services to higher-layer protocols (such as uniswap).
The high level wallet UI will be designed in terms of user intents. Some applications are already moving to this model, but it isn’t very popular yet. Android technically already has a concept similar to this, which is designed for inter-app communication.
Some intents may resolve immediately, and some may take time to resolve. But ongoing intents should be made available to the user, possibly allowing the user to abort it.
Examples of intents:
“send 2 Bitcoins to address bc1p...
”, completing when the transaction is confirmed/buried
“generate an address”, completing immediately (or an invoice, etc.)
“fulfill invoice lnbc1...
”, completing when we receive the preimage or the payment expires
“bridge 100 DAI from ETH mainnet to zkSync”, completing when the funds are available on the L2
“open a channel with peer 03cafebabe
”, completing when the channel is opened (and also closing, splicing, etc)
“upload a file to a Sia host”, completing when, yk, it’s uploaded
So note that number 3 involved two networks. This is where resources come in. Intents may require resources to operate on. Some of these are pretty broad like “a bitcoind
’s embedded wallet”, “x amount of funds on Foo ledger”, etc. I haven’t completely sorted out how intents are supposed to describe the resources they require (especially when it’s a fungible kind of thing). I also haven’t completely sorted out the relationship of how services should provide and allocate resources to intents.
But what’s relevant here is intents may require exclusive or shared locks on resources. An intent that spends funds would require exclusive ownership of the resource. Then we can schedule execution of intents such that they can’t compete with each other. A coinjoin or an atomic swap intent might require locks on specific utxos, whereas simple sends can be more coarse.
What this gives us is it lets us reason about future intents that we haven’t tried to begin yet. If we bridge funds from one network to another, we can expect that we’ll have the resource on the destination ledger once that intent completes. If we’re careful about how we design intents, a user should be able to schedule several intents into the future and then leave the wallet to deal with it later.
This sounds like a lot of complexity, which it is, but I’m confident that we can use a design language like this to build a very powerful wallet infrastructure and expose a subset of it to build a very well-polished series of wallets.
// TODO basically mist but with flatpak/wasm/etc and more restricted access to UIs