PoC for this finding.
Checkout: ghost-node at 0162da4604d9931c16d12a0cec40dd85951368c5.
Append these tests to pallets/weaver/src/tests.rs, then run:
cargo test -p ghost-weaver --offline --lib poc_attestation_vec
Remove the appended tests afterwards to leave the tree clean.
fn w2a_seed(i: u32) -> AuthorityPair {
AuthorityPair::from_string(&format!("//W2A{}", i), None).expect("valid seed")
}
fn w2a_rotate_to(n: u32) {
let grown: Vec<AuthorityPair> = (0..n).map(w2a_seed).collect();
let validators: Vec<(sp_runtime::AccountId32, crate::sr25519::AuthorityId)> = grown
.iter()
.enumerate()
.map(|(i, p)| {
let mut id = [0u8; 32];
id[0..8].copy_from_slice(&((i + 1) as u64).to_le_bytes());
(sp_runtime::AccountId32::from(id), p.public())
})
.collect();
<GhostWeaver as frame_support::traits::OneSessionHandler<sp_runtime::AccountId32>>::on_new_session(
false,
validators.iter().map(|(a, k)| (a, k.clone())).collect::<Vec<(&sp_runtime::AccountId32, crate::sr25519::AuthorityId)>>().into_iter(),
validators.iter().map(|(a, k)| (a, k.clone())).collect::<Vec<(&sp_runtime::AccountId32, crate::sr25519::AuthorityId)>>().into_iter(),
);
}
#[test]
fn poc_attestation_vec_never_grows_bricks_consensus_after_authority_growth() {
let initial: Vec<AuthorityPair> = (0..5).map(w2a_seed).collect();
let network_id = 1u32;
let network_data = NetworkDataBuilder::default().with_block_deviation(5).build();
let networks = vec![(network_id, network_data, 0u64)];
new_test_ext(initial.clone(), networks).execute_with(|| {
System::set_block_number(2);
// Five authorities attest; the vec is sized to 5.
for (i, pair) in initial.iter().enumerate() {
let att = AttestationContext::default()
.with_current_block(1)
.with_network_id(network_id)
.with_authority_index(i as u16)
.build_block_attestation(100 + i as u64);
let sig = pair.sign(&att.encode());
assert!(GhostWeaver::extend_warp(RuntimeOrigin::none(), att, sig).is_ok());
}
assert_eq!(GhostWeaver::network_attestations(network_id).len(), 5);
// Honest session rotation grows the validator set 5 -> 8.
w2a_rotate_to(8);
assert_eq!(GhostWeaver::authorities().len(), 8);
// A new authority's attestation is rejected: the vec was never resized.
System::set_block_number(3);
let new_pair = w2a_seed(7);
let att_new = AttestationContext::default()
.with_current_block(2)
.with_network_id(network_id)
.with_authority_index(7u16)
.build_block_attestation(200);
let sig_new = new_pair.sign(&att_new.encode());
let res = GhostWeaver::extend_warp(RuntimeOrigin::none(), att_new, sig_new);
assert_eq!(res, Err(Error::<TestRuntime>::TooManyAttestations.into()));
// on_initialize cannot reach consensus: 5 votes < byzantium threshold of 8 (= 6).
GhostWeaver::on_initialize(4);
assert!(GhostWeaver::tapestry_drafts(network_id).is_none());
// The stale vec is still there, still short -- nothing healed it.
assert_eq!(GhostWeaver::network_attestations(network_id).len(), 5);
});
}
#[test]
fn poc_attestation_vec_stale_len_survives_at_seven_authorities() {
let initial: Vec<AuthorityPair> = (0..5).map(w2a_seed).collect();
let network_id = 1u32;
let network_data = NetworkDataBuilder::default().with_block_deviation(5).build();
let networks = vec![(network_id, network_data, 0u64)];
new_test_ext(initial.clone(), networks).execute_with(|| {
System::set_block_number(2);
for (i, pair) in initial.iter().enumerate() {
let att = AttestationContext::default()
.with_current_block(1)
.with_network_id(network_id)
.with_authority_index(i as u16)
.build_block_attestation(100 + i as u64);
let sig = pair.sign(&att.encode());
assert!(GhostWeaver::extend_warp(RuntimeOrigin::none(), att, sig).is_ok());
}
// Grow 5 -> 7: threshold(7) = 5, so the 5 old votes still reach consensus
// (degraded -- new authorities are voiceless -- but alive).
w2a_rotate_to(7);
System::set_block_number(3);
let new_pair = w2a_seed(6);
let att_new = AttestationContext::default()
.with_current_block(2)
.with_network_id(network_id)
.with_authority_index(6u16)
.build_block_attestation(200);
let sig_new = new_pair.sign(&att_new.encode());
let res = GhostWeaver::extend_warp(RuntimeOrigin::none(), att_new, sig_new);
assert_eq!(res, Err(Error::<TestRuntime>::TooManyAttestations.into()));
GhostWeaver::on_initialize(4);
assert!(GhostWeaver::tapestry_drafts(network_id).is_some());
});
}
Observed: both ok. First test pins the death cliff: 5 to 8 bricks consensus, no TapestryDraft, vec stuck at 5. Second test shows 5 to 7 survives degraded.
Shahmeer