reddsa/rfcs/0001-messages.md

358 lines
13 KiB
Markdown
Raw Normal View History

2021-05-02 08:31:46 -07:00
# FROST messages
Proposes a message layout to exchange information between participants of a FROST setup using the [jubjub](https://github.com/zkcrypto/jubjub) curve.
2021-05-02 08:31:46 -07:00
## Motivation
Currently FROST library is complete for 2 round signatures with a dealer/aggregator setup.
This proposal is only considering that specific features, additions and upgrades will need to be made when DKG is implemented.
Assuming all participants have a FROST library available we need to define message structures in a way that data can be exchanged between participants. The proposal is a collection of data types so each side can do all the actions needed for a real life situation.
## Definitions
2021-05-11 06:27:16 -07:00
- `dealer` - Participant who distributes the initial package to all the other participants. The dealer can also be the aggregator and one of the signers.
- `aggregator` - Participant in charge of collecting all the signatures from the other participants and generating the final group signature. The aggregator can also be the dealer and one of the signers.
- `signer` - Participant that will receive the initial package, sign and send the signature to the aggregator to receive the final group signature. A signer can be also the dealer and the aggregator.
2021-05-02 08:31:46 -07:00
## Guide-level explanation
We propose a message separated in 2 parts, a header and a payload:
```rust
/// The data required to serialize a frost message.
2021-05-02 08:31:46 -07:00
struct Message {
header: Header,
payload: Payload,
}
```
`Header` will look as follows:
```rust
/// The data required to serialize the common header fields for every message.
///
/// Note: the `msg_type` is derived from the `payload` enum variant.
2021-05-02 08:31:46 -07:00
struct Header {
version: MsgVersion,
2021-05-10 07:24:03 -07:00
sender: ParticipantID,
receiver: ParticipantID,
2021-05-02 08:31:46 -07:00
}
```
While `Payload` will be defined as:
```rust
/// The data required to serialize the payload for a message.
2021-05-02 08:31:46 -07:00
enum Payload {
DealerBroadcast(MsgDealerBroadcast),
Commitments(MsgCommitments),
SigningPackage(MsgSigningPackage),
SignatureShare(MsgSignatureShare),
FinalSignature(MsgFinalSignature),
}
```
All the messages and new types will be defined in a new file `src/frost/messages.rs`
## Reference-level explanation
Here we explore in detail the header types and all the message payloads.
### Header
Fields of the header define new types. Proposed implementation for them is as follows:
```rust
/// The numeric values used to identify each `Payload` variant during serialization.
#[repr(u8)]
#[non_exhaustive]
2021-05-02 08:31:46 -07:00
enum MsgType {
DealerBroadcast,
Commitments,
SigningPackage,
SignatureShare,
FinalSignature,
2021-05-02 08:31:46 -07:00
}
/// The numeric values used to identify the protocol version during serialization.
struct MsgVersion(u8);
2021-05-02 08:31:46 -07:00
/// The numeric values used to identify each participant during serialization.
struct ParticipantId(u8);
2021-05-02 08:31:46 -07:00
```
### Payloads
Each payload defines a new message:
```rust
/// The data required to serialize `frost::SharePackage`.
///
2021-05-10 06:11:01 -07:00
/// Dealer must send this message with initial data to each participant involved.
/// With this, the participant should be able to build a `SharePackage` and use
2021-05-11 05:06:58 -07:00
/// the `sign()` function.
///
/// Note: `frost::SharePackage.public` can be calculated from `secret_share`.
struct messages::SharePackage {
/// This participant's secret key share: `frost::SharePackage.share.value`.
2021-05-11 05:06:58 -07:00
secret_share: frost::Scalar,
2021-05-10 06:11:01 -07:00
/// Commitment for the signer as a single jubjub::AffinePoint.
2021-05-11 05:06:58 -07:00
/// A set of commitments to the coefficients (which themselves are scalars)
/// for a secret polynomial _f_: `frost::SharePackage.share.commitment`
share_commitment: Vec<jubjub::AffinePoint>,
/// The public signing key that represents the entire group:
/// `frost::SharePackage.group_public`.
group_public: jubjub::AffinePoint,
2021-05-02 08:31:46 -07:00
}
/// The data required to serialize `frost::SigningCommitments`.
///
2021-05-11 05:06:58 -07:00
/// A signing commitment from the first round of the signing protocol.
struct messages::SigningCommitments {
2021-05-11 05:06:58 -07:00
/// The hiding point: `frost::SigningCommitments.hiding`
2021-05-06 07:01:41 -07:00
hiding: jubjub::AffinePoint,
2021-05-11 05:06:58 -07:00
/// The binding point: `frost::SigningCommitments.binding`
2021-05-06 07:01:41 -07:00
binding: jubjub::AffinePoint,
2021-05-02 08:31:46 -07:00
}
/// The data required to serialize `frost::SigningPackage`.
///
2021-05-10 06:11:01 -07:00
/// The aggregator decides what message is going to be signed and
/// sends it to each participant with all the commitments collected.
struct messages::SigningPackage {
2021-05-11 05:06:58 -07:00
/// The message to be signed: `frost::SigningPackage.message`
message: Vec<u8>,
2021-05-11 05:06:58 -07:00
/// The collected commitments for each signer as a hashmap of
/// unique participant identifiers: `frost::SigningPackage.signing_commitments`
///
/// Signing packages that contain duplicate or missing `ParticipantID`s are invalid.
signing_commitments: HashMap<ParticipantID, SigningCommitments>,
2021-05-02 08:31:46 -07:00
}
/// The data required to serialize `frost::SignatureShare`.
///
2021-05-10 06:11:01 -07:00
/// Each signer sends their signatures to the aggregator who is going to collect them
/// and generate a final spend signature.
struct messages::SignatureShare {
2021-05-11 05:06:58 -07:00
/// This participant's signature over the message:
/// `frost::SignatureShare.signature`
signature: frost::Scalar,
2021-05-02 08:31:46 -07:00
}
/// The data required to serialize a successful output from `frost::aggregate()`.
///
2021-05-11 05:06:58 -07:00
/// The final signature is broadcasted by the aggregator to any participant.
struct messages::AggregateSignature {
2021-05-11 05:06:58 -07:00
/// The aggregated group commitment: `Signature<SpendAuth>.r_bytes` returned by `frost::aggregate`
group_commitment: jubjub::AffinePoint,
/// A plain Schnorr signature created by summing all the signature shares:
/// `Signature<SpendAuth>.s_bytes` returned by `frost::aggregate`
schnorr_signature: frost::Scalar,
2021-05-02 08:31:46 -07:00
}
```
## Validation
Validation is implemented to each new data type as needed. This will ensure the creation of valid messages before they are send and right after they are received. We create a trait for this as follows:
2021-05-02 08:31:46 -07:00
```rust
2021-05-04 10:48:22 -07:00
pub trait Validate {
2021-05-06 08:07:44 -07:00
fn validate(&self) -> Result<&Self, MsgErr>;
2021-05-04 10:48:22 -07:00
}
```
And we implement where needed. For example, in the header, sender and receiver can't be the same:
2021-05-04 10:48:22 -07:00
```rust
impl Validate for Header {
2021-05-06 08:07:44 -07:00
fn validate(&self) -> Result<&Self, MsgErr> {
2021-05-02 08:31:46 -07:00
if self.sender.0 == self.receiver.0 {
2021-05-06 08:07:44 -07:00
return Err(MsgErr::SameSenderAndReceiver);
2021-05-02 08:31:46 -07:00
}
2021-05-06 08:07:44 -07:00
Ok(self)
2021-05-02 08:31:46 -07:00
}
}
```
2021-05-06 08:07:44 -07:00
This will require to have validation error messages as:
```rust
use thiserror::Error;
#[derive(Clone, Error, Debug)]
pub enum MsgErr {
#[error("sender and receiver are the same")]
SameSenderAndReceiver,
}
```
Then to create a valid `Header` in the sender side we call:
2021-05-04 10:48:22 -07:00
```rust
let header = Validate::validate(&Header {
..
2021-05-06 08:07:44 -07:00
}).expect("a valid header");
2021-05-04 10:48:22 -07:00
```
2021-05-06 08:07:44 -07:00
The receiver side will validate the header using the same method. Instead of panicking the error can be ignored to don't crash and keep waiting for other (potentially valid) messages.
```rust
2021-05-06 08:07:44 -07:00
if let Ok(header) = msg.header.validate() {
..
}
```
2021-05-11 05:42:31 -07:00
### Rules
The following rules must be implemented:
#### Header
- `msg_type` must be a known `MsgType` value.
- `version` must be a supported version.
- `sender` and `receiver` can't be the same.
- `sender` and `receiver` must be less than the maximum number of participants.
#### Payloads
2021-05-11 05:42:31 -07:00
- Each jubjub type must be validated during deserialization.
- `share_commitments`: For each round, the maximum number of participants is set by the length of `share_commitments`.
- `signing_commitments`:
- Signing packages that contain duplicate `ParticipantID`s are invalid
- Signing packages that contain missing `ParticipantID`s are invalid
- TODO: check if missing participants are allowed
2021-05-11 05:42:31 -07:00
- The length of `signing_commitments` must be less than or equal to the length of the `share_commitments` for this round.
- `message`: signed messages have a protocol-specific length limit. For Zcash, that limit is the maximum network protocol message length: `2^21` bytes (2 MB).
## Serialization/Deserialization
Each message struct needs to serialize to bytes representation before it is sent through the wire and must deserialize to the same struct (round trip) on the receiver side. We use `serde` and macro derivations (`Serialize` and `Deserialize`) to automatically implement where possible.
This will require deriving serde in several types defined in `frost.rs`.
Manual implementation of serialization/deserialization will be located at a new mod `src/frost/serialize.rs`.
2021-05-04 13:14:24 -07:00
2021-05-06 15:20:58 -07:00
### Byte order
Each byte chunk specified below is in little-endian order unless is specified otherwise.
2021-05-04 13:14:24 -07:00
### Header
The `Header` part of the message is 4 bytes total:
Bytes | Field name | Data type
------|------------|-----------
1 | msg_type | u8
1 | version | u8
1 | sender | u8
1 | receiver | u8
### Primitive types
2021-05-04 13:14:24 -07:00
2021-05-06 07:01:41 -07:00
`Payload`s use data types that we need to specify first. We have 2 primitive types inside the payload messages:
2021-05-04 13:14:24 -07:00
#### `Scalar`
2021-05-04 13:14:24 -07:00
`Scalar` is a an alias for `jubjub::Fr`. We use `Scalar::to_bytes` and `Scalar::from_bytes` to get a 32-byte little-endian canonical representation. See https://github.com/zkcrypto/bls12_381/blob/main/src/scalar.rs#L252
2021-05-04 13:14:24 -07:00
2021-05-06 07:01:41 -07:00
#### `AffinePoint`
2021-05-04 13:14:24 -07:00
Much of the math in FROST is done using `jubjub::ExtendedPoint`. But for message exchange `jubjub::AffinePoint`s are a better choice, as their byte representation is smaller.
2021-05-04 13:14:24 -07:00
2021-05-06 07:01:41 -07:00
Conversion from one type to the other is trivial:
https://docs.rs/jubjub/0.6.0/jubjub/struct.AffinePoint.html#impl-From%3CExtendedPoint%3E
https://docs.rs/jubjub/0.6.0/jubjub/struct.ExtendedPoint.html#impl-From%3CAffinePoint%3E
2021-05-04 13:14:24 -07:00
We use `AffinePoint::to_bytes` and `AffinePoint::from_bytes` to get a 32-byte little-endian canonical representation. See https://github.com/zkcrypto/jubjub/blob/main/src/lib.rs#L443
Similarly, `VerificationKey`s can be serialized using `<[u8; 32]>::from` and `VerificationKey::from`. See https://github.com/ZcashFoundation/redjubjub/blob/main/src/verification_key.rs#L86
2021-05-11 05:16:58 -07:00
### Payload
2021-05-04 13:14:24 -07:00
Payload part of the message is variable in size and depends on message type.
#### `MsgDealerBroadcast`
2021-05-04 13:14:24 -07:00
2021-05-11 05:26:05 -07:00
Bytes | Field name | Data type
----------------|------------------|-----------
32 | secret_share | Scalar
1 | participants | u8
2021-05-11 06:05:48 -07:00
32*participants | share_commitment | Vec\<AffinePoint\>
2021-05-11 05:26:05 -07:00
32 | group_public | AffinePoint
2021-05-04 13:14:24 -07:00
#### `MsgCommitments`
2021-05-04 13:14:24 -07:00
Bytes | Field name | Data type
--------|---------------------|-----------
32 | hiding | AffinePoint
32 | binding | AffinePoint
#### `MsgSigningPackage`
2021-05-04 13:14:24 -07:00
2021-05-11 05:26:05 -07:00
Bytes | Field name | Data type
-----------------------|--------------------|-----------
1 | participants | u8
(1+32+32)*participants | signing_commitments| HashMap<ParticipantID, SigningCommitments>
8 | message_length | u64
2021-05-11 06:05:48 -07:00
message_length | message | Vec\<u8\>
2021-05-04 13:14:24 -07:00
#### `SignatureShare`
Bytes | Field name | Data type
------|------------|-----------
32 | signature | Scalar
#### `MsgFinalSignature`
2021-05-08 16:43:36 -07:00
Bytes | Field name | Data type
------|------------------|-----------
2021-05-12 17:45:26 -07:00
32 | group_commitment | AffinePoint
2021-05-11 05:26:05 -07:00
32 | schnorr_signature| Scalar
2021-05-04 13:14:24 -07:00
## Not included
The following are a few things this RFC is not considering:
2021-05-09 04:36:46 -07:00
- After the dealer sends the initial `MsgDealerBroadcast` to all the participants, the aggregator must wait for signers to send the second message `MsgCommitments`. There is no timeout for this but only after the aggregator received all the commitments the process can continue. These restrictions and event waiting are not detailed in this RFC.
- This implementation considers not only communications between computer devices in the internet but allows the process to be done by other channels, the lack of timers can result in participants waiting forever for a message. It is the participants business to deal with this and other similars.
- The RFC does not describe a Service but just message structure and serialization.
2021-05-10 06:05:46 -07:00
- Messages larger than 4 GB are not supported on 32-bit platforms.
2021-05-12 16:20:46 -07:00
- Implementations should validate that message lengths are lower than a protocol-specific maximum length, then allocate message memory.
2021-05-04 13:14:24 -07:00
2021-05-02 08:31:46 -07:00
## Testing plan
### Test Vectors
#### Conversion on Test Vectors
- Test conversion from `frost` to `message` on a test vector
1. Implement the Rust `message` struct
2. Implement conversion from and to the `frost` type
3. Do a round-trip test from `frost` to `message` on a test vector
- Test conversion from `message` to bytes on a test vector
1. Implement conversion from and to the `message` type
2. Do a round-trip test from `message` to bytes on a test vector
#### Signing Rounds on Test Vectors
- Test signing using `frost` types on a test vector
1. Implement a single round of `frost` signing using a test vector
- Test signing using `message` types on a test vector
- Test signing using byte vectors on a test vector
### Property Tests
#### Conversion Property Tests
- Create property tests for each message
- Test round-trip conversion from `frost` to `message` types
- Test round-trip serialization and deserialization for each `message` type
#### Signing Round Property Tests
- Create property tests for signing rounds
- Test a signing round with `frost` types
- Test a signing round with `message` types
- Test a signing round with byte vectors