The ZF FROST Book

This is a guide-level reference for the ZF FROST library.

Getting Started

If you're not familiar with FROST, first read Understanding FROST.

Then read the Tutorial, and use the Rust docs as reference.

Understanding FROST

This explains the main concepts and flows of FROST in a generic manner. These are important to understand how to use the library, but rest assured that the Tutorial will have more concrete information.

FROST is a threshold signature scheme. It allows splitting a Schnorr signing key into n shares for a threshold t, such that t (or more) participants can together generate a signature that can be validated by the corresponding verifying key. One important aspect is that the resulting signature is indistinguishable from a non-threshold signature from the point of view of signature verifiers.

Note

FROST only supports Schnorr signatures. Therefore it can't produce ECDSA signatures.

Key Generation

There are two options for generating FROST key shares. In both cases, after the key generation procedure, each participant will get:

  • a secret share;
  • a verifying share (which can be used by other participants to verify the signature shares the participant produces);
  • a group verifying key, which is the public key matching the private key that was split into shares; it is used to verify the final signature generated with FROST.

Trusted Dealer Generation

An existing key (which can be freshly generated) is split into shares. It's the simplest approach, but it has the downside of requiring the entire key to exist in memory at some point in time, which may not be desired in high security applications. However, it is much simpler to set up. It requires an authenticated and confidential communication channel to distribute each share to their respective participants.

Learn how to do Trusted Dealer Generation with the ZF FROST library.

Distributed Key Generation

A two-round protocol after which each participant will have their share of the secret, without the secret being ever present in its entirety in any participant's memory. Its downside is that it requires a broadcast channel as well as an authenticated and confidential communication channel between each pair of participants, which may be difficult to deploy in practice.

Learn how to do Distributed Key Generation with the ZF FROST library.

Signing

Signing with FROST starts with a Coordinator (which can be one of the share holders, or not) which selects the message to be signed and the participants that will generate the signature.

Each participant sends fresh nonce commitments to the Coordinator, which then consolidates them and sends them to each participant. Each one will then produce a signature share, which is sent to the Coordinator who finally aggregates them and produces the final signature.

Note

If having a single coordinator is not desired, then all participants can act as coordinators. Refer to the spec for more information.

Warning

ALL participants who are selected for generating the signature need to produce their share, even if there are more than t of them. For example, in 2-of-3 signing, if 3 participants are selected, them all 3 must produce signature shares in order for the Coordinator be able to produce the final signature. Of course, the Coordinator is still free to start the process with only 2 participants if they wish.

Verifying Signatures

Signature verification is carried out as normal with single-party signatures, along with the signed message and the group verifying key as inputs.

Repairing Shares

Repairing shares allow participants to help another participant recover their share if they have lost it, or also issue a new share to a new participant (while keeping the same threshold).

The repair share functionality requires a threshold of participants to work. For example, in a 2-of-3 scenario, two participants can help the third recover their share, or they could issue a new share to move to a 2-of-4 group.

The functionality works in such a way that each participant running the repair share function is not able to obtain the share that is being recovered or issued.

Refreshing Shares

Refreshing shares allow participants (or a subset of them) to update their shares in a way that maintains the same group public key. Some applications are:

  • Make it harder for attackers to compromise the shares. For example, in a 2-of-3 threshold scenario, if an attacker steals one participant's device and all participants refresh their shares, the attacker will need to start over and steal two shares instead of just one more.
  • Remove a participant from the group. For example, in a 2-of-3 threshold scenario, if two participants decide to remove the third they can both refresh their shares and the third participant would no longer be able to participate in signing sessions with the others. (They can also then use the repair share functionality to issue a new share and move from 2-of-2 back to 2-of-3.)

Note

This is also possible via Distributed Key Generation but this has not yet been implemented.

Danger

It is critically important to keep in mind that the Refresh Shares functionality does not "restore full security" to a group. While the group evolves and participants are removed and new participants are added, the security of the group does not depend only on the threshold of the current participants being honest, but also on the threshold of all previous set of participants being honest! For example, if Alice, Mallory and Eve form a group and Mallory is eventually excluded from the group and replaced with Bob, it is not enough to trust 2 out of 3 between Alice, Bob and Eve. You also need to trust that Mallory won't collude with, say, Eve which could have kept her original pre-refresh share and they could both together recompute the original key and compromise the group. If that's a unnaceptable risk to your use case, you will need to migrate to a new group if that makes sense to your application.

Ciphersuites

FROST is a generic protocol that works with any adequate prime-order group, which in practice are constructed from elliptic curves. The spec specifies five ciphersuites with the Ristretto255, Ed25519, Ed448, P-256 and secp256k1 groups. It's possible (though not recommended) to use your own ciphersuite.

Tutorial

The ZF FROST suite consists of multiple crates. frost-core contains a generic implementation of the protocol, which can't be used directly without a concrete instantiation.

The ciphersuite crates (frost-ristretto255, frost-ed25519, frost-ed448, frost-p256, frost-secp256k1 and frost-secp256k1-tr) provide ciphersuites to use with frost-core, but also re-expose the frost-core functions without generics. If you will only use a single ciphersuite, then we recommend using those functions, and this tutorial will follow this approach. If you need to support multiple ciphersuites then feel free to use frost-core along with the ciphersuite types.

This tutorial will use the frost-ristretto255 crate, but changing to another ciphersuite should be a matter of simply changing the import.

Importing and General Information

Including frost-ristretto255

Add to your Cargo.toml file:

