Schnorr Key Pair Generation
0. Multisignature
Multisignatures can have various forms:
- Group signature - means there is a number of potential signers, and the signature is valid if any group member signed it on behalf of the group. The setup can be so that the signer may remain anonymous. This setup is not secure since compromising one signer compromises the entire group.
- Threshold signature - The signature is valid only when a sufficiently large subgroup of the participants have signed the message.
Multisignatures may have special requirements depending on the sphere of their implementation.
- In such use cases, all the signers must participate for a signature to be valid. This setup is less secure because it relies on every signer. Should any signer refuse to sign, the signature cannot be valid.
- Only a subgroup must participate for the group signature to be valid for an arbitrary message, which is our case. We rely on the majority of the signers, allowing some signers to be offline without compromising the availability of the service.
The above assumptions introduce the following terms applicable to multisignature:
- Flexibility - The ability to define the threshold of required signers out of those available. Since we use the Byzantine Fault Tolerance threshold of , our flexibility ranges from zero to 33% of the signers to be potentially faulty offline without compromising the security of the transactions.
- Accountability - The message can be considered trustworthy if the signers' threshold () approves it.
1. Regular Schnorr Key Pair Generation
The first step in any elliptic cryptography is Secret (private) and Public key pair generation. In our example, we'll do it in Rust, one of the most effective low-level languages when writing this doc. For the security of a decentralized signers network, it is crucial to generate keypairs without relying on trusted third parties.
Here is an example of simple Schnorr key pair generation in Rust.
...
[dependencies]
secp256k1="0.27.0"
rand="^0.6.0"
use rand::{rngs::OsRng, RngCore};
use secp256k1::{PublicKey, Secp256k1, SecretKey};
fn generate_schnorr_key_pair() -> Result<(SecretKey, PublicKey), secp256k1::Error> {
// Creates a new range from the machine OS
let mut rng = OsRng::new().expect("Failed to create OsRng");
// Create an empty [u8] array of zeroes
let mut secret_key_bytes = [0u8; 32];
// Fill the [u8] array with random bytes
rng.fill_bytes(&mut secret_key_bytes);
// Generates a Secret Key from a random [u8] slice
let secret_key =
SecretKey::from_slice(&secret_key_bytes).expect("32 bytes, within curve order");
// Generate a public key from the secret key
let public_key = PublicKey::from_secret_key(&Secp256k1::new(), &secret_key);
// Returns the keypair
Ok((secret_key, public_key))
}
fn main() {
let (secret_key, public_key) =
generate_schnorr_key_pair().expect("Failed to generate key pair");
println!("Generated Secret key: {:?}", secret_key);
println!("Generated Public key: {:?}", public_key);
}
2. FROST Key Generation
XP.NETWORK uses FROST - Flexible Round Optimised Schnorr Threshold signature. To be more precise, we're using frost-dalek implementation in Rust.
Signatures are generated in a distributed way in the following order:
1. Validators determine the total number of the multisignature participants and store it in .
Let's assume there are ten validators.
2. Validators calculate the BFT threshold .
In our example, it becomes . Rounded down to the nearest integer, we're getting
fn get_bft_threshold(g: u32) -> u32 {
(2 * g / 3) + 1
}
// Usage
fn main() {
let g = 10; // Signer group size
let threshold: u32 = get_bft_threshold(g);
println!("BFT threshold: {}", threshold);
}
Example output:
BFT threshold: 7
3. Then they distribute the ordinal numbers setting a specific to each validator.
Example of code for each validator:
[dependencies]
frost-dalek="0.2.3"
use frost_dalek::Parameters;
use frost_dalek::Participant;
fn main() {
let g = 10; // Signer group size
let threshold: u32 = get_bft_threshold(g);
let params = Parameters { t: threshold, n: g };
let participant_index = 5;
let (participant_5, participant_5_coefficients) = Participant::new(¶ms, participant_index);
println!("Partial: {:?}", participant_5);
}
4. Validators exchange their values with each other in a way that cannot compromise the integrity of the data.
Participant #5 must ensure all the other participants have their part participant_5
.
5. Zero Knowledge proof verification of participant_5'th
partial:
let p_5 = participant_5.proof_of_secret_key.verify(&participant_5.index, &participant_5.public_key().unwrap());
6. Distributed key generation:
use frost_dalek::DistributedKeyGeneration;
let mut p_5_other_participants: Vec<Participant> = vec!(
participant_1.clone(),
participant_2.clone(),
participant_3.clone(),
participant_4.clone(),
// skip self
participant_6.clone(),
participant_7.clone(),
participant_8.clone(),
participant_9.clone(),
participant_10.clone(),
);
let p_5_state = DistributedKeyGeneration::<_>::new(
¶ms,
&participant_5.index,
&participant_5_coefficients,
&mut p_5_other_participants
);
7. Collecting secret shares & then distributing it with the other participants:
let p_5_their_secret_shares = p_5_state.their_secret_shares();
8. Now, every participant has a list of secret shares of the other participants:
let p_5_my_secret_shares = vec!(
p_1_their_secret_shares[0].clone(),
// ...
// Skip self
// ...
p_10_their_secret_shares[1].clone()
);
9. Now the private key derivation is possible:
let p_5_state = alice_state.to_round_two(p_5_my_secret_shares);
let (p_5_group_key, p_5_secret_key) = p_5_state.finish(participant_5.public_key().unwrap())?;
10. In the final step, the validators exchange their version of the group key and compare them:
assert!(p_5_group_key == p_1_group_key);
// ...
// Skip self
// ...
assert!(p_5_group_key == p_10_group_key);