Skip to content

Latest commit

 

History

History

umbra-js

umbra-js

JavaScript library for interacting with the Umbra Protocol.

Getting Started

Requirements for use:

Once verifying compatibility, install with yarn add @umbracash/umbra-js

Overview

The umbra-js library aims to abstract away the math and cryptography required for using Umbra so it's easy to build on top of the Umbra protocol. Most documentation lives in the FAQ, and the Technical Details section provides a pretty thorough overview. Here we cover some of the documentation specific to umbra-js.

Below is an overview of the files within this library. If you like reading code to understand things, read in the order files are listed below. This will start from the full, big-picture view of how Umbra works then go into the details.

  1. src/classes/Umbra.ts: The Umbra class is a high-level class intended for developers to directly interact with. It abstracts away the complexity of the protocol into a few main methods: 1. send() is used to send funds to another user, and automatically handles the underlying cryptography required. Check out the code in this method to understand the required steps for sending funds via Umbra. 1. generatePrivateKeys() prompts the user for a signature and generates their spending and viewing keys. Note: make sure the wallet being used supports deterministic ECDSA signatures with RFC 6979 1. scan() lets you find funds sent to the specified user, by providing just the user’s spending public key and viewing private key 1. withdraw() lets a stealth address directly withdraw both tokens and ETH 1. withdrawOnBehalf() uses meta-transactions to relay a withdraw transaction on behalf of another user, and the signWithdraw() method is used to get the required signature relayWithdrawOnBehalf() can be used to relay a meta-transaction using the default Umbra relayer
  2. src/classes/KeyPair.ts: This class is where the core cryptography logic lives. A KeyPair class is instantiated with either a private or public key, and the class methods help you perform various operations with those keys, including encryption/decryption, multiplication, and compression/decompression of public keys
  3. src/classes/RandomNumber.ts: This simple class is used to generate our 32 byte random number, and will properly format the number when provided an optional 16 byte payload extension
  4. src/utils/utils.ts contains various helper methods for a range of tasks, primarily related to getting a recipient’s public keys
  5. src/types.ts: You’ll see a few custom types used throughout the library, which are all defined here

Concepts

This section gives an overview of how different parts of Umbra work. When applicable, we link to the corresponding FAQ question on the Umbra website which may provide additional information.

For an introduction and background on elliptic curve cryptography, see the references below:

Sending and Receiving ETH vs. Tokens

When ETH is sent, it's transferred directly to the recipient's stealth address, but when tokens are sent they are held by the contract until the recipient withdraws them. ETH is always needed to send the transaction required to move funds, so sending ETH directly to a stealth address provides no issues and funds can easily be transferred out of the stealth address to another address.

But sending tokens directly to a stealth address would pose some difficulties. The stealth address would have no ETH to pay for the transaction to transfer the tokens, and getting ETH into the address without compromising privacy is its own challenge. The easiest and cheapest solution to this issue is to have tokens instead held by the contract, and support withdrawal of the tokens using meta-transactions that only require a signature from the stealth address. This lets the recipient pay the gas fee for the withdrawal transaction in tokens, and the relayer will pay the ETH fee

Random Numbers and Payload Extension

The shared secret used to encrypt the random number is 256 bits, so we XOR that with a random 256 bit number to generate the 256 bit ciphertext emitted in Announcement events. But because the strength of elliptic curves is roughly equal to half the size of the prime field, that means the secp256k1 curve used by Ethereum only provides ~128 bits of security, not 256 bits. As a result, a 128 bit random number would be just as secure as a 256 bit random number.

Since the XOR of these two parameters results in a 256 bit ciphertext of the same strength regardless of whether the random number is 128 bits or 256 bits, the RandomNumber class only generates a 128 bit random number, and lets the user provide the other 128 bits. This "free" 128 bits of data is known as the payload extension, and can be used to send short memos, recognize app-specific transactions, or whatever else developers can think of.

The corresponding FAQ question can be found here.

Private Key Generation

Umbra is based on elliptic curve math, and needs access to a user's private key to perform mathematical operations on it. For security reasons, wallets of course will not provide an app with the user's private key, and asking the user to input their private key into a form is a security risk and bad UX. Similarly, randomly generating a private key on the user's first visit requires them to backup a new key, and this is also bad UX.

Instead, Umbra uses an approach similar to zkSync, Loopring, Aztec, and StarkWare to generate app-specific private keys. We ask the user to sign a message, and use the hash of the signature to generate private keys. The corresponding public keys are what users would use to send you funds. The message signed includes the chain ID your wallet is connected to to prevent replay attacks across network.

Since Umbra supports separate spending and viewing keys (see below), the r and s components of the signature are hashed separately to generate the two private keys.

Spending and Viewing Keys

Borrowing the nomenclature from Zcash, Umbra allows, but does not require, users to use different private keys for the "encrypt random number" and “compute stealth address” steps. This is the default behavior of the Umbra app, but it can be overridden by using Advanced Mode.

This allows users to give their viewing key to third party scanning services that can alert them of received funds, but without giving those services access to their funds.

The corresponding FAQ question can be found here.

Hooks

If you’re familiar with ERC-777 or other similar standards, you are already familiar with the concept of hooks. Hooks let the caller perform other actions in addition to the core logic of the method being called. In the case of ERC-777, a transfer hook can be used to call a method on a contract after transferring tokens to that contract.

