@@ -98,9 +98,8 @@ enum NextCommittee {
98
98
/// As part of the Timeboost protocol, it produces decryption shares by threshold-decrypting these
99
99
/// ciphertexts. The shares are exchanged with other decrypters, and once a sufficient number are
100
100
/// collected, shares can be combined ("hatching") to obtain the plaintext.
101
- ///
102
- /// In addition, the `Decrypter` extracts DKG bundles from candidate lists and combines them to derive
103
- /// the keys used for threshold decryption/combining.
101
+ /// In addition, the `Decrypter` extracts DKG bundles from candidate lists and combines them to
102
+ /// derive the keys used for threshold decryption/combining.
104
103
pub struct Decrypter {
105
104
/// Public key of the node.
106
105
label : PublicKey ,
@@ -398,10 +397,10 @@ enum WorkerState {
398
397
/// Obtains the threshold decryption key from DKG bundles.
399
398
///
400
399
/// A node can recover its threshold decryption key in two ways:
401
- /// 1. **Consensus**: by combining DKG bundles extracted directly from candidate lists
402
- /// produced by peers.
403
- /// 2. **Network**: by receiving and combining an agreed-upon subset of DKG bundles from a
404
- /// designated set of peers.
400
+ /// 1. **Consensus**: by combining DKG bundles extracted directly from candidate lists produced
401
+ /// by peers.
402
+ /// 2. **Network**: by receiving and combining an agreed-upon subset of DKG bundles from a
403
+ /// designated set of peers.
405
404
///
406
405
/// For the initial DKG, method (1) is used.
407
406
/// For resharing, method (2) is used, with the source peers being the previous committee.
@@ -598,7 +597,7 @@ impl Worker {
598
597
. unwrap_or ( RoundNumber :: genesis ( ) )
599
598
}
600
599
601
- /// Returns true iff DKG is completed for the committee (ongoing or no pending DKG found)
600
+ /// Returns true iff DKG is completed for the given committee.
602
601
fn dkg_completed ( & self , committee_id : & CommitteeId ) -> bool {
603
602
if let Some ( acc) = self . tracker . get ( committee_id) {
604
603
acc. completed ( )
@@ -713,7 +712,7 @@ impl Worker {
713
712
let acc = DkgAccumulator :: from_subset ( current. clone ( ) , subset. to_owned ( ) ) ;
714
713
self . tracker . insert ( committee. id ( ) , acc) ;
715
714
let dec_key = subset
716
- . extract_key ( & current, & self . dkg_sk , prev. as_ref ( ) )
715
+ . extract_key ( current, & self . dkg_sk , prev. as_ref ( ) )
717
716
. map_err ( |e| DecrypterError :: Dkg ( e. to_string ( ) ) ) ?;
718
717
719
718
self . dec_key . set ( dec_key) ;
@@ -895,9 +894,7 @@ impl Worker {
895
894
/// NOTE: when a ciphertext is malformed, we will skip decrypting it (treat as garbage) here.
896
895
/// but will later be marked as decrypted during `hatch()`
897
896
async fn decrypt ( & mut self , incl : & InclusionList ) -> Result < DecShareBatch > {
898
- let dec_key: DecryptionKey = self . dec_key . get ( ) . ok_or_else ( || {
899
- DecrypterError :: Internal ( "Worker running without dec key" . to_string ( ) )
900
- } ) ?;
897
+ let dec_key: DecryptionKey = self . decryption_key ( ) ?;
901
898
902
899
let round = Round :: new ( incl. round ( ) , self . current ) ;
903
900
let ciphertexts: Vec < _ > = incl
@@ -963,51 +960,105 @@ impl Worker {
963
960
Ok ( ( ) )
964
961
}
965
962
966
- /// Attempt to hatch for round, returns Ok(Some(_)) if hatched successfully, Ok(None) if
967
- /// insufficient shares or inclusion list yet received (hatching target arrive later than
968
- /// decrypted shares is possible due to out-of-order delivery).
969
- /// Local cache are garbage collected for hatched rounds.
963
+ /// Attempts to decrypt and finalize an inclusion list for the given round number.
970
964
async fn hatch ( & mut self , round : RoundNumber ) -> Result < Option < InclusionList > > {
971
- let dec_key = match & self . state {
972
- WorkerState :: Running => self . dec_key . get ( ) . ok_or_else ( || {
973
- DecrypterError :: Internal ( "Worker running without dec key" . to_string ( ) )
974
- } ) ?,
975
- _ => {
976
- return Err ( DecrypterError :: Dkg (
977
- "(hatching) worker state does not hold decryption key" . to_string ( ) ,
978
- ) ) ;
979
- }
980
- } ;
965
+ let dec_key = self . decryption_key ( ) ?;
981
966
982
967
let Some ( ( incl, is_encrypted) ) = self . incls . get ( & round) else {
983
968
return Ok ( None ) ;
984
969
} ;
985
970
let mut incl = incl. clone ( ) ;
986
971
987
- // return immediately to parent if no encrypted transactions
988
972
if !is_encrypted {
989
- self . gc ( & round) . await ?;
990
- self . last_hatched_round = round ;
973
+ return Ok ( Some ( self . finalize_hatch ( round, incl ) . await ?) ) ;
974
+ }
991
975
992
- self . tx
993
- . send ( incl. clone ( ) )
994
- . await
995
- . map_err ( |_| EndOfPlay :: DecrypterDown ) ?;
996
- return Ok ( Some ( incl) ) ;
976
+ let Some ( decrypted) = self . decrypt_ciphertexts ( round, & incl, & dec_key) . await ? else {
977
+ return Ok ( None ) ;
978
+ } ;
979
+
980
+ self . update_inclusion_list ( & mut incl, decrypted) ?;
981
+
982
+ Ok ( Some ( self . finalize_hatch ( round, incl) . await ?) )
983
+ }
984
+
985
+ /// Get the current decryption key
986
+ fn decryption_key ( & self ) -> Result < DecryptionKey > {
987
+ match & self . state {
988
+ WorkerState :: Running => self . dec_key . get ( ) . ok_or_else ( || {
989
+ DecrypterError :: Internal ( "Worker running without dec key" . to_string ( ) )
990
+ } ) ,
991
+ _ => Err ( DecrypterError :: Dkg (
992
+ "(hatching) worker state does not hold decryption key" . to_string ( ) ,
993
+ ) ) ,
994
+ }
995
+ }
996
+
997
+ /// Get the current key store for the committee.
998
+ fn current_store ( & self ) -> Result < KeyStore > {
999
+ let guard = self . key_stores . read ( ) ;
1000
+ guard
1001
+ . get ( self . current )
1002
+ . cloned ( )
1003
+ . ok_or_else ( || DecrypterError :: NoCommittee ( self . current ) )
1004
+ }
1005
+
1006
+ /// Update the inclusion list with decrypted plaintexts.
1007
+ fn update_inclusion_list (
1008
+ & self ,
1009
+ incl : & mut InclusionList ,
1010
+ decrypted : Vec < Option < Plaintext > > ,
1011
+ ) -> Result < ( ) > {
1012
+ let mut num_encrypted_priority_bundles = 0 ;
1013
+
1014
+ // update priority bundles
1015
+ incl. priority_bundles_mut ( )
1016
+ . iter_mut ( )
1017
+ . filter ( |pb| pb. bundle ( ) . is_encrypted ( ) )
1018
+ . zip ( decrypted. clone ( ) )
1019
+ . for_each ( |( pb, opt_plaintext) | {
1020
+ num_encrypted_priority_bundles += 1 ;
1021
+ match opt_plaintext {
1022
+ Some ( pt) => pb. set_data ( timeboost_types:: Bytes :: from ( <Vec < u8 > >:: from ( pt) ) ) ,
1023
+ // None means garbage (undecryptable ciphertext), simply mark as decrypted
1024
+ None => pb. set_data ( pb. bundle ( ) . data ( ) . clone ( ) ) ,
1025
+ }
1026
+ } ) ;
1027
+
1028
+ // update regular bundles
1029
+ incl. regular_bundles_mut ( )
1030
+ . iter_mut ( )
1031
+ . filter ( |b| b. is_encrypted ( ) )
1032
+ . zip ( decrypted[ num_encrypted_priority_bundles..] . to_vec ( ) )
1033
+ . for_each ( |( bundle, opt_plaintext) | {
1034
+ match opt_plaintext {
1035
+ Some ( pt) => bundle. set_data ( timeboost_types:: Bytes :: from ( <Vec < u8 > >:: from ( pt) ) ) ,
1036
+ // None means garbage (undecryptable ciphertext), simply mark as decrypted
1037
+ None => bundle. set_data ( bundle. data ( ) . clone ( ) ) ,
1038
+ }
1039
+ } ) ;
1040
+
1041
+ if incl. is_encrypted ( ) {
1042
+ return Err ( DecrypterError :: Internal (
1043
+ "didn't fully decrypt inclusion list" . to_string ( ) ,
1044
+ ) ) ;
997
1045
}
998
1046
1047
+ Ok ( ( ) )
1048
+ }
1049
+
1050
+ /// Decrypt ciphertexts using available shares.
1051
+ async fn decrypt_ciphertexts (
1052
+ & mut self ,
1053
+ round : RoundNumber ,
1054
+ incl : & InclusionList ,
1055
+ dec_key : & DecryptionKey ,
1056
+ ) -> Result < Option < Vec < Option < Plaintext > > > > {
999
1057
let ciphertexts = incl. filter_ciphertexts ( ) . map ( |b| deserialize ( b) . ok ( ) ) ;
1000
1058
let Some ( dec_shares) = self . dec_shares . get ( & round) else {
1001
1059
return Ok ( None ) ;
1002
1060
} ;
1003
- let key_store = {
1004
- let guard = self . key_stores . read ( ) ;
1005
- let Some ( key_store) = guard. get ( self . current ) else {
1006
- error ! ( node = %self . label, committee = %self . current, "current committee not found" ) ;
1007
- return Err ( DecrypterError :: NoCommittee ( self . current ) ) ;
1008
- } ;
1009
- key_store. clone ( )
1010
- } ;
1061
+ let key_store = self . current_store ( ) ?;
1011
1062
1012
1063
if dec_shares. is_empty ( )
1013
1064
|| dec_shares. iter ( ) . any ( |opt_dec_shares| {
@@ -1020,13 +1071,6 @@ impl Worker {
1020
1071
return Ok ( None ) ;
1021
1072
}
1022
1073
1023
- // hatching ciphertext
1024
- // Option<_> uses None to indicate either invalid ciphertext, or 2f+1 invalid decryption
1025
- // share both imply "skip hatching this garbage bundle which will result in no-op
1026
- // during execution"
1027
-
1028
- let mut decrypted: Vec < Option < Plaintext > > = vec ! [ ] ;
1029
-
1030
1074
let Some ( per_ct_opt_dec_shares) = self . dec_shares . get_mut ( & round) else {
1031
1075
return Ok ( None ) ;
1032
1076
} ;
@@ -1052,7 +1096,7 @@ impl Worker {
1052
1096
. into_par_iter ( )
1053
1097
. zip ( per_ct_opt_dec_shares. par_iter_mut ( ) )
1054
1098
. map ( |( maybe_ct, decryption_shares) | {
1055
- // Collect valid decryption shares
1099
+ // collect valid decryption shares
1056
1100
let valid_shares: Vec < _ > = decryption_shares
1057
1101
. iter ( )
1058
1102
. filter_map ( |s| s. as_ref ( ) )
@@ -1080,7 +1124,7 @@ impl Worker {
1080
1124
) {
1081
1125
Ok ( pt) => CombineResult :: Success ( pt) ,
1082
1126
Err ( ThresholdEncError :: FaultySubset ( wrong_indices) ) => {
1083
- CombineResult :: FaultySubset ( wrong_indices. into ( ) )
1127
+ CombineResult :: FaultySubset ( wrong_indices)
1084
1128
}
1085
1129
Err ( e) => CombineResult :: Error ( e) ,
1086
1130
}
@@ -1090,18 +1134,19 @@ impl Worker {
1090
1134
} )
1091
1135
. await ?;
1092
1136
1137
+ let mut decrypted = Vec :: new ( ) ;
1093
1138
for ( result, decryption_shares) in combine_results. into_iter ( ) . zip ( per_ct_opt_dec_shares) {
1094
1139
match result {
1095
1140
CombineResult :: Success ( pt) => decrypted. push ( Some ( pt) ) ,
1096
1141
CombineResult :: FaultySubset ( wrong_indices) => {
1097
- // Remove faulty decryption shares
1142
+ // remove faulty decryption shares
1098
1143
decryption_shares. retain ( |opt_s| {
1099
1144
opt_s
1100
1145
. clone ( )
1101
1146
. is_none_or ( |s| !wrong_indices. contains ( & s. index ( ) ) )
1102
1147
} ) ;
1103
1148
warn ! ( node = %self . label, ?wrong_indices, "combine found faulty subset" ) ;
1104
- // Not ready to hatch this ciphertext
1149
+ // not ready to hatch this ciphertext
1105
1150
return Ok ( None ) ;
1106
1151
}
1107
1152
CombineResult :: Error ( e) => {
@@ -1114,50 +1159,27 @@ impl Worker {
1114
1159
}
1115
1160
}
1116
1161
1117
- // construct/modify the inclusion list to replace with decrypted payload
1118
- let mut num_encrypted_priority_bundles = 0 ;
1119
- incl. priority_bundles_mut ( )
1120
- . iter_mut ( )
1121
- . filter ( |pb| pb. bundle ( ) . is_encrypted ( ) )
1122
- . zip ( decrypted. clone ( ) )
1123
- . for_each ( |( pb, opt_plaintext) | {
1124
- num_encrypted_priority_bundles += 1 ;
1125
- match opt_plaintext {
1126
- Some ( pt) => pb. set_data ( timeboost_types:: Bytes :: from ( <Vec < u8 > >:: from ( pt) ) ) ,
1127
- // None means garbage (undecryptable ciphertext), simply mark as decrypted
1128
- None => pb. set_data ( pb. bundle ( ) . data ( ) . clone ( ) ) ,
1129
- }
1130
- } ) ;
1131
- incl. regular_bundles_mut ( )
1132
- . iter_mut ( )
1133
- . filter ( |b| b. is_encrypted ( ) )
1134
- . zip ( decrypted[ num_encrypted_priority_bundles..] . to_vec ( ) )
1135
- . for_each ( |( bundle, opt_plaintext) | {
1136
- match opt_plaintext {
1137
- Some ( pt) => bundle. set_data ( timeboost_types:: Bytes :: from ( <Vec < u8 > >:: from ( pt) ) ) ,
1138
- // None means garbage (undecryptable ciphertext), simply mark as decrypted
1139
- None => bundle. set_data ( bundle. data ( ) . clone ( ) ) ,
1140
- }
1141
- } ) ;
1142
- if incl. is_encrypted ( ) {
1143
- return Err ( DecrypterError :: Internal (
1144
- "didn't fully decrypt inclusion list" . to_string ( ) ,
1145
- ) ) ;
1146
- }
1162
+ Ok ( Some ( decrypted) )
1163
+ }
1147
1164
1148
- // garbage collect hatched rounds
1165
+ /// Finalize the hatching process by updating state and sending the inclusion list.
1166
+ async fn finalize_hatch (
1167
+ & mut self ,
1168
+ round : RoundNumber ,
1169
+ incl : InclusionList ,
1170
+ ) -> Result < InclusionList > {
1149
1171
self . last_hatched_round = round;
1150
-
1151
1172
self . gc ( & round) . await ?;
1152
1173
1153
1174
self . tx
1154
1175
. send ( incl. clone ( ) )
1155
1176
. await
1156
1177
. map_err ( |_| EndOfPlay :: DecrypterDown ) ?;
1157
1178
1158
- Ok ( Some ( incl) )
1179
+ Ok ( incl)
1159
1180
}
1160
1181
1182
+ /// Adds a new committee to the worker and updates network connections.
1161
1183
async fn on_next_committee ( & mut self , c : AddressableCommittee , k : KeyStore ) -> Result < ( ) > {
1162
1184
info ! ( node = %self . label, committee = %c. committee( ) . id( ) , "add next committee" ) ;
1163
1185
let key_store = {
@@ -1188,6 +1210,17 @@ impl Worker {
1188
1210
Ok ( ( ) )
1189
1211
}
1190
1212
1213
+ /// Prepares for a committee switch at the specified round.
1214
+ ///
1215
+ /// This method is called on nodes that are members of the upcoming committee.
1216
+ /// It handles two distinct cases:
1217
+ ///
1218
+ /// 1. **New Committee Member**: If this is the node's first committee, it requests DKG subsets
1219
+ /// from the current committee to construct the threshold decryption key.
1220
+ ///
1221
+ /// 2. **Existing Committee Member**: If the node is in both current and next committee, it
1222
+ /// generates and caches the new decryption keys immediately, ensuring they're ready for use
1223
+ /// when the switch occurs at the specified round.
1191
1224
async fn on_use_committee ( & mut self , round : Round ) -> Result < ( ) > {
1192
1225
info ! ( node = %self . label, %round, "use committee" ) ;
1193
1226
let committee_id = round. committee ( ) ;
@@ -1239,6 +1272,7 @@ impl Worker {
1239
1272
Ok ( ( ) )
1240
1273
}
1241
1274
1275
+ /// Switches to the next committee if the clock is past the start round.
1242
1276
async fn maybe_switch_committee ( & mut self ) -> Result < ( ) > {
1243
1277
let NextCommittee :: Use ( start, next_key) = & self . next_committee else {
1244
1278
return Ok ( ( ) ) ;
@@ -1288,6 +1322,7 @@ impl Worker {
1288
1322
Ok ( ( ) )
1289
1323
}
1290
1324
1325
+ /// Removes the old committee and updates network connections.
1291
1326
async fn remove_old_committee ( & mut self ) -> Result < ( ) > {
1292
1327
let NextCommittee :: Del ( round) = self . next_committee else {
1293
1328
return Ok ( ( ) ) ;
@@ -1297,13 +1332,7 @@ impl Worker {
1297
1332
return Ok ( ( ) ) ;
1298
1333
} ;
1299
1334
1300
- let key_store = {
1301
- let guard = self . key_stores . read ( ) ;
1302
- let Some ( key_store) = guard. get ( self . current ) else {
1303
- return Err ( DecrypterError :: NoCommittee ( self . current ) ) ;
1304
- } ;
1305
- key_store. clone ( )
1306
- } ;
1335
+ let key_store = self . current_store ( ) ?;
1307
1336
let old = self
1308
1337
. net
1309
1338
. parties ( )
0 commit comments