[dependencies]
frost-ristretto255 = "2.0.0"

Handling errors

Most crate functions mentioned below return Results with Errors. All errors should be considered fatal and should lead to aborting the key generation or signing procedure.

Serializing structures

FROST is a distributed protocol and thus it requires sending messages between participants. While the ZF FROST library does not handle communication, it can help with serialization in the following ways:

Default byte-oriented serialization

With the serialization feature, which is enabled by default, all structs that need to be communicated will have serialize() and deserialize() methods. The serialization format is described in Serialization Format.

serde

Alternatively, if you would like to use another format such as JSON, you can enable the serde feature (which is not enabled by default). When it is enabled, you can use serde to serialize any structure that needs to be transmitted. The importing would look like:

[dependencies]
frost-ristretto255 = { version = "2.0.0", features = ["serde"] }

Note that serde usage is optional. Applications can use different encodings, and to support that, all structures that need to be transmitted have public getters and new() methods allowing the application to encode and decode them as it wishes. (Note that fields like Scalar and Element do have standard byte string encodings; the application can encode those byte strings as it wishes, as well the structure themselves and things like maps and lists.)

Trusted Dealer Key Generation

The diagram below shows the trusted dealer key generation process. Dashed lines represent data being sent through an authenticated and confidential communication channel.

Diagram of Trusted Dealer Key Generation, illustrating what is explained in the text

To generate the key shares, the dealer calls generate_with_dealer(). It returns a BTreeMap mapping the (automatically generated) Identifiers to their respective SecretShares, and a PublicKeyPackage which contains the VerifyingShare for each participant and the group public key (VerifyingKey).

use frost_ristretto255 as frost;
use rand::thread_rng;
use std::collections::BTreeMap;

let mut rng = thread_rng();
let max_signers = 5;
let min_signers = 3;
let (shares, pubkey_package) = frost::keys::generate_with_dealer(
    max_signers,
    min_signers,
    frost::keys::IdentifierList::Default,
    &mut rng,
)?;

Each SecretShare must then be sent via an authenticated and confidential channel for each participant, who must verify the package to obtain a KeyPackage which contains their signing share, verifying share and group verifying key. This is done with KeyPackage::try_from():

    let key_package = frost::keys::KeyPackage::try_from(secret_share)?;

Info

Check the crate documentation for a full working example; keep in mind it's an artificial one since everything runs in the same program.

Info

You can specify which identifiers to use by using IdentifierList::Custom. Refer to the DKG section for an example on how to create identifiers.

Danger

Which authenticated and confidential channel to use is up to the application. Some examples:

  • Manually require the dealer to sent the SecretShares to the partipants using some secure messenger such as Signal;
  • Use a TLS connection, authenticating the server with a certificate and the client with some user/password or another suitable authentication mechanism;

Refer to the Terminology page for more details.

Failure of using a confidential channel may lead to the shares being stolen and possibly allowing signature forgeries if a threshold number of them are stolen.

Failure of using an authenticated channel may lead to shares being sent to the wrong person, possibly allowing unintended parties to generate signatures.

Danger

The KeyPackage contents must be stored securely. For example:

  • Make sure other users in the system can't read it;
  • If possible, use the OS secure storage such that the package contents can only be opened with the user's password or biometrics.

Warning

The participants may wish to not fully trust the dealer. While the dealer has access to the original secret and can forge signatures by simply using the secret to sign (and this can't be possibly avoided with this method; use Distributed Key Generation if that's an issue), the dealer could also tamper with the SecretShares in a way that the participants will never be able to generate a valid signature in the future (denial of service). Participants can detect such tampering by comparing the VerifiableSecretSharingCommitment values from their SecretShares (either by some manual process, or by using a broadcast channel) to make sure they are all equal.

Signing

The diagram below shows the signing process. Dashed lines represent data being sent through an authenticated communication channel.

Diagram of Signing, illustrating what is explained in the text

Coordinator, Round 1

To sign, the Coordinator must select which participants are going to generate the signature, and must signal to start the process. This needs to be implemented by users of the ZF FROST library and will depend on the communication channel being used.

Participants, Round 1

Each selected participant will then generate the nonces (a SigningNonces) and their commitments (a SigningCommitments) by calling round1::commit():

    let (nonces, commitments) = frost::round1::commit(
        key_package.signing_share(),
        &mut rng,
    );

The SigningNonces must be kept by the participant to use in Round 2, while the SigningCommitments must be sent to the Coordinator using an authenticated channel.

Coordinator, Round 2

The Coordinator will get all SigningCommitments from the participants and the message to be signed, and then build a SigningPackage by calling SigningPackage::new().

let message = "message to sign".as_bytes();
// In practice, the SigningPackage must be sent to all participants
// involved in the current signing (at least min_signers participants),
// using an authenticate channel (and confidential if the message is secret).
let signing_package = frost::SigningPackage::new(commitments_map, message);

The SigningPackage must then be sent to all the participants using an authenticated channel. (Of course, if the message is confidential, then the channel must also be confidential.)

Warning

In all of the main FROST ciphersuites, the entire message must be sent to participants. In some cases, where the message is too big, it may be necessary to send a hash of the message instead. We strongly suggest creating a specific ciphersuite for this, and not just sending the hash as if it were the message. For reference, see how RFC 8032 handles "pre-hashing".

Participants, Round 2

