Skip to content

fix: truncate every shred to canonical size in duplicate detection - #102

Open
latent-9 wants to merge 1 commit into
solana-program:mainfrom
latent-9:fix-non-canonical-shred-padding
Open

fix: truncate every shred to canonical size in duplicate detection#102
latent-9 wants to merge 1 commit into
solana-program:mainfrom
latent-9:fix-non-canonical-shred-padding

Conversation

@latent-9

Copy link
Copy Markdown

What

get_payload inside is_shred_duplicate only truncates resigned shreds to their canonical size. A non-resigned shred padded with extra trailing bytes therefore compares unequal to its unpadded twin and is treated as a duplicate, so a DuplicateBlockProof built from one genuinely signed shred and a padded copy of it records a violation against a leader that never equivocated.

Why

Shred::new_from_payload does not truncate the payload to the canonical shred size, and get_payload returns the full payload for non-resigned shreds:

if !shred.resigned {
    return shred.payload;
}

merkle_root() and the header getters read fixed offsets, so trailing bytes past the canonical size are ignored: a padded copy keeps the same merkle root, the same valid leader signature, and the same slot, index, and shred type. The only difference is_shred_duplicate observes is the byte length, so check_shreds returns Ok(()) and stores a violation for a leader that did not equivocate.

Upstream agave truncates every merkle shred to its canonical size at construction (ledger/src/shred/merkle.rs, payload.truncate(Self::SIZE_OF_PAYLOAD)), which is why its is_shred_duplicate is safe with the same non-resigned branch.

Change

Always truncate to proof_offset + proof_size in get_payload. For a non-resigned shred this equals the canonical size, so it strips only the trailing padding and never drops a real payload difference. Resigned shreds are unaffected, since their retransmitter signature still sits past proof_offset + proof_size.

Test

test_non_canonical_padding_payload_proof_invalid builds a non-resigned data shred and a non-resigned coding shred, pads each payload with one trailing byte, and asserts the proof is rejected with InvalidPayloadProof. It fails before the change (the padded shred is accepted as a duplicate) and passes after, and the existing tests continue to pass.

Fixes #101

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Non-canonical shred padding forges a duplicate-block proof against an innocent leader

1 participant