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.
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.
If having a single coordinator is not desired, then all participants can act as coordinators. Refer to the spec for more information.
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.)
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 Result
s with
Error
s.
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 user 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.
To generate the key shares, the dealer calls
generate_with_dealer()
.
It returns a BTreeMap
mapping the (automatically generated) Identifier
s to
their respective SecretShare
s, 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)?;
Check the crate documentation for a full working example; keep in mind it's an artificial one since everything runs in the same program.
You can specify which identifiers to use by using IdentifierList::Custom
. Refer to the DKG section for an example on how to create identifiers.
Which authenticated and confidential channel to use is up to the application. Some examples:
- Manually require the dealer to sent the
SecretShare
s 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.
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.
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 SecretShare
s
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 SecretShare
s (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.
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.)
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.
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 SignatureShare
s 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
.
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
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,
)?;
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.
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::Package
s, 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 Identifier
s to round2::Package
s:
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::Package
s must be sent to their respective participants with the
given Identifier
s, 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::Package
s received in Part 2, the round2::Package
s
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,
)?;
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.
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.
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.
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
- frost-core
- frost-rerandomized
- frost-ed25519
- frost-ed448
- frost-p256
- frost-ristretto255
- frost-secp256k1
- frost-secp256k1-tr
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
- Hiding (byte array):
- Identifier (byte array):
- Message (variable size byte array):
68656c6c6f20776f726c64
("hello world"
in UTF-8)
Is encoded as
00d76ecff5012a00000000000000000000000000000000000000000000000000
00000000000000d76ecff5e2f2ae0a6abc4e71a884a961c500515f58e30b6aa5
82dd8db6a65945e08d2d766a493210f7499cd17fecb510ae0cea23a110e8d5b9
01f8acadd3095c73a3b9190b68656c6c6f20776f726c64
00
: the version of the formatd76ecff5
: the ciphersuite ID of the SigningPackage; CRC-32 ofFROST-RISTRETTO255-SHA512-v1
01
: the length of the map2a00000000000000000000000000000000000000000000000000000000000000
: the identifierd76ecff5
: the ciphersuite ID of the SigningCommitments; CRC-32 ofFROST-RISTRETTO255-SHA512-v1
e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76
: the hinding commitment6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919
: the binding commitment0b
: the length of the message68656c6c6f20776f726c64
: the message
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 transaprent transactions use ECDSA, therefore FROST does not work with Zcash transaparent 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:
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
they 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 onlynsk
andovk
; - Or generating
nsk
andovk
by themselves (or generating them from a randomsk
which is thrown away).
- An
- With Orchard, the rest of the keys can be derived from that
ak
and either:- An
sk
which is used to derive onlynk
andrivk
; - Or generating
nk
andrivk
by themselves (or generating them from a randomsk
which is thrown away).
- An
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
andnsk
) 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.
- Sapling:
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 catastrophical. 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).
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
.
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.
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).
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.
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)
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:
- Consistent. Each participant has the same view of the message sent over the channel.
- Authenticated. Players know that the message was in fact sent by the claimed sender. In practice, this requirement is often fulfilled by a PKI.
- Reliable Delivery. Player i knows that the message it sent was in fact received by the intended participants.
- 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
Name | Version | Notes |
---|---|---|
frost-core | v0.2.0 | |
frost-ed25519 | v0.2.0 | |
frost-ed448 | v0.2.0 | |
frost-p256 | v0.2.0 | |
frost-ristretto255 | v0.2.0 | |
frost-secp256k1 | v0.2.0 |
ZF Dependencies
Name | Version | Notes |
---|---|---|
redjubjub | v0.6.0 | This library is being partially audited as part of the Zebra audit. |
reddsa | v0.5.0 | This library is being partially audited as part of the Zebra audit. |
Partial Audit
Name | Version | Reason | Notes |
---|---|---|---|
ed448-goldilocks | v0.4.0 | Doesn't have a lot of users on github (12) or crates.io (~2k recent downloads) and it's not been previously audited and reviewed | A 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
Name | Version | Notes |
---|---|---|
frost-rerandomized | v0.2.0 | To be audited after the security proof is complete. |
frost-secp256k1-tr | N/A | frost-secp256k1 with Taproot support, has not been audited yet. |
frost-core
Dependencies
Name | Version | Reason | Notes |
---|---|---|---|
byteorder | v1.4.3 | Library for reading/writing numbers in big-endian and little-endian. | |
criterion | v0.4.0 | Statistics-driven micro-benchmarking library | |
debugless-unwrap | v0.0.4 | This library provides alternatives to the standard .unwrap* methods on Result and Option that don't require Debug to be implemented on the unexpected variant. | |
digest | v0.10.6 | Traits for cryptographic hash functions and message authentication codes | |
hex | v0.4.3 | Encoding and decoding data into/from hexadecimal representation. | |
proptest | v1.1.0 | Hypothesis-like property-based testing and shrinking. | |
proptest-derive | v0.3.0 | Custom-derive for the Arbitrary trait of proptest. | |
rand_core | v0.6.4 | Core random number generator traits and tools for implementation. | |
serde_json | v1.0.93 | A JSON serialization file format | |
thiserror | v1.0.38 | This library provides a convenient derive macro for the standard library's std::error::Error trait. | |
visibility | v0.0.1 | Attribute to override the visibility of items (useful in conjunction with cfg_attr) | |
zeroize | v1.5.7 | This 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
Name | Version | Reason | Notes |
---|---|---|---|
curve25519-dalek | v4.0.0-pre.1 | A pure-Rust implementation of group operations on ristretto255 and Curve25519 | |
rand_core | v0.6.4 | Core random number generator traits and tools for implementation. | |
sha2 | v0.10.6 | Pure Rust implementation of the SHA-2 hash function family including SHA-224, SHA-256, SHA-384, and SHA-512. |
frost-ed448
Dependencies
Name | Version | Reason | Notes |
---|---|---|---|
rand_core | v0.6.4 | Pure Rust implementation of the SHA-2 hash function family including SHA-224, SHA-256, SHA-384, and SHA-512. | |
sha3 | v0.10.6 | SHA-3 (Keccak) hash function |
frost-p256
Dependencies
Name | Version | Reason | Notes |
---|---|---|---|
p256 | v0.11.1 | Pure 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_core | v0.6.4 | Core random number generator traits and tools for implementation. | |
sha2 | v0.10.6 | Pure Rust implementation of the SHA-2 hash function family including SHA-224, SHA-256, SHA-384, and SHA-512. |
frost-rerandomized
Dependencies
Name | Version | Reason | Notes |
---|---|---|---|
rand_core | v0.6.4 | Core random number generator traits and tools for implementation. |
frost-ristretto255
Dependencies
None
frost-secp256k1
Dependencies
Name | Version | Reason | Notes |
---|---|---|---|
k256 | v0.12.0-pre.0 | secp256k1 (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_core | v0.6.4 | Core random number generator traits and tools for implementation. | |
sha2 | v0.10.6 | Pure 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
- 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
). - 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
- 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
-
Check current version for each crate. This is in the Cargo.toml file for frost-core, frost-ed448 etc.
-
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.
-
Create new issue. E.g. Release v0.4.0
Make changes
-
Bump the version of each crate in their Cargo.toml files
-
Bump the version used in the tutorial (importing.md)
-
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.
- Move version in changelog to Released
- Create a new version in “unreleased” in changelog
-
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 -
Create a PR with subject
Release \<version number>
containing all these changes -
You’ll need someone to review and approve it
-
Wait for it to pass CI checks
Publish
-
Checkout main branch, in the commit of the previously merged PR (in case other stuff got merged after that)
-
Run
cargo publish -p frost-core --dry-run
to check if it’s ready to publish. Fix issues if any. -
Draft and publish a new release for frost-core.
- In “Choose a tag” type
<crate>/<version>
e.g. “frost-core/v0.2.0” and click “Create new tag” - 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.
- In “Release title” use
<crate> <version>
e.g. “frost-core v0.2.0” - Paste the (raw Markdown) changelog for this version into the description box.
- Leave “Set as pre-release” unchecked (we should have checked it in earlier versions but the ship has sailed. It doesn’t matter much)
- Check “Set as the latest release”
- In “Choose a tag” type
-
Publish it with
cargo publish -p frost-core
-
Check if frost-rerandomized is ready to be published:
cargo publish -p frost-rerandomized --dry-run
. Fix any errors if needed. -
Draft and publish a frost-rerandomized release
- Use the same process as described for frost-core above, but you can leave the changelog empty and uncheck “Set as the latest release”
-
Publish it with
cargo publish -p frost-rerandomized
-
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.-
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 packagefrost-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.
-
-
Draft and publish releases for each of those crates (sorry, that will be boring)
- 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”
-
Publish those crates:
for cs in ristretto255 ed25519 secp256k1 secp256k1-tr p256 ed448; do cargo publish -p frost-$cs; done
Confirm
-
Check versions in the crates to confirm everything worked:
-
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
- Run tests
cargo test
- Run formatter
cargo fmt
- Check linter
cargo clippy --all-features --all-targets -- -D warnings
and if you want to automatically fix then runcargo clippy --fix
Coverage
Test coverage checks are performed in the pipeline. This is configured here: .github/workflows/coverage.yaml
To run these locally:
- Install coverage tool by running
cargo install cargo-llvm-cov
- Run
cargo llvm-cov --ignore-filename-regex '.*(tests).*|benches.rs|gencode|helpers.rs'
(you may be asked if you want to installllvm-tools-preview
, if so typeY
)