Upon receiving the SigningPackage, each participant will then produce their signature share using their KeyPackage from the key generation process and their SigningNonces from Round 1, by calling round2::sign():

    let signature_share = frost::round2::sign(&signing_package, nonces, key_package)?;

The resulting SignatureShare must then be sent back to the Coordinator using an authenticated channel.

Important

In most applications, it is important that the participant must be aware of what they are signing. Thus the application should show the message to the participant and obtain their consent to proceed before producing the signature share.

Coordinator, Aggregate

Upon receiving the SignatureShares from the participants, the Coordinator can finally produce the final signature by calling aggregate() with the same SigningPackage sent to the participants and the PublicKeyPackage from the key generation (which is used to validate each SignatureShare).

let group_signature = frost::aggregate(&signing_package, &signature_shares, &pubkey_package)?;

The returned signature, a Signature, will be a valid signature for the message in the SigningPackage in Round 2 for the group verifying key in the PublicKeyPackage.

Note

FROST supports identifiable abort: if a participant misbehaves and produces an invalid signature share, then aggregation will fail and the returned error will have the identifier of the misbehaving participant. (If multiple participants misbehave, only the first one detected will be returned.)

What should be done in that case is up to the application. The misbehaving participant could be excluded from future signing sessions, for example.

Verifying signatures

The Coordinator could verify the signature with:

let is_signature_valid = pubkey_package
    .verifying_key()
    .verify(message, &group_signature)
    .is_ok();

(There is no need for the Coordinator to verify the signature since that already happens inside aggregate(). This just shows how the signature can be verified.)

Distributed Key Generation

The diagram below shows the distributed key generation process. Dashed lines represent data being sent through an authenticated and confidential communication channel. Note that the first dashed line requires a broadcast channel

Diagram of Distributed Key Generation, illustrating what is explained in the text

Part 1

To start the DKG, each participant calls dkg::part1() passing its identifier, the desired threshold and total number of participants. (Thus, they need to agree on those parameters via some mechanism which is up to the application.) It returns a round1::SecretPackage and a round1::Package:

use rand::thread_rng;
use std::collections::BTreeMap;

use frost_ristretto255 as frost;

let mut rng = thread_rng();

let max_signers = 5;
let min_signers = 3;

    // Ask the user which identifier they would like to use. You can create
    // an identifier from a non-zero u16 or derive from an arbitrary string.
    // Some fixed examples follow (each participant must choose a different identifier)
    let participant_identifier = Identifier::try_from(7u16)?;
    let participant_identifier = Identifier::derive("alice@example.com".as_bytes())?;

    let (round1_secret_package, round1_package) = frost::keys::dkg::part1(
        participant_identifier,
        max_signers,
        min_signers,
        &mut rng,
    )?;

Info

Check the crate documentation for a full working example; keep in mind it's an artificial one since everything runs in the same program.

The round1::SecretPackage must be kept in memory to use in the next round. The round1::Package must be sent to all other participants using a broadcast channel to ensure that all participants receive the same value.

Danger

A broadcast channel in this context is not simply broadcasting the value to all participants. It requires running a protocol to ensure that all participants have the same value or that the protocol is aborted. Check the linked Terminology section for more details.

Failure in using a proper broadcast channel will make the key generation insecure.

Part 2

Upon receiving the other participants' round1::Packages, each participant then calls dkg::part2() passing their own previously created round1::SecretPackage and a map of the received round1::Packages, keyed by the Identifiers of the participant that sent each one of them. (These identifiers must come from whatever mapping the coordinator has between communication channels and participants, i.e. they must have assurance that the round1::Package came from the participant with that identifier.) It returns a round2::SecretPackage and a BTreeMap mapping other participants's Identifiers to round2::Packages:

    let (round2_secret_package, round2_packages) =
        frost::keys::dkg::part2(round1_secret_package, round1_packages)?;

The round2::SecretPackage must be kept in memory for the next part; the round1::SecretPackage is consumed and is not required anymore.

The round2::Packages must be sent to their respective participants with the given Identifiers, using an authenticated and confidential communication channel.

Part 3

Finally, upon receiving the other participant's round2::Package, the DKG is concluded by calling dkg::part3() passing the same round1::Packages received in Part 2, the round2::Packages just received (again keyed by the Identifier of the participant that sent each one of them), and the previously stored round2::SecretPackage for the participant. It returns a KeyPackage, with the participant's secret share, and a PublicKeyPackage containing the group verifying key:

    let (key_package, pubkey_package) = frost::keys::dkg::part3(
        round2_secret_package,
        round1_packages,
        round2_packages,
    )?;

Note

All participants will generate the same PublicKeyPackage.

Refreshing Shares using a Trusted Dealer

The diagram below shows the refresh share process. Dashed lines represent data being sent through an authenticated and confidential communication channel.

The Trusted Dealer needs to first run compute_refreshing_shares() which returns SecretShares (the "refreshing shares") and a PublicKeyPackage. Each SecretShare must then be sent along with the PublicKeyPackage via an authenticated and confidential channel for each participant.

Each Participant then runs refresh_share() to generate a new KeyPackage which will replace their old KeyPackage; they must also replace their old PublicKeyPackage with the one sent by the Trusted Dealer.

Danger

The refreshed KeyPackage contents must be stored securely and the original KeyPackage should be deleted. For example:

  • Make sure other users in the system can't read it;
  • If possible, use the OS secure storage such that the package contents can only be opened with the user's password or biometrics.

Danger

