Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions program/src/duplicate_block_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,63 @@ mod tests {
)
}

#[test]
fn test_non_canonical_padding_payload_proof_invalid() {
// A non-resigned shred padded with extra trailing bytes must not be
// accepted as a duplicate of the original shred. Both carry the same
// leader signature and merkle root (the fixed-offset merkle_root ignores
// the padding), the same slot, index, and type, so the only difference is
// the payload length. get_payload truncates to the canonical size, so the
// padded and original payloads compare equal and the proof is rejected.
let mut rng = rand::rng();
let leader = Arc::new(Keypair::new());
let leader_pubkey = leader.pubkey();
let shredder = Shredder::new(SLOT, PARENT_SLOT, REFERENCE_TICK, VERSION).unwrap();
let next_shred_index = rng.random_range(0..32_000);

// is_last_in_slot = false yields non-resigned merkle shreds (data + coding).
let (data_shreds, coding_shreds) = new_rand_shreds(
&mut rng,
next_shred_index,
next_shred_index,
10,
true, /* merkle_variant */
&shredder,
&leader,
false, /* is_last_in_slot */
);

for shred in [data_shreds[0].clone(), coding_shreds[0].clone()] {
let payload = shred.payload().as_ref().to_vec();
let mut padded = payload.clone();
padded.push(0u8);

let merkle_root = Shred::new_from_payload(&payload)
.unwrap()
.merkle_root()
.unwrap();
let signature: [u8; SIGNATURE_BYTES] =
shred.signature().as_ref().try_into().unwrap();
let context = DuplicateBlockProofContext {
expected_pubkey: &leader_pubkey,
expected_shred1_merkle_root: &merkle_root,
expected_shred2_merkle_root: &merkle_root,
expected_shred1_signature: &signature,
expected_shred2_signature: &signature,
};
let proof_data = DuplicateBlockProofData {
shred1: &payload,
shred2: &padded,
};
assert_eq!(
proof_data
.verify_proof(context, SLOT, &leader_pubkey)
.unwrap_err(),
SlashingError::InvalidPayloadProof,
);
}
}

#[test]
fn test_unpack_context() {
let node_pubkey = Pubkey::new_unique();
Expand Down
10 changes: 7 additions & 3 deletions program/src/shred.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,13 @@ impl<'a> Shred<'a> {
let Ok((proof_offset, proof_size)) = shred.get_proof_offset_and_size() else {
return shred.payload;
};
if !shred.resigned {
return shred.payload;
}
// Truncate to the canonical payload end for every shred, not just
// resigned ones. For a non-resigned shred `proof_offset + proof_size`
// equals the canonical size, so any trailing bytes past it (which the
// fixed-offset `merkle_root()` and header getters ignore) are excluded
// from the comparison. Without this a shred padded with extra trailing
// bytes compares unequal to its unpadded twin and is treated as a
// duplicate even though it is the same shred.
let Some(offset) = proof_offset.checked_add(proof_size) else {
return shred.payload;
};
Expand Down