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
Signature verification is carried out as normal with single-party signatures, along with the signed message and the group verifying key as inputs.
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
, and frost-secp256k1
) 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 = "1.0.0-rc.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 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 = "0.7.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 SecretPackage
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_packages[&participant_identifier].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 the list of
received round1::Packages
. 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, 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,
)?;
User Documentation
- frost-core
- frost-rerandomized
- frost-ed25519
- frost-ed448
- frost-p256
- frost-ristretto255
- frost-secp256k1
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 explaing 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 repositories:
git clone --branch add-redpallas https://github.com/ZcashFoundation/frost-zcash-demo.git
git clone --branch frost-demo https://github.com/ZcashFoundation/zwallet.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 --features redpallas
Answer the prompts with 2
(minimum number of signers), 3
(maximum) and empty, pressing enter to submit each.
A bunch of information will be printed. Copy and paste them somewhere to use them later, or leave the terminal open.
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 --features redpallas
and follow the instructions. (There will be a considerable amount of copy&pasting!)
Generating the Full Viewing Key for the wallet
In a new terminal, switch to the folder of the signer tool:
cd zwallet/native/zcash-sync/
Before running it, you will need to create a seed phrase which is used to
generate the Sapling address. This wouldn't be needed since the demo only works
with an Orchard address, but due to current limitations in the underlying
crates, we also need to generate a Sapling address which won't be used in the
demo. Generate a fresh 24-word seed phrase, for example using this
site (reminder: don't use random sites to
generate seed phrases unless for testing purposes!), then write to a file called
.env
in the signer folder in the following format, putting the seed phrase
inside the quotes:
KEY="seed phrase"
We can finally generate a new wallet. Run the following command; it will take a bit to compile. It will show a bunch of warnings which is normal.
cargo build --release --bin sign --features dotenv -- -g
When prompted for the ak
, paste the verifying_key
value that was printed in
the previous part, inside the Public Key Package. For example, in the following
package
Public key package:
{"verifying_shares": ...snip... ,"verifying_key":"d2bf40ca860fb97e9d6d15d7d25e4f17d2e8ba5dd7069188cbf30b023910a71b","ciphersuite":"FROST(Pallas, BLAKE2b-512)"}
you would need to use
d2bf40ca860fb97e9d6d15d7d25e4f17d2e8ba5dd7069188cbf30b023910a71b
. Press
enter to submit.
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".
In the "Rescan from..." window, pick today's date (since the wallet was just created) and press OK. The wallet should open.
You will need to change some of Ywallet configurations. Click the three dots at the top right and go to Settings. Switch to Advanced mode and click OK. Go back to the Settings and uncheck "Use QR for offline signing".
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). Wait until the funds become spendable (this may take ~10 minutes). You can check if the funds are spendable by clicking the arrow button and checking "Spendable Balance"
The address being show by Ywallet is an 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 happens depend on multiple factors so it's probably easier to just use the Orchard-only address printed by the signer.
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 "Send".
The wallet will show the transaction plan. Click "Send". It won't actually send - it will prompt you for where to save the transaction plan. Save it somewhere.
Signing the transaction
Go back to the signer terminal and run (adjust paths accordingly. The "tx.json" input parameters must point to the file you save in the previous step, and the "tx.raw" output parameter is where the signed transaction will be written).
cargo run --release --bin sign --features dotenv -- -t ~/Downloads/tx.json -o ~/Downloads/tx.raw
When prompted, paste the UFVK generated previously.
The program will print a SIGHASH and a Randomizer.
Now you will need to simulate two participants and a Coordinator to sign the transaction. It's probably easier to open three terminals.
In the first one, the Coordinator, run (in the same folder where key generation was run):
cargo run --bin coordinator --features redpallas
And then:
- Paste the JSON public key package generate during key generation (it's a single line with a JSON object.
- Type
2
for the number of participants. - Paste the identifier of the first participant, you can see it in the Secret
Share printed during key generation. If you used trusted dealer key
generation, it will be
0100000000000000000000000000000000000000000000000000000000000000
. - Paste the second identifier, e.g.
0200000000000000000000000000000000000000000000000000000000000000
. - When prompted for the message to be signed, paste the SIGHASH printed by the
signer above (just the hex value, e.g.
4d065453cfa4cfb4f98dbc9cff60c4a3904ed91c523b8ef8d67d28bea7f12ea3
).
Create a new terminal, for participant 1, and run:
cargo run --bin participant --features redpallas
And then:
- Paste the Secret Share printed during key generation (or Key Package if you used DKG).
- Copy the SigningCommitments line and paste into the Coordinator CLI.
Do the same for participant 2.
You should be at the Coordinator CLI. Paste the Randomizer generated by the signer before and copy the Signing Package line that it was printed by the Coordinator CLI before the Randomizer prompt.
Switch to participant 1 and:
- Paste the Signing Package
- Paste the Randomizer printed by the signer before.
- Copy the SignatureShare line and paste it into the Coordinator CLI.
Do the same for participant 2.
You should be at the Coordinator CLI. It has just printed 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.
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-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 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 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 cofigured 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
)