Applications should first ensure that all participants who refreshed their KeyPackages were actually able to do so successfully, before deleting their old KeyPackages. How this is done is up to the application; it might require sucessfully generating a signature with all of those participants.

Danger

Refreshing Shares may be not enough to address security concerns after a share has been compromised. Refer to to the Understanding FROST section.

User Documentation

Serialization Format

With the serialization feature, which is enabled by default, all structs that need to communicated will have serialize() and deserialize() methods.

The format is basically the serde encoding of the structs using the postcard crate. But since this is an implementation detail, we describe the format as follows:

  • Integers are encoded in varint format
  • Fixed-size byte arrays are encoded as-is (e.g. scalars, elements)
    • Note that the encoding of scalars and elements are defined by the ciphersuites.
  • Variable-size byte arrays are encoded with a length prefix (varint-encoded) and the array as-is (e.g. the message)
  • Maps are encoded as the varint-encoded item count, followed by concatenated item encodings.
  • Structs are encoded as the concatenation of the encodings of its items, with a Header struct as the first item, which contains the format version (a u8) and the ciphersuite ID.
    • The format currently described is identified by the constant 0.
    • Ciphersuite IDs are encoded as the 4-byte CRC-32 of the ID string (the constant Ciphersuite::ID, which for default ciphersuites is the contextString of the ciphersuite, per the FROST spec).

For example, the following Signing Package:

  • Header (map):
    • Version (u8): 0
    • Ciphersuite ID (4 bytes): CRC-32 of FROST-RISTRETTO255-SHA512-v1
  • Commitments (map):
    • Identifier (byte array): 2a00000000000000000000000000000000000000000000000000000000000000
    • Signing Commitments:
      • Hiding (byte array): e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76
      • Bindng (byte array): 6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919
      • Ciphersuite ID (4 bytes): CRC-32 of FROST-RISTRETTO255-SHA512-v1
  • Message (variable size byte array): 68656c6c6f20776f726c64 ("hello world" in UTF-8)

Is encoded as

00d76ecff5012a00000000000000000000000000000000000000000000000000
00000000000000d76ecff5e2f2ae0a6abc4e71a884a961c500515f58e30b6aa5
82dd8db6a65945e08d2d766a493210f7499cd17fecb510ae0cea23a110e8d5b9
01f8acadd3095c73a3b9190b68656c6c6f20776f726c64
  • 00: the version of the format
  • d76ecff5: the ciphersuite ID of the SigningPackage; CRC-32 of FROST-RISTRETTO255-SHA512-v1
  • 01: the length of the map
  • 2a00000000000000000000000000000000000000000000000000000000000000: the identifier
  • d76ecff5: the ciphersuite ID of the SigningCommitments; CRC-32 of FROST-RISTRETTO255-SHA512-v1
  • e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76: the hinding commitment
  • 6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919: the binding commitment
  • 0b: the length of the message
  • 68656c6c6f20776f726c64: the message

Note

The ciphersuite ID is encoded multiple times in this case because SigningPackage includes SigningCommitments, which also need to be communicated in Round 1 and thus also encodes its ciphersuite ID. This is the only instance where this happens.

Test Vectors

Check the snapshots files in each ciphersuite crate for test vectors.

FROST with Zcash

FROST can be used with the Zcash cryptocurrency, allowing the creation of a wallet shared between multiple participants where multiple participants must authorize a transaction before it can go through.

In a regular Zcash wallet, the spending key (commonly derived from a seed phrase) allows freely spending from the wallet. If the key is lost or gets hacked, then the wallet owner will lose access to their funds forever.

With FROST, only shares of the related spend authorization key will exist, between multiple participants. During wallet creation, a threshold is set, and only that number of participants (or more) can jointly create a transaction that spends funds from the wallet.

Some possible applications are:

  • Creating a wallet that is shared between members of a organization that manages certain funds. For example, a 3-of-5 wallet can be created which will require 3 members to authorize spending the funds.
  • Shared custody services can be created so that users can have their own wallet and can spend their funds with the help of the service, and will not lose access to the funds in case they lose the device with their key share. For example, a 2-of-3 wallet where the user keeps one share, the service keeps another, and the third share is backed up in the user's cloud.

FROST thus helps addressing one of the biggest challenges in cryptocurrencies, which is the protecting the wallet key from either being accidentally lost or being hacked. Before, users needed to choose to either manage their own funds will puts a huge amount of responsibility on them and is well known to work greatly in practice, or to leave their funds to be managed by some custody service which is also known to be a risk. With FROST, users can share the responsibility between multiple entities or persons (or even multiple devices they own).

This section describes in more details how FROST can be used with Zcash.

Technical Details

FROST only works with Schnorr signatures. Zcash transparent transactions use ECDSA, therefore FROST does not work with Zcash transparent addresses. (This could change if the Taproot upgrade from Bitcoin is ported to Zcash, but it seems unlikely.)

Zcash supports three shielded pools: Sprout, Sapling and Orchard. Sprout is being deprecated so we only need to care about Sapling and Orchard.

Sapling and Orchard keys are commonly derived from a single spending key (which in turn is commonly derived from a seed phrase). This is shown in the figure below, taken from the Zcash protocol:

Sapling and Orchard key trees

To use FROST with Zcash, the key that needs to be split is the Spend Authorizing Key or ask. This is the key that signs transactions and allow them to go through.

Key Derivation and DKG Support

As shown in the figure, the ask is commonly derived from the Spending Key sk. At first, this could indicate that it is impossible to use Distributed Key Generation (DKG) in this scenario, since the key it creates is unpredictable.

