Comment on page
How our MPC was implemented
Logging into a self-custody crypto service is a nuanced concept because the user owns all the credentials on his own. In Rainmaker, the user's credentials are stored between Firebase and Torus. Together, these are used to construct a private key to an EOA wallet that owns a ZeroDev smart contract wallet. When the user wishes to execute transactions, he can use that EOA wallet in conjunction with the smart contract wallet to sign and execute. What's interesting is that the Rainmaker backend is never actually necessary for any of this process. So what is a Rainmaker account? We actually use the Rainmaker backend for just a few simple purposes:
- 1.It generates unsigned call data for the client to execute (deposits, routing swaps, etc).
- 2.It acts as a cache for blockchain history in the form of transaction receipts, wallet metrics, etc. This is mostly for our own accounting and useful display to the user. While the Rainmaker client could technically operate without the Rainmaker backend, we require it both from a UX and business perspective.
Since a user's credentials are self-custodied, we can't actually store their private key or password or any other sensitive info. Absent this, we can still validate that they are the wallet owner via cryptographic signatures. In order to sign up or log in, the user first generates all credentials, then signs a message to prove they own their account. The Rainmaker backend can then verify that signature and issue a JWT for use in subsequent requests. The flow looks roughly like this:
- 1.Client uses email/password or email/OTP or phone/OTP to generate an MPC share.
- 2.Client uses recovery key to generate an MPC.
- 3.Client uses shares to create a private key and associated EOA account/signer. This EOA account ultimately owns the user's ZeroDev smart contract wallet and can also be used for generating signatures.
- 4.Client signs a message comprised of a timestamp, EOA address, and smart contract address. These credentials are sent to the server for verification.
- 5.Server verifies the signature is valid, the signer matches the EOA address, the EOA account owns the smart contract account, and the timestamp is fresh.
- 6.If all credentials pass, the backend issues a fresh JWT which can be used for all future requests.
Steps 1-3 in the last section all describe some means to generate a private key. The rainmaker backend isn't opinionated at all which method the user chooses to generate that private key. In fact, the only reason we even record this on the backend is because we want to present the user with the right UI to log in.
- 1.User types in email and password.
- 2.We check "auth type". If it's TORUS_CYAN or TORUS_TESTNET, we know we'll use tkey for auth, but we also need to determine if the user has updated his secret share.
- 3.If the device already has a secret share stored in secure storage, use it. If not, we need to prompt the user: "If you set a share, type it in. If not, tell us that you never set one."
- 4.Regenerate the private key based on email/password and secret share. If the user never set one, use the default.
- 1.User types in email and password.
- 2.Ask the user if he'd like to set a secret share as a second form of auth. If not, use the default secret.
- 3.Generate the private key based on email/password and secret share.
Auth is actually comprised of a handful accounts:
- 1.Firebase email/password
- 2.Torus auth (with secret share)
- 3.Rainmaker user This means the user can get into partial signup states which corrupt the login experience. Going forward, we'll build the app to assume some conditions, and we'll just display an error if the conditions are not met.
- 4.We assume 1 and 2 are bundled together. If the user has a Firebase account, we assume he also has Torus data. If this is incorrect, we can always manually delete the user's Firebase user.
- 5.If the user tries to sign up and we detect that he already has a Firebase account, we should punt him to the login form.
- 6.If the user has both 1 and 2 but not 3, we can always create a new Rainmaker account for him.
Each of the following identifies the user. All of these shares are "offline", and each of them must act like Magic Link's delegate security model in that some secure storage much be used to store the share.
- Email / OTP
- Email / Password
- Phone / OTP
- OAuth (Google, Facebook, etc)
- Device Password (or PIN)
- Authenticator OTP
- Sign in with another wallet
- Social Recovery (from another wallet)
- Social Recovery (from another user login)
- Rainmaker Recovery (automatic after 24 hours)
Pioneered by Argent, "Recovery Mode" is a special mode that users may use to recover their wallet. It acts identically to other MPC shares with the exception that the user is first notified that the wallet is in recovery mode and the recovery share will not work for some time delay. For example, if the user forgets his device password, he may put his wallet in recovery mode. He's immediately sent an email telling him the wallet is in recovery mode and he may either cancel the recovery or wait 24 hours. After 24 hours, he can then use the recovery share to log in -- in this case, he has perhaps designated a friend as his recovery contact and that friend can log in to act as a recovery proxy. Like any other shares, recovery shares must be enabled prior to the user getting locked out of his account. For that reason, it's likely a good idea to push the user into the recovery share setup flow early in the onboarding process.
- 1.User chooses his form of identity.
- 2.User logs into identity provider (email/password, completes OTP flow, etc). Once logged in, we know the user owns his account, so we can divulge more account details such as what other shares are necessary to reconstruct the password.
- 3.Client fetches identity share.
- 4.Client fetches list of secondary shares from Rainmaker.
- 5.Based on the required shares, the client either logs in or prompts the user to fulfill missing shares.
- 6.Once enough shares are present, client reconstructs the private key and proceeds.
Rainmaker Auth Storage
- Identity table holds identities (email, phone number, or OAuth provider).
- Auth Details table holds the overall auth requirements for a user -- how many shares are necessary to log in, is the user in recovery mode, etc.
- Auth Methods table holds a full list of the user's share types and any additional details associated with each share (secret share id, authenticator authority, etc).
- Identity service: This service handles authentication for each of the auth types. In the case of email/password, it verifies credentials. In OTP usage, it verifies the OTP. In OAuth, it verifies the OAuth token. In all cases, once a user is authenticated, it allows the user to retrieve his identity share and also to retrieve a list of secondary shares. We can start by building just email/password. We can even use Firebase for actual auth management which gets us password resetting for free at the expense of an additional service dependency.
- OTP service: When a user logs in with an email/OTP or phone/OTP account, we need to send a code accordingly. When he then types that code into the app, we need to verify it. The identity service can handle verification, but we must first build the OTP senders and temporary code stores.
- OAuth support: If we want to offer social login, we need to build both the client experience and backend validation into the identity service.