Skip to content

Commit a890c1d

Browse files
feat: add 34-slot TTL for ValidatorRegistration and VoluntaryExit messages (#711)
Related to #692 Adds time-to-live validation for ValidatorRegistration and VoluntaryExit messages, which previously had no lateness checks. These messages now use the same 34-slot TTL as Committee and Aggregator roles. This enables more resilient operator doppelgänger protection by: - Preventing replay attacks from malicious nodes - Bounding the acceptance window for stale messages - Working seamlessly with the doppelgänger grace period Co-Authored-By: diego <[email protected]>
1 parent ce7c8bc commit a890c1d

File tree

2 files changed

+216
-3
lines changed

2 files changed

+216
-3
lines changed

anchor/message_validator/src/lib.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -638,11 +638,9 @@ fn message_lateness(
638638
) -> Result<Duration, ValidationFailure> {
639639
let ttl = match validation_context.role {
640640
Role::Proposer | Role::SyncCommittee => 1 + LATE_SLOT_ALLOWANCE,
641-
Role::Committee | Role::Aggregator => {
641+
Role::Committee | Role::Aggregator | Role::ValidatorRegistration | Role::VoluntaryExit => {
642642
validation_context.slots_per_epoch + LATE_SLOT_ALLOWANCE
643643
}
644-
// No lateness check for these roles
645-
Role::ValidatorRegistration | Role::VoluntaryExit => return Ok(Duration::from_secs(0)),
646644
};
647645

648646
let deadline = slot_start_time(slot + ttl, validation_context.slot_clock.clone())

anchor/message_validator/src/partial_signature.rs

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -936,4 +936,219 @@ mod tests {
936936
"TripleValidatorIndexInPartialSignatures",
937937
);
938938
}
939+
940+
// TTL test constants
941+
const SLOTS_PER_EPOCH_TEST: u64 = 32;
942+
const LATE_SLOT_ALLOWANCE_TEST: u64 = 2;
943+
const TTL_SLOTS: u64 = SLOTS_PER_EPOCH_TEST + LATE_SLOT_ALLOWANCE_TEST; // 34 slots
944+
const BEYOND_TTL_SLOTS: u64 = 40;
945+
946+
// Helper to create validation context for TTL tests
947+
fn create_ttl_validation_context<'a>(
948+
signed_msg: &'a SignedSSVMessage,
949+
committee_info: &'a crate::CommitteeInfo,
950+
role: Role,
951+
operator_pub_keys: &'a HashMap<OperatorId, Rsa<Public>>,
952+
slots_late: u64,
953+
) -> ValidationContext<'a, ManualSlotClock> {
954+
let now = SystemTime::now();
955+
let slot_clock = ManualSlotClock::new(
956+
Slot::new(0),
957+
now.duration_since(UNIX_EPOCH).unwrap(),
958+
Duration::from_secs(12),
959+
);
960+
slot_clock.advance_slot();
961+
962+
ValidationContext {
963+
signed_ssv_message: signed_msg,
964+
committee_info,
965+
role,
966+
received_at: now + Duration::from_secs(12 * slots_late),
967+
slots_per_epoch: SLOTS_PER_EPOCH_TEST,
968+
epochs_per_sync_committee_period: 256,
969+
sync_committee_size: 512,
970+
slot_clock,
971+
operator_pub_keys,
972+
}
973+
}
974+
975+
// Helper to create a signed partial signature message for TTL tests
976+
fn create_signed_partial_sig_message(
977+
role: Role,
978+
kind: PartialSignatureKind,
979+
signer_id: OperatorId,
980+
private_key: &Rsa<Private>,
981+
) -> SignedSSVMessage {
982+
let (mut partial_sig_messages, _) = create_test_partial_signature(
983+
role,
984+
kind,
985+
signer_id,
986+
PartialSigTestOptions::default(),
987+
Some(private_key.clone()),
988+
);
989+
partial_sig_messages.slot = Slot::new(1);
990+
991+
let msg_id = create_message_id_for_test(role);
992+
let ssv_msg = SSVMessage::new(
993+
MsgType::SSVPartialSignatureMsgType,
994+
msg_id,
995+
partial_sig_messages.as_ssz_bytes(),
996+
)
997+
.unwrap();
998+
999+
let p_key = PKey::from_rsa(private_key.clone()).unwrap();
1000+
let mut signer = Signer::new(MessageDigest::sha256(), &p_key).unwrap();
1001+
signer.update(&ssv_msg.as_ssz_bytes()).unwrap();
1002+
let signature = signer.sign_to_vec().unwrap().try_into().unwrap();
1003+
1004+
SignedSSVMessage::new(vec![signature], vec![signer_id], ssv_msg, vec![]).unwrap()
1005+
}
1006+
1007+
#[test]
1008+
fn test_validator_registration_within_ttl_accepted() {
1009+
// Setup
1010+
let committee_info = create_committee_info(FOUR_NODE_COMMITTEE);
1011+
let (private_key, public_key) = generate_test_key_pair();
1012+
let map =
1013+
create_operator_pub_keys(committee_info.committee_members.clone(), vec![public_key]);
1014+
let signed_msg = create_signed_partial_sig_message(
1015+
Role::ValidatorRegistration,
1016+
PartialSignatureKind::ValidatorRegistration,
1017+
OperatorId(1),
1018+
&private_key,
1019+
);
1020+
1021+
let validation_context = create_ttl_validation_context(
1022+
&signed_msg,
1023+
&committee_info,
1024+
Role::ValidatorRegistration,
1025+
&map,
1026+
TTL_SLOTS,
1027+
);
1028+
1029+
// Execute
1030+
let result = validate_partial_signature_message(
1031+
validation_context,
1032+
&mut DutyState::new(64),
1033+
Arc::new(MockDutiesProvider {
1034+
voluntary_exit_duty_count: 0,
1035+
}),
1036+
);
1037+
1038+
// Assert
1039+
assert!(result.is_ok(), "Expected ok but got: {result:?}");
1040+
}
1041+
1042+
#[test]
1043+
fn test_validator_registration_beyond_ttl_rejected() {
1044+
// Setup
1045+
let committee_info = create_committee_info(FOUR_NODE_COMMITTEE);
1046+
let (private_key, public_key) = generate_test_key_pair();
1047+
let map =
1048+
create_operator_pub_keys(committee_info.committee_members.clone(), vec![public_key]);
1049+
let signed_msg = create_signed_partial_sig_message(
1050+
Role::ValidatorRegistration,
1051+
PartialSignatureKind::ValidatorRegistration,
1052+
OperatorId(1),
1053+
&private_key,
1054+
);
1055+
1056+
let validation_context = create_ttl_validation_context(
1057+
&signed_msg,
1058+
&committee_info,
1059+
Role::ValidatorRegistration,
1060+
&map,
1061+
BEYOND_TTL_SLOTS,
1062+
);
1063+
1064+
// Execute
1065+
let result = validate_partial_signature_message(
1066+
validation_context,
1067+
&mut DutyState::new(64),
1068+
Arc::new(MockDutiesProvider {
1069+
voluntary_exit_duty_count: 0,
1070+
}),
1071+
);
1072+
1073+
// Assert
1074+
assert_validation_error(
1075+
result,
1076+
|failure| matches!(failure, ValidationFailure::LateSlotMessage { .. }),
1077+
"LateSlotMessage",
1078+
);
1079+
}
1080+
1081+
#[test]
1082+
fn test_voluntary_exit_within_ttl_accepted() {
1083+
// Setup
1084+
let committee_info = create_committee_info(FOUR_NODE_COMMITTEE);
1085+
let (private_key, public_key) = generate_test_key_pair();
1086+
let map =
1087+
create_operator_pub_keys(committee_info.committee_members.clone(), vec![public_key]);
1088+
let signed_msg = create_signed_partial_sig_message(
1089+
Role::VoluntaryExit,
1090+
PartialSignatureKind::VoluntaryExit,
1091+
OperatorId(1),
1092+
&private_key,
1093+
);
1094+
1095+
let validation_context = create_ttl_validation_context(
1096+
&signed_msg,
1097+
&committee_info,
1098+
Role::VoluntaryExit,
1099+
&map,
1100+
TTL_SLOTS,
1101+
);
1102+
1103+
// Execute
1104+
let result = validate_partial_signature_message(
1105+
validation_context,
1106+
&mut DutyState::new(64),
1107+
Arc::new(MockDutiesProvider {
1108+
voluntary_exit_duty_count: 1,
1109+
}),
1110+
);
1111+
1112+
// Assert
1113+
assert!(result.is_ok(), "Expected ok but got: {result:?}");
1114+
}
1115+
1116+
#[test]
1117+
fn test_voluntary_exit_beyond_ttl_rejected() {
1118+
// Setup
1119+
let committee_info = create_committee_info(FOUR_NODE_COMMITTEE);
1120+
let (private_key, public_key) = generate_test_key_pair();
1121+
let map =
1122+
create_operator_pub_keys(committee_info.committee_members.clone(), vec![public_key]);
1123+
let signed_msg = create_signed_partial_sig_message(
1124+
Role::VoluntaryExit,
1125+
PartialSignatureKind::VoluntaryExit,
1126+
OperatorId(1),
1127+
&private_key,
1128+
);
1129+
1130+
let validation_context = create_ttl_validation_context(
1131+
&signed_msg,
1132+
&committee_info,
1133+
Role::VoluntaryExit,
1134+
&map,
1135+
BEYOND_TTL_SLOTS,
1136+
);
1137+
1138+
// Execute
1139+
let result = validate_partial_signature_message(
1140+
validation_context,
1141+
&mut DutyState::new(64),
1142+
Arc::new(MockDutiesProvider {
1143+
voluntary_exit_duty_count: 1,
1144+
}),
1145+
);
1146+
1147+
// Assert
1148+
assert_validation_error(
1149+
result,
1150+
|failure| matches!(failure, ValidationFailure::LateSlotMessage { .. }),
1151+
"LateSlotMessage",
1152+
);
1153+
}
9391154
}

0 commit comments

Comments
 (0)