However, the protocol does not require ask to be derived from sk. This would allow creating key shares using DKG, which will also output ak (which is simply the public key matching ask). Thus:

  • With Sapling, the rest of the keys can be derived from that ak and either:
    • An sk which is used to derive only nsk and ovk;
    • Or generating nsk and ovk by themselves (or generating them from a random sk which is thrown away).
  • With Orchard, the rest of the keys can be derived from that ak and either:
    • An sk which is used to derive only nk and rivk;
    • Or generating nk and rivk by themselves (or generating them from a random sk which is thrown away).

Arguably this should be done even if Trusted Dealer Key Generation is used: the goal of FROST is requiring multiple entities to authorize a transaction, not having a seed phrase that allows recreating the ask ensures that property.

Backing Up Key Shares

Zcash users are familiar with the concept of seed phrases and with how they can always recover their wallet with it.

However, FROST wallets need more information to be restored:

  • FROST-related:
    • Key share
    • Verifying shares of all participants
    • Public keys and identifiers of all participants if online communication is being used
  • Zcash-related:
    • Sapling: sk, or both Proof authorizing key (ak and nsk) and Outgoing viewing key (ovk)
    • Orchard: sk, or Full viewing key (ak, nk, rivk). The upside of using the Full viewing key is that there is already a format for encoding it.

Thus, even if a sk derived from a seed phrase is used, that is not enough to restore a FROST wallet. In fact it would be probably confusing to use a seed phrase since users wouldn't be able to tell if it's a regular Zcash seed phrase or a FROST seed phrase which needs additional information to be restored.

For this reason it seems impossible to easily encode a FROST wallet, so using something like a JSON file with all this information is advisable.

Of course, unlike regular Zcash wallets, a user losing their FROST wallet is not catastrophic. Users can recover their key share with the help of other participants, and would only need to remember their identifier (and other participants can probably help with that).

Note

Orchard is simpler to handle, so it may be a good idea to just support it with FROST.

Note

The only secret information is the key share. So another possibility is to just ask the user to backup it (using a seed phrase format, or other string encoding) and get the remaining information from the other participants when recovering a wallet.

Communications

The biggest challenge in using FROST with Zcash is allowing participants to communicate securely with each other, which is required to run the FROST protocol. Since wallets don't currently need to communication to each other, a whole new mechanism will need to be implemented.

For this to happen, two things are required:

  • Allowing wallets to actually communicate with each other (regardless of security). This is challenging because users are usually behind NATs and firewalls, so they can't simply open TCP connections with each other. So some kind of signaling server may be needed.
  • Making the communication secure. This is actually fairly solvable while not trivial and we're planning on working on a library to address it. It needs to provide authentication and confidentiality, for example using the Noise protocol. Also needs to provide a broadcast channel on top of it.
  • Managing public keys. Another challenging part. Users need to be able to create a key pair of keys used for the secure communication, and exporting public keys to each other. This is similar to contact management that some wallets have, so a possibility is to expand on that (instead of storing just name and address, also store identifier and public key).
One long-term idea is to extend the Zcash protocol to allow P2P messaging.
This has been discussed in the context of sending encrypted notes via the network
instead of publishing them on the blockchain.
Another idea is to extend lightwalletd servers to support messaging, since
wallets are all already connected to a server (not the same one, so inter-server
communications would be also needed)

Ywallet Demo Tutorial

This tutorial explaining how to run the FROST demo using Ywallet that was presented during Zcon4.

Ywallet supports offline signing, which allows having a view-only account that can generate a transaction plan, which can be signed by a offline wallet also running Ywallet. The demo uses this mechanism but signs the transaction plan with a command line tool, using FROST.

This tutorial assumes familiarity with the command line.

Setting up

Install cargo and git.

Install Ywallet.

Clone the repository:

git clone https://github.com/ZcashFoundation/frost-zcash-demo.git

Generating FROST key shares

First we will generate the FROST key shares. For simplicity we'll use trusted dealer, DKG will be described later.

Run the following (it will take a bit to compile):

cd frost-zcash-demo/
cargo run --bin trusted-dealer -- -C redpallas

This will by default generate a 2-of-3 key shares. The public key package will be written to public-key-package.json, while key packages will be written to key-package-1.json through -3. You can change the threhsold, number of shares and file names using the command line; append -- -h to the commend above for the command line help.

Info

If you want to use DKG instead of Trusted Dealer, instead of the command above, run this for each participant, in separate terminals for each:

cargo run --bin dkg -- -C redpallas

and follow the instructions. (There will be a considerable amount of copy&pasting!)

Generating the Full Viewing Key for the wallet

Get the verifying_key value that is listed inside the Public Key Package in public-key-package.json. For example, in the following package

{"verifying_shares": ...snip... ,"verifying_key":"d2bf40ca860fb97e9d6d15d7d25e4f17d2e8ba5dd7069188cbf30b023910a71b","ciphersuite":"FROST(Pallas, BLAKE2b-512)"}

you would need to copy d2bf40ca860fb97e9d6d15d7d25e4f17d2e8ba5dd7069188cbf30b023910a71b.

The run the following command, replacing <ak> with the value you copied.

cd zcash-sign/
cargo run --release -- generate --ak <ak> --danger-dummy-sapling

It will print an Orchard address, and a Unified Full Viewing Key. Copy and paste both somewhere to use them later.

Importing the Full Viewing Key into Ywallet