Umbra works similarly—when withdrawing funds from the contract, users might want to deposit them straight into a DeFi protocol or swap their DAI for ETH. Hooks let you do this. See the corresponding FAQ question and the implementation in Umbra.sol for more information on how to use hooks.

Usage Example

To send funds to a recipient via Umbra, follow the steps in the code snippet below:

import { hexlify, hexZeroPad } from "@ethersproject/bytes";
import { toUtf8Bytes } from "@ethersproject/strings";
export { parseUnits } from "@ethersproject/units";

import { Umbra } from "@scopelift/umbra-js";
import { signer } from "the/users/connected/wallet"; // assume user previously connected wallet and has signer

// Define the special address the Umbra contract uses to represent ETH
const ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";

// Define the send parameters
const recipientId = "msolomon.eth";
const amount = parseUnits("1", 18); // sending 1 ETH
const tokenAddress = ETH_ADDRESS; // we're sending ETh

// Get token approvals if necessary
if (tokenAddress !== ETH_ADDRESS) {
  // check allowance, and if allowance is insufficient, request approval transaction
}

// Define an optional payload extension. If you don't want to provide one, either leave out the
// overrides parameter or simply leave the `payloadExtension` out of the overrides object.
// Here we convert a string to hex and pad it to ensure it's 16 bytes
const payloadExtension = hexZeroPad(hexlify(toUtf8Bytes("Hello world!")), 16);

// Define our overrides
const overrides = { payloadExtension /* gasPrice, gasLimit */ };

// Send the transaction
const provider = signer.provider;
const umbra = new Umbra(provider, provider.network.chainId);
const { tx, stealthKeyPair } = await umbra.send(
  signer,
  tokenAddress,
  amount,
  recipientId,
  overrides
);
await tx.wait(); // transaction mined
// stealthKeyPair.address gives the address funds were sent to

To scan for received funds, follow the steps in the code snippet below:

import { Umbra } from "@scopelift/umbra-js";
import { signer } from "the/users/connected/wallet"; // assume user previously connected wallet and has signer

// Prompt the user for their signature to get their private keys
const { spendingKeyPair, viewingKeyPair } =
  await umbra.value.generatePrivateKeys(signer.value);

// Define a custom range of blocks to scan. Leave this parameter out to scan all blocks
const startBlock = 12290000;
const endBlock = 10000000;
const overrides = { startBlock, endBlock };

// Scan for funds
const provider = signer.provider;
const umbra = new Umbra(provider, provider.network.chainId);
const spendingPublicKey = spendingKeyPair.publicKeyHex;
const viewingPrivateKey = viewingKeyPair.privateKeyHex;
const { userAnnouncements } = await umbra.scan(
  spendingPublicKey,
  viewingPrivateKey,
  overrides
);
// Right now userAnnouncements is the only thing returned, but it's returned as an object to
// allow additional return values in the future without breaking existing implementations

To withdraw funds, follow the steps in the code snippet below:

import { Umbra } from "@scopelift/umbra-js";
import { signer } from "the/users/connected/wallet"; // assume user previously connected wallet and has signer

// Define the special address the Umbra contract uses to represent ETH
const ETH_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";

// Prompt the user for their signature to get their private keys
const { spendingKeyPair, viewingKeyPair } =
  await umbra.value.generatePrivateKeys(signer.value);

// Let's assume we're working with the first announcement outputs from the above snippet
const announcement = userAnnouncements[0];
const { randomNumber, token: tokenAddress } = announcement; // token gives the token address

// Define address to withdraw funds to (don't use the zero address in real life)
const destinationAddress = "0x0000000000000000000000000000000000000000";

// Configure Umbra class
const provider = signer.provider;
const chainId = provider.network.chainId;
const spendingPrivateKey = spendingKeyPair.privateKeyHex;
const umbra = new Umbra(provider, chainId);

// Get the stealth private key needed for withdrawal
const stealthKeyPair = spendingKeyPair.mulPrivateKey(randomNumber);
const stealthPrivateKey = stealthKeyPair.privateKeyHex;

// Handle withdraw based on token address
if (tokenAddress === ETH_ADDRESS) {
  // Handle ETH withdrawal
  const tx = await umbra.withdraw(
    stealthPrivateKey,
    tokenAddress,
    destinationAddress
  );
} else {
  // Define the sponsor address (who is relaying the transaction) and the fee they'll get
  const sponsor = "0xAddressOfYourRelayer";
  const sponsorFee = "123";

  // Get a users signature to relay the withdrawal
  const { v, r, s } = await Umbra.signWithdraw(
    stealthPrivateKey,
    chainId,
    umbra.chainConfig.umbraAddress,
    destinationAddress,
    tokenAddress,
    sponsor,
    sponsorFee
  );

  // Relay the transaction
  // Assume your app defines a signer called mySigner that sends the relay transaction
  const tx = await umbra.withdrawOnBehalf(
    mySigner,
    stealthKeyPair.address,
    destinationAddress,
    tokenAddress,
    sponsor,
    sponsorFee,
    v,
    r,
    s
  );
}

API Reference

For a full API reference, navigate to the umbra-js folder in your terminal and run yarn docs. Open the resulting umbra-js/docs/index.html file in your browser to view the documentation.

Development

  1. Copy the .env.example to .env and populate it with your own configuration parameters.
cp .env.example .env
  1. Run yarn to install packages
  2. Run yarn test to run all tests.