Skip to content

Commit 6140c6f

Browse files
authored
fix(leios): fix vote message (#1863)
Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
1 parent 267610d commit 6140c6f

2 files changed

Lines changed: 111 additions & 11 deletions

File tree

protocol/leiosnotify/messages.go

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ func NewMsgFromCbor(msgType uint, data []byte) (protocol.Message, error) {
4848
ret = &MsgVotesOffer{}
4949
case MessageTypeDone:
5050
ret = &MsgDone{}
51+
default:
52+
return nil, fmt.Errorf("%s: unknown message type %d", ProtocolName, msgType)
5153
}
5254
if _, err := cbor.Decode(data, ret); err != nil {
5355
return nil, fmt.Errorf("%s: decode error: %w", ProtocolName, err)
5456
}
55-
if ret != nil {
56-
// Store the raw message CBOR
57-
ret.SetCbor(data)
58-
}
57+
// Store the raw message CBOR
58+
ret.SetCbor(data)
5959
return ret, nil
6060
}
6161

@@ -132,7 +132,8 @@ func NewMsgBlockTxsOffer(point pcommon.Point) *MsgBlockTxsOffer {
132132
// elements) when the peer pushes votes directly (the prototype dialect).
133133
//
134134
// After decoding, exactly one of Votes / FullVotes is populated depending on
135-
// the per-vote CBOR array length. Encoding prefers FullVotes when present.
135+
// the known per-vote CBOR array length. Encoding prefers FullVotes when
136+
// present.
136137
type MsgVotesOffer struct {
137138
protocol.MessageBase
138139
Votes []MsgVotesOfferVote
@@ -224,9 +225,54 @@ func (m *MsgVotesOffer) UnmarshalCBOR(data []byte) error {
224225
)
225226
}
226227
m.FullVotes = append(m.FullVotes, vote)
228+
case 3:
229+
// prototype-2026w27 (the deployed musashi relay) diffuses a vote as
230+
// a 3-element array [endorser_block_hash (hash32), voter_id (uint),
231+
// signature (bytes .size 48)] — the 4-element full vote with the
232+
// leading slot field dropped. Verified against live captures from
233+
// leios-node.play.dev.cardano.org:3001 (network magic 164). Decode
234+
// the three fields; SlotNo is left zero because the wire omits it.
235+
var ebHash []byte
236+
if _, err := cbor.Decode(elems[0], &ebHash); err != nil {
237+
return fmt.Errorf(
238+
"%s: votes offer: decode vote %d endorser block hash: %w",
239+
ProtocolName, idx, err,
240+
)
241+
}
242+
if len(ebHash) != lcommon.Blake2b256Size {
243+
return fmt.Errorf(
244+
"%s: votes offer: vote %d endorser block hash is %d bytes, expected %d",
245+
ProtocolName, idx, len(ebHash), lcommon.Blake2b256Size,
246+
)
247+
}
248+
var voterId uint64
249+
if _, err := cbor.Decode(elems[1], &voterId); err != nil {
250+
return fmt.Errorf(
251+
"%s: votes offer: decode vote %d voter id: %w",
252+
ProtocolName, idx, err,
253+
)
254+
}
255+
var sig []byte
256+
if _, err := cbor.Decode(elems[2], &sig); err != nil {
257+
return fmt.Errorf(
258+
"%s: votes offer: decode vote %d signature: %w",
259+
ProtocolName, idx, err,
260+
)
261+
}
262+
if len(sig) != lcommon.LeiosBlsSignatureSize {
263+
return fmt.Errorf(
264+
"%s: votes offer: vote %d signature is %d bytes, expected %d",
265+
ProtocolName, idx, len(sig), lcommon.LeiosBlsSignatureSize,
266+
)
267+
}
268+
m.FullVotes = append(m.FullVotes, lcommon.LeiosVote{
269+
EndorserBlockHash: lcommon.NewBlake2b256(ebHash),
270+
VoterId: voterId,
271+
VoteSignature: sig,
272+
})
227273
default:
228274
return fmt.Errorf(
229-
"%s: votes offer: vote %d has unexpected element count %d",
275+
"%s: votes offer: vote %d unexpected element count %d",
230276
ProtocolName,
231277
idx,
232278
len(elems),

protocol/leiosnotify/messages_test.go

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
package leiosnotify
1616

1717
import (
18+
"bytes"
1819
"reflect"
1920
"testing"
2021

2122
"github.com/blinklabs-io/gouroboros/cbor"
23+
lcommon "github.com/blinklabs-io/gouroboros/ledger/common"
2224
"github.com/blinklabs-io/gouroboros/protocol"
2325
pcommon "github.com/blinklabs-io/gouroboros/protocol/common"
2426
"github.com/stretchr/testify/assert"
@@ -198,13 +200,11 @@ func TestMsgDone(t *testing.T) {
198200
}
199201

200202
func TestNewMsgFromCborUnknownType(t *testing.T) {
201-
// Test with unknown message type - the NewMsgFromCbor function tries to decode
202-
// into a nil pointer, which results in an error
203203
data := []byte{0x80} // empty array
204204
msg, err := NewMsgFromCbor(999, data)
205-
// When the message type is unknown, ret is nil, and cbor.Decode(data, nil) returns an error
206-
assert.Error(t, err)
207-
assert.Nil(t, msg)
205+
require.Error(t, err)
206+
assert.Contains(t, err.Error(), "unknown message type 999")
207+
require.Nil(t, msg)
208208
}
209209

210210
func TestMsgVotesOfferEmpty(t *testing.T) {
@@ -235,3 +235,57 @@ func TestMsgBlockAnnouncementEmpty(t *testing.T) {
235235
// so we just check it's not nil
236236
assert.NotNil(t, decodedMsg)
237237
}
238+
239+
// TestMsgVotesOfferThreeElementVote verifies decoding the prototype-2026w27
240+
// deployed vote shape: a 3-element array [endorser_block_hash (hash32),
241+
// voter_id (uint), signature (bytes .size 48)] — the full LeiosVote with the
242+
// leading slot dropped. Shape confirmed from live musashi captures.
243+
func TestMsgVotesOfferThreeElementVote(t *testing.T) {
244+
ebHash := bytes.Repeat([]byte{0xAB}, lcommon.Blake2b256Size)
245+
sig := bytes.Repeat([]byte{0xCD}, lcommon.LeiosBlsSignatureSize)
246+
voteCbor, err := cbor.Encode([]any{ebHash, uint64(7), sig})
247+
require.NoError(t, err)
248+
msgCbor, err := cbor.Encode([]any{
249+
uint(MessageTypeVotesOffer),
250+
[]cbor.RawMessage{cbor.RawMessage(voteCbor)},
251+
})
252+
require.NoError(t, err)
253+
254+
var m MsgVotesOffer
255+
require.NoError(t, m.UnmarshalCBOR(msgCbor))
256+
require.Len(t, m.FullVotes, 1)
257+
require.Empty(t, m.Votes)
258+
v := m.FullVotes[0]
259+
assert.Equal(t, uint64(0), v.SlotNo, "3-element vote omits slot")
260+
assert.Equal(t, ebHash, v.EndorserBlockHash.Bytes())
261+
assert.Equal(t, uint64(7), v.VoterId)
262+
assert.Equal(t, sig, v.VoteSignature)
263+
264+
// A 3-element vote with a wrong-length signature is rejected.
265+
badCbor, err := cbor.Encode([]any{ebHash, uint64(7), []byte{0x01, 0x02}})
266+
require.NoError(t, err)
267+
badMsg, err := cbor.Encode([]any{
268+
uint(MessageTypeVotesOffer),
269+
[]cbor.RawMessage{cbor.RawMessage(badCbor)},
270+
})
271+
require.NoError(t, err)
272+
var m2 MsgVotesOffer
273+
require.ErrorContains(t, m2.UnmarshalCBOR(badMsg), "signature is 2 bytes")
274+
}
275+
276+
func TestMsgVotesOfferUnknownVoteShape(t *testing.T) {
277+
voteCbor, err := cbor.Encode([]any{uint64(100)})
278+
require.NoError(t, err)
279+
msgCbor, err := cbor.Encode([]any{
280+
uint(MessageTypeVotesOffer),
281+
[]cbor.RawMessage{cbor.RawMessage(voteCbor)},
282+
})
283+
require.NoError(t, err)
284+
285+
var m MsgVotesOffer
286+
require.ErrorContains(
287+
t,
288+
m.UnmarshalCBOR(msgCbor),
289+
"votes offer: vote 0 unexpected element count 1",
290+
)
291+
}

0 commit comments

Comments
 (0)