Skip to content

Commit f3dadfe

Browse files
rootulpclaude
andauthored
fix: correct pruning condition in RS Decode for last part padding (#2794)
## Summary - Fix a bug in `Decode` (`types/part_set.go`) where `len(data[:(ops.Total()-1)])` compared a **part count** against `lastPartLen` (a **byte length**). When `Total-1` numerically equaled `lastPartLen`, pruning of RS padding was skipped, causing a Merkle root mismatch and breaking RS recovery for block data sizes of `k * 65537` bytes. - Change the condition to `len(data[ops.Total()-1])` which correctly compares the byte length of the last reconstructed part against the expected unpadded length. - Extract the pruning logic into a standalone `pruneLastPart` function for testability. - Replace the complex `TestDecodePruningBug` (which spun up full RS encode/decode machinery) with a focused `TestPruneLastPart` unit test that directly exercises the extracted function. ## Test plan - [x] `TestPruneLastPart` passes for both subtests (prunes when padded, no-op when already correct) - [x] Existing `TestEncodingDecodingRoundTrip` continues to pass - [ ] Run full `go test ./types/` suite 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f968cad commit f3dadfe

File tree

2 files changed

+29
-3
lines changed

2 files changed

+29
-3
lines changed

types/part_set.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,14 @@ func (ps *PartSet) IsReadyForDecoding() bool {
363363
return ps.IsComplete()
364364
}
365365

366+
// pruneLastPart trims the last original part to its true length, removing any
367+
// Reed-Solomon padding bytes that were added during encoding.
368+
func pruneLastPart(data [][]byte, lastIndex uint32, lastPartLen int) {
369+
if len(data[lastIndex]) != lastPartLen {
370+
data[lastIndex] = data[lastIndex][:lastPartLen]
371+
}
372+
}
373+
366374
// Decode uses the block parts that are provided to reconstruct the original
367375
// data. It throws an error if the PartSet is incomplete or the resulting root
368376
// is different from that in the PartSetHeader. Parts are fully complete with
@@ -410,9 +418,7 @@ func Decode(ops, eps *PartSet, lastPartLen int) (*PartSet, *PartSet, error) {
410418
}
411419

412420
// prune the last part if we need to
413-
if len(data[:(ops.Total()-1)]) != lastPartLen {
414-
data[(ops.Total() - 1)] = data[(ops.Total() - 1)][:lastPartLen]
415-
}
421+
pruneLastPart(data, ops.Total()-1, lastPartLen)
416422

417423
// recalculate all of the proofs since we apparently don't have a function
418424
// to generate a single proof... TODO: don't generate proofs for block parts

types/part_set_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,26 @@ func TestEncodingDecodingRoundTrip(t *testing.T) {
141141
}
142142
}
143143

144+
func TestPruneLastPart(t *testing.T) {
145+
tests := []struct {
146+
name string
147+
partLen int
148+
lastPartLen int
149+
wantLen int
150+
}{
151+
{"prunes when padded", 100, 50, 50},
152+
{"no-op when already correct", 50, 50, 50},
153+
}
154+
for _, tt := range tests {
155+
t.Run(tt.name, func(t *testing.T) {
156+
lastIndex := uint32(1)
157+
data := [][]byte{make([]byte, 100), make([]byte, tt.partLen)}
158+
pruneLastPart(data, lastIndex, tt.lastPartLen)
159+
require.Len(t, data[lastIndex], tt.wantLen)
160+
})
161+
}
162+
}
163+
144164
func TestEncoding(t *testing.T) {
145165
data := cmtrand.Bytes(testPartSize * 100)
146166
partSet, err := NewPartSetFromData(data, testPartSize)

0 commit comments

Comments
 (0)