Skip to content

Commit 1bf9fe6

Browse files
committed
Merge branch 'fixPayload' into 'main'
fix: panic in FromSignedPayload on empty payload See merge request flarenetwork/FSP/flare-system-client!80
2 parents 362ed00 + 49c08b8 commit 1bf9fe6

5 files changed

Lines changed: 254 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
### Fixed
6+
7+
- Panic in `FromSignedPayload` when a submitSignatures transaction contained a zero-length payload; the empty slice is now rejected with an error and skipped by the caller.
8+
39
## [v.1.0.12](https://github.com/flare-foundation/flare-system-client/tree/v1.0.12) - 2026-4-17
410

511
### Added

client/finalizer/payload_utils.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ func ExtractPayloads(data []byte) ([]payloadMessage, error) {
3434
protocol := data[0] // 1 byte protocol ID
3535
votingRound := binary.BigEndian.Uint32(data[1:5]) // 4 bytes votingRoundID
3636
length := binary.BigEndian.Uint16(data[5:7]) // 2 bytes length of payload in bytes
37-
end := 7 + length
38-
if len(data) < int(end) {
37+
end := 7 + int(length)
38+
if len(data) < end {
3939
return nil, errors.New("wrongly formatted tx input")
4040
}
4141

@@ -70,6 +70,9 @@ type submitSignaturesPayload struct {
7070
}
7171

7272
func (s *submitSignaturesPayload) FromSignedPayload(payloadMsg payloadMessage) error {
73+
if len(payloadMsg.payload) < 1 {
74+
return errors.New("empty payload")
75+
}
7376
typeID := payloadMsg.payload[0]
7477

7578
var signatureStart, signatureEnd int
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package finalizer
2+
3+
import (
4+
"bytes"
5+
"encoding/binary"
6+
"testing"
7+
8+
"github.com/ethereum/go-ethereum/crypto"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/flare-foundation/flare-system-client/client/protocol"
12+
"github.com/flare-foundation/go-flare-common/pkg/payload"
13+
)
14+
15+
const (
16+
type0PayloadLen = 1 + 38 + 65 // typeID + message + signature
17+
type1PayloadLen = 1 + 65 // typeID + signature
18+
)
19+
20+
func TestFromSignedPayload(t *testing.T) {
21+
message := bytes.Repeat([]byte{0xAB}, 38)
22+
signature := bytes.Repeat([]byte{0xCD}, 65)
23+
24+
type0Payload := append(append([]byte{0x00}, message...), signature...)
25+
type1Payload := append([]byte{0x01}, signature...)
26+
27+
tests := []struct {
28+
name string
29+
msg payloadMessage
30+
wantErr bool
31+
check func(t *testing.T, s *submitSignaturesPayload)
32+
}{
33+
{
34+
name: "nil payload",
35+
msg: payloadMessage{payload: nil},
36+
wantErr: true,
37+
},
38+
{
39+
name: "empty payload",
40+
msg: payloadMessage{payload: []byte{}},
41+
wantErr: true,
42+
},
43+
{
44+
name: "type 0 only type byte",
45+
msg: payloadMessage{payload: []byte{0x00}},
46+
wantErr: true,
47+
},
48+
{
49+
name: "type 0 one byte short",
50+
msg: payloadMessage{payload: type0Payload[:type0PayloadLen-1]},
51+
wantErr: true,
52+
},
53+
{
54+
name: "type 1 one byte short",
55+
msg: payloadMessage{payload: type1Payload[:type1PayloadLen-1]},
56+
wantErr: true,
57+
},
58+
{
59+
name: "invalid typeID 2",
60+
msg: payloadMessage{payload: append([]byte{0x02}, signature...)},
61+
wantErr: true,
62+
},
63+
{
64+
name: "invalid typeID 0xFF",
65+
msg: payloadMessage{payload: append([]byte{0xFF}, signature...)},
66+
wantErr: true,
67+
},
68+
{
69+
name: "type 0 exact length",
70+
msg: payloadMessage{
71+
protocolID: 7,
72+
votingRoundID: 42,
73+
payload: type0Payload,
74+
},
75+
check: func(t *testing.T, s *submitSignaturesPayload) {
76+
t.Helper()
77+
require.Equal(t, uint8(7), s.protocolID)
78+
require.Equal(t, uint32(42), s.votingRoundID)
79+
require.Equal(t, uint8(0), s.typeID)
80+
require.Equal(t, message, []byte(s.message))
81+
require.Equal(t, signature, s.signature)
82+
require.Equal(t, -1, s.voterIndex)
83+
},
84+
},
85+
{
86+
name: "type 1 exact length",
87+
msg: payloadMessage{
88+
protocolID: 9,
89+
votingRoundID: 1234,
90+
payload: type1Payload,
91+
},
92+
check: func(t *testing.T, s *submitSignaturesPayload) {
93+
t.Helper()
94+
require.Equal(t, uint8(9), s.protocolID)
95+
require.Equal(t, uint32(1234), s.votingRoundID)
96+
require.Equal(t, uint8(1), s.typeID)
97+
require.Nil(t, s.message)
98+
require.Equal(t, signature, s.signature)
99+
require.Equal(t, -1, s.voterIndex)
100+
},
101+
},
102+
{
103+
name: "type 0 with trailing bytes",
104+
msg: payloadMessage{
105+
protocolID: 3,
106+
votingRoundID: 99,
107+
payload: append(append([]byte{}, type0Payload...), 0xAA, 0xBB, 0xCC),
108+
},
109+
check: func(t *testing.T, s *submitSignaturesPayload) {
110+
t.Helper()
111+
require.Equal(t, message, []byte(s.message))
112+
require.Equal(t, signature, s.signature)
113+
require.Len(t, s.signature, 65)
114+
require.Len(t, s.message, 38)
115+
},
116+
},
117+
{
118+
name: "type 1 with trailing bytes",
119+
msg: payloadMessage{
120+
protocolID: 3,
121+
votingRoundID: 99,
122+
payload: append(append([]byte{}, type1Payload...), 0xAA, 0xBB),
123+
},
124+
check: func(t *testing.T, s *submitSignaturesPayload) {
125+
t.Helper()
126+
require.Nil(t, s.message)
127+
require.Equal(t, signature, s.signature)
128+
require.Len(t, s.signature, 65)
129+
},
130+
},
131+
}
132+
133+
for _, tc := range tests {
134+
t.Run(tc.name, func(t *testing.T) {
135+
var s submitSignaturesPayload
136+
err := s.FromSignedPayload(tc.msg)
137+
if tc.wantErr {
138+
require.Error(t, err)
139+
return
140+
}
141+
require.NoError(t, err)
142+
tc.check(t, &s)
143+
})
144+
}
145+
}
146+
147+
// TestRoundTripWithEncodePayload generates a tx-style input with protocol.EncodePayload
148+
// for both protocol types, parses it back through ExtractPayloads and FromSignedPayload,
149+
// and confirms the round trip preserves protocolID, votingRoundID, typeID, and message.
150+
func TestRoundTripWithEncodePayload(t *testing.T) {
151+
privateKey, err := crypto.HexToECDSA(testPrivateKeyHex)
152+
require.NoError(t, err)
153+
154+
const votingRound int64 = 1234
155+
156+
type0Message := bytes.Repeat([]byte{0x11}, 38)
157+
type1Message := bytes.Repeat([]byte{0x22}, 16)
158+
159+
cases := []struct {
160+
protocolID uint8
161+
protocolType uint8
162+
data []byte
163+
}{
164+
{protocolID: 1, protocolType: 0, data: type0Message},
165+
{protocolID: 5, protocolType: 1, data: type1Message},
166+
}
167+
168+
buf := new(bytes.Buffer)
169+
buf.Write([]byte{0xde, 0xad, 0xbe, 0xef}) // function selector
170+
171+
for _, c := range cases {
172+
resp := &protocol.SubProtocolResponse{
173+
Status: payload.Ok,
174+
Data: c.data,
175+
}
176+
err := protocol.EncodePayload(buf, votingRound, resp, c.protocolID, c.protocolType, privateKey)
177+
require.NoError(t, err)
178+
}
179+
180+
payloads, err := ExtractPayloads(buf.Bytes())
181+
require.NoError(t, err)
182+
require.Len(t, payloads, len(cases))
183+
184+
for i, c := range cases {
185+
var s submitSignaturesPayload
186+
require.NoError(t, s.FromSignedPayload(payloads[i]))
187+
require.Equal(t, c.protocolID, s.protocolID)
188+
require.Equal(t, uint32(votingRound), s.votingRoundID)
189+
require.Equal(t, c.protocolType, s.typeID)
190+
require.Len(t, s.signature, 65)
191+
require.Equal(t, -1, s.voterIndex)
192+
if c.protocolType == 0 {
193+
require.Equal(t, c.data, []byte(s.message))
194+
} else {
195+
require.Nil(t, s.message)
196+
}
197+
}
198+
}
199+
200+
func TestExtractPayloadsZeroLength(t *testing.T) {
201+
// 4-byte selector + 1-byte protocol + 4-byte votingRound + 2-byte length=0
202+
data := make([]byte, 4+1+4+2)
203+
binary.BigEndian.PutUint32(data[5:9], 1)
204+
binary.BigEndian.PutUint16(data[9:11], 0)
205+
206+
payloads, err := ExtractPayloads(data)
207+
require.NoError(t, err)
208+
require.Len(t, payloads, 1)
209+
210+
var s submitSignaturesPayload
211+
require.Error(t, s.FromSignedPayload(payloads[0]))
212+
}
213+
214+
func TestExtractUint16Overflow(t *testing.T) {
215+
// 4-byte selector + 1-byte protocol + 4-byte votingRound + 2-byte length=0
216+
data := make([]byte, 4+1+4+2)
217+
binary.BigEndian.PutUint32(data[5:9], 1)
218+
binary.BigEndian.PutUint16(data[9:11], 0xffff)
219+
220+
_, err := ExtractPayloads(data)
221+
require.Error(t, err)
222+
223+
dataTrue := make([]byte, 4+1+4+2+0xffff)
224+
binary.BigEndian.PutUint32(dataTrue[5:9], 1)
225+
binary.BigEndian.PutUint16(dataTrue[9:11], 0xffff)
226+
227+
payloads, err := ExtractPayloads(dataTrue)
228+
require.NoError(t, err)
229+
require.Len(t, payloads, 1)
230+
}

client/finalizer/submission_processor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func (c *client) ProcessTransaction(tx database.Transaction) error {
2929
return nil
3030
}
3131

32-
signaturePayloads := []*submitSignaturesPayload{}
32+
signaturePayloads := make([]*submitSignaturesPayload, 0, len(payloads))
3333
for i := range payloads {
3434
signaturePayload := new(submitSignaturesPayload)
3535
err := signaturePayload.FromSignedPayload(payloads[i])

client/protocol/submitter.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,11 +257,21 @@ func newSignatureSubmitter(
257257
}
258258
}
259259

260-
// WritePayload encodes payload to buffer.
260+
// WritePayload encodes payload to buffer using the submitter's signing key.
261261
// Payload data should be valid (data length 38, additional data length <= maxuint16 - 66).
262262
// If an error is returned, the buffer is unchanged.
263263
func (s *SignatureSubmitter) WritePayload(
264264
buffer *bytes.Buffer, epoch int64, data *SubProtocolResponse, protocolID, protocolType uint8,
265+
) error {
266+
return EncodePayload(buffer, epoch, data, protocolID, protocolType, s.protocolContext.signerPrivateKey)
267+
}
268+
269+
// EncodePayload encodes a signed submitSignatures payload to buffer.
270+
// Payload data should be valid (data length 38, additional data length <= maxuint16 - 66).
271+
// If an error is returned, the buffer is unchanged.
272+
func EncodePayload(
273+
buffer *bytes.Buffer, epoch int64, data *SubProtocolResponse, protocolID, protocolType uint8,
274+
signerPrivateKey *ecdsa.PrivateKey,
265275
) error {
266276
var dataLength int
267277
switch protocolType {
@@ -274,7 +284,7 @@ func (s *SignatureSubmitter) WritePayload(
274284
}
275285

276286
dataHash := accounts.TextHash(crypto.Keccak256(data.Data))
277-
signature, err := crypto.Sign(dataHash, s.protocolContext.signerPrivateKey)
287+
signature, err := crypto.Sign(dataHash, signerPrivateKey)
278288
if err != nil {
279289
return errors.Wrap(err, "error signing submitSignatures data")
280290
}

0 commit comments

Comments
 (0)