Open Ywallet and click "New account". Check "Restore an account" and paste the Unified Full Viewing Key created in the previous step. Click "Import".

Funding the wallet

Now you will need to fund this wallet with some ZEC. Use the Orchard address printed by the signer (see warning below). Send ZEC to that address using another account (or try ZecFaucet).

Warning

The address being show by Ywallet is a unified address that includes both an Orchard and Sapling address. For the demo to work, you need to receive funds in you Orchard address. Whether that will happen depends on multiple factors so it's probably easier to use just the Orchard-only address printed by the signer. If you send it to the Sapling address, the funds will be unspendable and lost!

Creating the transaction

Now you will create the transaction that you wish to sign with FROST. Click the arrow button and paste the destination address (send it to yourself if you don't know where to send it). Type the amount you want to send and click the arrow button.

The wallet will show the transaction plan. Click the snowflake button. It will show a QR code, but we want that information as a file, so click the floppy disk button and save the file somewhere (e.g. tx.raw as suggested by Ywallet).

Signing the transaction

Go back to the signer terminal and run the following, replacing <tx_plan_path> with the path to the file you saved in the previous step, <ufvk> with the UFVK hex string printed previously, and <tx_signed_path> with the path where you want to write the signed transaction (e.g. tx-signed.raw).

cargo run --release -- sign --tx-plan <tx_plan_path> --ufvk <ufvk> -o <tx_signed_path>

The program will print a SIGHASH and a Randomizer.

Running the server

Now you will need to simulate two participants and a Coordinator to sign the transaction, and the FROST server that handles communications between them. It's probably easier to open four terminals.

In the first one, the server, run (in the same folder where key generation was run):

RUST_LOG=debug cargo run --bin server

Registering users

In order to interact with the server, you will need to register users. For this guide we will need two. In a new terminal, run the following command for user "alice" (replace the password if you want):

curl --data-binary '{"username": "alice", "password": "foobar10", "pubkey": ""}' -H 'Content-Type: application/json' http://127.0.0.1:2744/register

It will output "null". (The "pubkey" parameter is not used currently and should be empty.) Also register user "bob":

curl --data-binary '{"username": "bob", "password": "foobar10", "pubkey": ""}' -H 'Content-Type: application/json' http://127.0.0.1:2744/register

You only need to do this once, even if you want to sign more than one transaction. If for some reason you want to start over, close the server and delete the db.sqlite file.

Feel free to close this terminal, or reuse it for the next step.

Warning

Do not use passwords that you use in practice; use dummy ones instead. (You shouldn't reuse passwords anyway!) For real world usage you would need to take more care to not end up writing the password to your shell history. (In real world usage we'd expect this to be done by applications anyway.)

Coordinator

In the second terminal, the Coordinator, run (in the same folder where key generation was run):

export PW=foobar10
cargo run --bin coordinator -- -C redpallas --http -u alice -w PW -S alice,bob -r -

We will use "alice" as the Coordinator, so change the value next to export PW= if you used another password when registering "alice".

And then:

  • It should read the public key package from public-key-package.json.
  • When prompted for the message to be signed, paste the SIGHASH printed by the signer above (just the hex value, e.g. 4d065453cfa4cfb4f98dbc9cff60c4a3904ed91c523b8ef8d67d28bea7f12ea3).
  • When prompted for the randomizer, paste the randomizer printed by the signer above (again just the hex value)

Warning

If you prefer to pass the randomizer as a file by using the --randomizer argument, you will need to convert it to binary format.

Participant 1 (alice)

In the third terminal, Participant 1, run the following:

export PW=foobar10
cargo run --bin participant -- -C redpallas --http --key-package key-package-1.json -u alice -w PW

(We are using "alice" again. There's nothing stopping a Coordinator from being a Partcipant too!)

Participant 2 (bob)

In the fourth terminal, for Participant 2, run the following:

export PW=foobar10
cargo run --bin participant -- -C redpallas --http --key-package key-package-2.json -u bob -w PW

Coordinator

Go back to the Coordinator CLI. The protocol should run and complete succesfully. It will print the final FROST-generated signature. Hurrah! Copy it (just the hex value).

Go back to the signer and paste the signature. It will write the raw signed transaction to the file you specified.

Broadcasting the transaction

Go back to Ywallet and return to its main screen. In the menu, select "Advanced" and "Broadcast". Select the raw signed transaction file you have just generated (tx-signed.raw if you followed the suggestion).

That's it! You just sent a FROST-signed Zcash transaction.

Terminology

Broadcast channel

A secure broadcast channel in the context of multi-party computation protocols such as FROST has the following properties:

  1. Consistent. Each participant has the same view of the message sent over the channel.
  2. Authenticated. Players know that the message was in fact sent by the claimed sender. In practice, this requirement is often fulfilled by a PKI.
  3. Reliable Delivery. Player i knows that the message it sent was in fact received by the intended participants.
  4. Unordered. The channel does not guarantee ordering of messages.

Possible deployment options:

  • Echo-broadcast (Goldwasser-Lindell)
  • Posting commitments to an authenticated centralized server that is trusted to provide a single view to all participants (also known as 'public bulletin board')

Identifier

An identifier is a non-zero scalar (i.e. a number in a range specific to the ciphersuite) which identifies a specific party. There are no restrictions to them other than being unique for each participant and being in the valid range.

In the ZF FROST library, they are either automatically generated incrementally during key generation or converted from a u16 using a TryFrom<u16>.

ZF FROST also allows deriving identifiers from arbitrary byte strings with Identifier::derive(). This allows deriving identifiers from usernames or emails, for example.

Peer to peer channel

Peer-to-peer channels are authenticated, reliable, and unordered, per the definitions above. Additionally, peer-to-peer channels are confidential; i.e., only participants i and j are allowed to know the contents of a message msg_i,j.

Possible deployment options:

  • Mutually authenticated TLS
  • Wireguard

Threshold secret sharing

Threshold secret sharing does not require a broadcast channel because the dealer is fully trusted.

Verifiable secret sharing

Verifiable secret sharing requires a broadcast channel because the dealer is not fully trusted: keygen participants verify the VSS commitment which is transmitted over the broadcast channel before accepting the shares distributed from the dealer, to ensure all participants have the same view of the commitment.

Developer Documentation

This section contains information only relevant to ZF FROST developers or contributors.

FROST dependencies

This is a list of production Rust code that is in scope and out of scope for FROSTs second audit.

--

Full Audit

FROST Crates

NameVersionNotes
frost-corev0.2.0
frost-ed25519v0.2.0
frost-ed448v0.2.0
frost-p256v0.2.0
frost-ristretto255v0.2.0
frost-secp256k1v0.2.0

ZF Dependencies

NameVersionNotes
redjubjubv0.6.0This library is being partially audited as part of the Zebra audit.
reddsav0.5.0This library is being partially audited as part of the Zebra audit.

Partial Audit

NameVersionReasonNotes
ed448-goldilocksv0.4.0Doesn't have a lot of users on github (12) or crates.io (~2k recent downloads) and it's not been previously audited and reviewedA pure-Rust implementation of Ed448 and Curve448 and Decaf.

The following ed448-goldilocks modules are used by frost-ed448:

  • src/field/scalar.rs
  • src/curve/edwards/extended.rs (converting to/from TwistedExtendedPoint, MontgomeryPoint and AffinePoint are out of scope)
  • src/field/mod.rs
  • src/curve/scalar_mul/variable_base.rs

Out of Scope

The following crates and dependencies are out of scope for the audit.

FROST Crates

NameVersionNotes
frost-rerandomizedv0.2.0To be audited after the security proof is complete.
frost-secp256k1-trN/Afrost-secp256k1 with Taproot support, has not been audited yet.

frost-core Dependencies

NameVersionReasonNotes
byteorderv1.4.3Library for reading/writing numbers in big-endian and little-endian.
criterionv0.4.0Statistics-driven micro-benchmarking library
debugless-unwrapv0.0.4This library provides alternatives to the standard .unwrap* methods on Result and Option that don't require Debug to be implemented on the unexpected variant.
digestv0.10.6Traits for cryptographic hash functions and message authentication codes
hexv0.4.3Encoding and decoding data into/from hexadecimal representation.
proptestv1.1.0Hypothesis-like property-based testing and shrinking.
proptest-derivev0.3.0Custom-derive for the Arbitrary trait of proptest.
rand_corev0.6.4Core random number generator traits and tools for implementation.
serde_jsonv1.0.93A JSON serialization file format
thiserrorv1.0.38This library provides a convenient derive macro for the standard library's std::error::Error trait.
visibilityv0.0.1Attribute to override the visibility of items (useful in conjunction with cfg_attr)
zeroizev1.5.7This crate implements a portable approach to securely zeroing memory using techniques which guarantee they won't be "optimized away" by the compiler.

frost-ed25519 Dependencies

NameVersionReasonNotes
curve25519-dalekv4.0.0-pre.1A pure-Rust implementation of group operations on ristretto255 and Curve25519
rand_corev0.6.4Core random number generator traits and tools for implementation.
sha2v0.10.6Pure Rust implementation of the SHA-2 hash function family including SHA-224, SHA-256, SHA-384, and SHA-512.

frost-ed448 Dependencies

NameVersionReasonNotes
rand_corev0.6.4Pure Rust implementation of the SHA-2 hash function family including SHA-224, SHA-256, SHA-384, and SHA-512.
sha3v0.10.6SHA-3 (Keccak) hash function

frost-p256 Dependencies

NameVersionReasonNotes
p256v0.11.1Pure Rust implementation of the NIST P-256 (a.k.a. secp256r1, prime256v1) elliptic curve with support for ECDH, ECDSA signing/verification, and general purpose curve arithmetic
rand_corev0.6.4Core random number generator traits and tools for implementation.
sha2v0.10.6Pure Rust implementation of the SHA-2 hash function family including SHA-224, SHA-256, SHA-384, and SHA-512.

frost-rerandomized Dependencies

NameVersionReasonNotes
rand_corev0.6.4Core random number generator traits and tools for implementation.

frost-ristretto255 Dependencies

None

frost-secp256k1 Dependencies

NameVersionReasonNotes
k256v0.12.0-pre.0secp256k1 (a.k.a. K-256) elliptic curve library written in pure Rust with support for ECDSA signing/verification/public-key recovery, Taproot Schnorr signatures, Elliptic Curve Diffie-Hellman (ECDH), and general-purpose secp256k1 elliptic curve group operations which can be used to implement arbitrary group-based protocols.
rand_corev0.6.4Core random number generator traits and tools for implementation.
sha2v0.10.6Pure Rust implementation of the SHA-2 hash function family including SHA-224, SHA-256, SHA-384, and SHA-512.

Release Checklist

One-time crates.io setup

  1. Follow the steps in https://doc.rust-lang.org/cargo/reference/publishing.html#before-your-first-publish (you can create a token scoped to publish-update).
  2. To get permissions to publish you’ll need to be in the owners group. If you aren’t in there, ask someone in that group to add you

Communication

  1. Post in #frost slack channel and tag the Frost team that you’re going to be doing a release to freeze PR approvals until it’s done. E.g “@frost-core I’m doing a release of <version> of Frost. Please do not merge any more PRs until I’m finished. Thanks.”

Checks

  1. Check current version for each crate. This is in the Cargo.toml file for frost-core, frost-ed448 etc.

    1. Frost core version number
    2. Frost ed25519 version number
    3. Frost ed448 version number
    4. Frost p256 version number
    5. Frost re randomized version number
    6. Frost ristretto255 version number
    7. Frost secp256k1 version number
    8. Frost secp256k1 tr version number
  2. Decide which version to tag the release with (e.g. v0.3.0). Currently we always use the same release number for all crates, but it's possible for them to get out of sync in the future.

  3. Create new issue. E.g. Release v0.4.0

Make changes

  1. Bump the version of each crate in their Cargo.toml files

  2. Bump the version used in the tutorial (importing.md)

  3. Check if the changelog is up to date and update if required (we’re only keeping the one in frost-core for now). Double check using FROST releases which will have a list of all the PRs that have been closed since the last release. Things to include in the changelog will be anything that impacts production code and big documentation changes. I.e. script and test changes should not be included. NOTE: Please add to the changelog whenever you make changes to the library as this will make things simpler for the person in charge of the release.

    1. Move version in changelog to Released
    2. Create a new version in “unreleased” in changelog
  4. Update the version number for frost-core and frost-rerandomized in the Ciphersuite crates, e.g. in frost-core = { path = "../frost-core", version = "0.4.0", features = ["test-impl"] }. You'll need to do this for dependencies and dev-dependencies

  5. Create a PR with subject Release \<version number> containing all these changes

  6. You’ll need someone to review and approve it

  7. Wait for it to pass CI checks

Publish

  1. Checkout main branch, in the commit of the previously merged PR (in case other stuff got merged after that)

  2. Run cargo publish -p frost-core --dry-run to check if it’s ready to publish. Fix issues if any.

  3. Draft and publish a new release for frost-core.

    1. In “Choose a tag” type <crate>/<version> e.g. “frost-core/v0.2.0” and click “Create new tag”
    2. In “Target” select “main” as long as other PRs haven’t been merged after the version bump PR. Otherwise, select the commit matching the PR that was merged above.
    3. In “Release title” use <crate> <version> e.g. “frost-core v0.2.0”
    4. Paste the (raw Markdown) changelog for this version into the description box.
    5. Leave “Set as pre-release” unchecked (we should have checked it in earlier versions but the ship has sailed. It doesn’t matter much)
    6. Check “Set as the latest release”
  4. Publish it with cargo publish -p frost-core

  5. Check if frost-rerandomized is ready to be published: cargo publish -p frost-rerandomized --dry-run. Fix any errors if needed.

  6. Draft and publish a frost-rerandomized release

    1. Use the same process as described for frost-core above, but you can leave the changelog empty and uncheck “Set as the latest release”
  7. Publish it with cargo publish -p frost-rerandomized

  8. Check if other crates are ready to be published: for cs in ristretto255 ed25519 secp256k1 secp256k1-tr p256 ed448; do cargo publish -p frost-$cs --dry-run; done. Fix any issues if needed.

    1. If you get an error like this:

      “error: failed to verify package tarball Caused by: failed to select a version for the requirement frost-core = "^0.3.0" candidate versions found which didn't match: 0.2.0, 0.1.0 location searched: crates.io index required by package frost-ed25519 v0.3.0 (frost/target/package/frost-ed25519-0.3.0)

      This is because the ciphersuite crates aren’t pointing at the new frost-core package. This is because you need to publish frost-core before you can publish the others otherwise they will not have the expected version to point to.

  9. Draft and publish releases for each of those crates (sorry, that will be boring)

    1. Use the same process as described for frost-core above (actions 1 - 3), but you can leave the changelog empty and uncheck “Set as the latest release”
  10. Publish those crates: for cs in ristretto255 ed25519 secp256k1 secp256k1-tr p256 ed448; do cargo publish -p frost-$cs; done

Confirm

  1. Check versions in the crates to confirm everything worked:

    1. Frost core
    2. Frost ed25519
    3. Frost ed448
    4. Frost p256
    5. Frost ristretto255
    6. Frost secp256k1
    7. Frost secp256k1 tr
    8. Frost rerandomized
  2. Let the team know in the #frost slack channel that the release is complete and successful

In the case of an unsuccessful release

If something was wrongly tagged, you can just retag it. If something was wrongly pushed to crates.io, you will need to make a new fixed release and yank the wrong release.

Developer Guide

Pre-commit checks

  1. Run tests cargo test
  2. Run formatter cargo fmt
  3. Check linter cargo clippy --all-features --all-targets -- -D warnings and if you want to automatically fix then run cargo clippy --fix

Coverage

Test coverage checks are performed in the pipeline. This is configured here: .github/workflows/coverage.yaml To run these locally:

  1. Install coverage tool by running cargo install cargo-llvm-cov
  2. Run cargo llvm-cov --ignore-filename-regex '.*(tests).*|benches.rs|gencode|helpers.rs' (you may be asked if you want to install llvm-tools-preview, if so type Y)