DKG Flow
This document traces the complete DKG flow from epoch triggering through threshold key usage.
Overview
Section titled “Overview”DKG generates threshold BLS keys each epoch. No single validator holds the full private key - instead, each validator holds a share that can produce partial signatures. Combining t-of-n partial signatures yields a valid threshold signature.
Epoch boundary approaches --> DKG triggered --> 4-phase protocol -->Threshold keys generated --> Keys used for TLE decryption & VRFKey Properties
Section titled “Key Properties”| Property | Value |
|---|---|
| Curve | BLS12-381 (MinSig variant) |
| Threshold | ~2/3 + 1 of validators |
| Public key type | G2 element |
| Signature/share type | G1 element / Scalar |
| Share encryption | X25519 ECDH + ChaCha20-Poly1305 |
| Lead time | 10 blocks before epoch boundary |
1. Epoch Triggering
Section titled “1. Epoch Triggering”DKG is triggered when approaching an epoch boundary:
// In DkgDriver event loopmatch event { DkgDriverEvent::BlockFinalized { height } => { // Check if we should start DKG for next epoch if height % epoch_length >= epoch_length - lead_blocks { let next_epoch = (height / epoch_length) + 1; self.start_round(rng, next_epoch, participants)?; } }}Configuration
Section titled “Configuration”DkgConfig { commit_timeout: Duration::from_secs(30), share_timeout: Duration::from_secs(30), complaint_timeout: Duration::from_secs(30), finalize_timeout: Duration::from_secs(30), lead_blocks: 10, // Start DKG 10 blocks before epoch end max_retries: 3, retry_base_delay: Duration::from_secs(1), retry_max_delay: Duration::from_secs(30),}Round Initialization
Section titled “Round Initialization”fn start_round<R: Rng + CryptoRng>( &mut self, rng: &mut R, epoch: u64, participants: Set<PublicKey>,) -> Result<(), DkgError> { let n = participants.len() as u32; let t = compute_threshold(n); // ~2/3 + 1
// Generate fresh polynomial and shares let (polynomial, shares) = ops::generate_shares::<_, MinSig>(rng, None, n, t)?;
// Create round state let round = DkgRoundState::new(epoch, participants, polynomial, shares); self.coordinator.in_progress.insert(epoch, round);
Ok(())}2. Phase 1: Commitment Broadcasting
Section titled “2. Phase 1: Commitment Broadcasting”Each validator acting as dealer broadcasts their polynomial commitment.
Commitment Structure
Section titled “Commitment Structure”pub struct DkgCommitment { pub epoch: u64, pub dealer: Vec<u8>, // Dealer's ed25519 public key pub commitment: Vec<u8>, // Serialized polynomial commitment (G2 elements) pub signature: Vec<u8>, // ed25519 signature}Signing
Section titled “Signing”// Domain separatorconst COMMITMENT_DOMAIN: &[u8] = b"CHAIN-DKG-COMMITMENT-V1";
fn sign_commitment(epoch: u64, polynomial: &Poly<Evaluation>) -> DkgCommitment { let commitment_bytes = serialize_polynomial(polynomial); let message = [ COMMITMENT_DOMAIN, &dealer_pubkey, &epoch.to_le_bytes(), &sha256(&commitment_bytes), ].concat();
let signature = ed25519_sign(&privkey, &message); // ...}Handler
Section titled “Handler”fn handle_commitment(&mut self, commitment: DkgCommitment) -> Result<(), DkgError> { // 1. Validate dealer is in participant set // 2. Verify ed25519 signature // 3. Store in round.commitments round.commitments.insert(dealer_pubkey, commitment.commitment); Ok(())}3. Phase 2: Share Distribution
Section titled “3. Phase 2: Share Distribution”Each dealer encrypts and sends a unique share to each participant.
Share Message Structure
Section titled “Share Message Structure”pub struct DkgShareMessage { pub epoch: u64, pub dealer: Vec<u8>, // Dealer's ed25519 public key pub recipient: Vec<u8>, // Recipient's ed25519 public key pub encrypted_share: Vec<u8>, // X25519 + ChaCha20-Poly1305 ciphertext pub signature: Vec<u8>, // ed25519 signature}Encryption Flow
Section titled “Encryption Flow”fn encrypt_share( recipient_ed25519: &[u8; 32], share: &group::Share,) -> Vec<u8> { // 1. Convert ed25519 pubkey to X25519 let recipient_x25519 = ed25519_pubkey_to_x25519(recipient_ed25519);
// 2. Generate ephemeral X25519 keypair let ephemeral_secret = EphemeralSecret::random_from_rng(rng); let ephemeral_public = X25519PublicKey::from(&ephemeral_secret);
// 3. ECDH key exchange let shared_secret = ephemeral_secret.diffie_hellman(&recipient_x25519);
// 4. Derive ChaCha20-Poly1305 key let key = sha256(shared_secret.as_bytes());
// 5. Encrypt let nonce = random_nonce_12(); let ciphertext = ChaCha20Poly1305::encrypt(&key, &nonce, &share_bytes);
// 6. Format: ephemeral_pubkey (32) || nonce (12) || ciphertext [ephemeral_public.as_bytes(), &nonce, &ciphertext].concat()}Decryption Flow
Section titled “Decryption Flow”fn decrypt_share( my_ed25519_privkey: &[u8; 32], encrypted_share: &[u8],) -> Result<group::Share, DkgError> { // 1. Parse ciphertext let ephemeral_public = &encrypted_share[0..32]; let nonce = &encrypted_share[32..44]; let ciphertext = &encrypted_share[44..];
// 2. Convert ed25519 privkey to X25519 let my_x25519_secret = ed25519_privkey_to_x25519(my_ed25519_privkey);
// 3. ECDH with ephemeral public let shared_secret = my_x25519_secret.diffie_hellman(ephemeral_public);
// 4. Derive same key let key = sha256(shared_secret.as_bytes());
// 5. Decrypt let plaintext = ChaCha20Poly1305::decrypt(&key, nonce, ciphertext)?;
// 6. Deserialize share deserialize_share(&plaintext)}4. Phase 3: Complaints & Justifications
Section titled “4. Phase 3: Complaints & Justifications”Handles disputes when shares are invalid or missing.
Complaint Structure
Section titled “Complaint Structure”pub struct DkgComplaint { pub epoch: u64, pub complainer: Vec<u8>, pub dealer: Vec<u8>, pub reason: ComplaintReason, // MissingShare | InvalidShare | InvalidCommitment pub signature: Vec<u8>,}Justification Structure
Section titled “Justification Structure”pub struct DkgJustification { pub epoch: u64, pub dealer: Vec<u8>, pub complainer: Vec<u8>, pub revealed_share: Vec<u8>, // Share in plaintext for public verification pub signature: Vec<u8>,}When a dealer receives a complaint, they can reveal the share publicly. All validators can then verify if the share matches the commitment.
5. Phase 4: Finalization
Section titled “5. Phase 4: Finalization”When threshold conditions are met, finalize the DKG round.
Threshold Check
Section titled “Threshold Check”if commitments.len() < threshold as usize { return Err(DkgError::ThresholdNotMet { ... });}Aggregation Process
Section titled “Aggregation Process”fn finalize_round(&mut self, epoch: u64) -> Result<DkgOutput, DkgError> { let round = self.coordinator.in_progress.get(&epoch)?;
// 1. Deserialize all commitments let commitments: Vec<Poly<Evaluation>> = round.commitments .values() .filter_map(|bytes| deserialize_polynomial(bytes).ok()) .collect();
// 2. Parallel verify all shares against commitments let verified_shares: Vec<group::Share> = shares_to_verify .par_iter() .filter_map(|(dealer, share, dealer_idx)| { let commitment = commitments.get(dealer_idx)?; if ops::verify_share::<MinSig>(commitment, my_index, &share).is_ok() { Some(share.clone()) } else { None } }) .collect();
// 3. Include our own dealing share verified_shares.push(our_dealing_share);
// 4. Aggregate public polynomial (combines all dealer commitments) let aggregate_polynomial = ops::construct_public::<MinSig>( commitments.iter(), threshold )?;
// 5. Aggregate shares (sum scalars - Shamir aggregation) let aggregate_share = aggregate_shares(&verified_shares, my_index)?;
// 6. Create output Ok(DkgOutput { polynomial: aggregate_polynomial, // Collective BLS public key share: aggregate_share, // Our threshold share participants: participants.clone(), })}DKG Output
Section titled “DKG Output”pub struct DkgOutput { pub polynomial: Poly<Evaluation>, // BLS12-381 polynomial (G2 for MinSig) pub share: group::Share, // Our private threshold share (scalar) pub participants: Set<PublicKey>, // Ordered participant list}The collective public key is the polynomial evaluated at 0: polynomial.evaluate(0).
6. Registration in Supervisor
Section titled “6. Registration in Supervisor”After DKG completes, keys are registered for use:
pub fn register_dkg_output( &self, epoch: Epoch, polynomial: Poly<Evaluation>, share: group::Share, participants: Set<PublicKey>,) { let mut inner = self.inner.write(); inner.dkg_polynomials.insert(epoch, polynomial); inner.dkg_shares.insert(epoch, share);}Accessing Keys
Section titled “Accessing Keys”pub fn dkg_polynomial(&self, epoch: Epoch) -> Option<Poly<Evaluation>> { // Returns polynomial where P(0) = collective BLS public key}
pub fn dkg_share(&self, epoch: Epoch) -> Option<group::Share> { // Returns our threshold share for signing}7. Threshold Key Usage
Section titled “7. Threshold Key Usage”TLE Encryption (Client Side)
Section titled “TLE Encryption (Client Side)”pub fn seal( plaintext: &[u8], master_public: &G2, // Collective public key from DKG epoch: u64,) -> Result<TleSealedTransaction, ...> { // Encrypt using collective public key // Can only be decrypted with threshold signature}TLE Decryption (Validator Side)
Section titled “TLE Decryption (Validator Side)”Each validator produces a partial signature:
pub fn sign_decryption_share( share: &group::Share, // Our DKG share epoch: u64,) -> TleDecryptionShare { let target = epoch.to_le_bytes(); let partial_sig = ops::sign_message::<MinSig>( &share.secret, Some(TLE_TARGET_PREFIX), &target ); // Returns G1 element}Leader combines t-of-n partial signatures:
pub fn combine_partial_signatures( threshold: usize, partial_sigs: &[(u16, G1Affine)],) -> Result<G1Affine, ...> { // Lagrange interpolation in G1 ops::threshold_signature_recover::<MinSig, _>(threshold, partial_sigs)}Threshold signature decrypts the ciphertext:
pub fn unseal( &self, threshold_sig: &G1Affine, expected_epoch: u64,) -> Result<Vec<u8>, ...> { tle::decrypt::<MinSig>(threshold_sig, &ciphertext)}8. Fallback Mechanism
Section titled “8. Fallback Mechanism”If DKG fails, the previous epoch’s keys can be used:
pub struct DkgCoordinatorState { pub completed: BTreeMap<u64, DkgOutput>, pub in_progress: BTreeMap<u64, DkgRoundState>, pub fallback_epoch: Option<u64>, // Last successful epoch pub last_epoch_used_fallback: Option<u64>,}Rules:
- Can fallback to previous epoch’s keys if DKG fails
- Cannot use fallback for two consecutive epochs
ConsecutiveFallbackerror if epoch N and N+1 both fail
9. Sequence Diagram
Section titled “9. Sequence Diagram”EPOCH BOUNDARY APPROACHING (height % epoch_length >= epoch_length - lead_blocks) │ ▼DkgDriver triggered │ ├─ start_round(epoch, participants) │ └─ ops::generate_shares() → polynomial + shares │ ▼PHASE 1: COMMITMENT (timeout: 30s) │ ├─ Each dealer broadcasts DkgCommitment │ ├─ Serialize polynomial commitment (G2 elements) │ ├─ Sign with ed25519 │ └─ Broadcast to all │ ├─ Validators receive and verify │ └─ Store in round.commitments │ ▼PHASE 2: SHARE DISTRIBUTION (timeout: 30s) │ ├─ Each dealer sends DkgShareMessage to each recipient │ ├─ Encrypt share (X25519 ECDH + ChaCha20-Poly1305) │ ├─ Sign with ed25519 │ └─ Send to recipient │ ├─ Recipients decrypt and verify │ └─ Store in round.shares │ ▼PHASE 3: COMPLAINTS & JUSTIFICATIONS (timeout: 30s) │ ├─ If share invalid/missing → DkgComplaint ├─ Dealer responds with DkgJustification (revealed share) └─ Public verification against commitment │ ▼PHASE 4: FINALIZATION (timeout: 30s) │ ├─ Check threshold met (>= t commitments) │ ├─ Deserialize all commitments │ ├─ Parallel verify shares against commitments │ ├─ Aggregate public polynomial │ └─ ops::construct_public() → Poly<G2> │ ├─ Aggregate shares (sum scalars) │ └─ aggregate_shares() → group::Share │ ├─ Create DkgOutput { polynomial, share, participants } │ └─ Mark complete: coordinator.completed[epoch] = output │ ▼REGISTRATION IN SUPERVISOR │ ├─ register_dkg_output(epoch, polynomial, share) │ └─ Keys available for threshold operations │ ▼USAGE │ ├─ TLE Encryption: polynomial.evaluate(0) as master public key (G2) │ ├─ TLE Decryption: │ ├─ Each validator: sign_decryption_share(share, epoch) → G1 │ ├─ Leader: combine_partial_signatures(t, partials) → G1 │ └─ Decrypt: tle::decrypt(threshold_sig, ciphertext) │ └─ VRF: Similar threshold signature flow10. Type Reference
Section titled “10. Type Reference”BLS12-381 MinSig Variant:├─ Public key: G2 element (96 bytes)├─ Signature: G1 element (48 bytes)└─ Secret: Scalar in Fr field
DKG Types:├─ Poly<Evaluation>: Polynomial with G2 coefficients├─ group::Share: { index: u32, secret: Scalar }└─ Threshold signature: G1 element
Identity:├─ Network identity: ed25519 public key (32 bytes)└─ Share encryption: X25519 (derived from ed25519)Related Documentation
Section titled “Related Documentation”- Sealed Transactions - TLE encryption/decryption using DKG keys
- Finalization Flow - Block finalization with BLS signatures