Skip to content

Commit 3d94b8d

Browse files
committed
fix(leios): support prototype vote wire format
Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
1 parent b170eaf commit 3d94b8d

4 files changed

Lines changed: 99 additions & 31 deletions

File tree

ledger/common/leios.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,36 @@ type LeiosVote struct {
3737
VoteSignature []byte
3838
}
3939

40+
// LeiosPrototypeVote is the current prototype vote wire shape. Its signed
41+
// message is AnnouncingRbHash. Slot and endorser-block identity are resolved
42+
// from the announcing ranking block by the consumer.
43+
type LeiosPrototypeVote struct {
44+
cbor.StructAsArray
45+
AnnouncingRbHash Blake2b256
46+
VoterId uint64
47+
VoteSignature []byte
48+
}
49+
50+
func (v *LeiosPrototypeVote) UnmarshalCBOR(cborData []byte) error {
51+
type tLeiosPrototypeVote LeiosPrototypeVote
52+
var tmp tLeiosPrototypeVote
53+
if _, err := cbor.Decode(cborData, &tmp); err != nil {
54+
return err
55+
}
56+
if err := LeiosPrototypeVote(tmp).Validate(); err != nil {
57+
return err
58+
}
59+
*v = LeiosPrototypeVote(tmp)
60+
return nil
61+
}
62+
63+
func (v LeiosPrototypeVote) Validate() error {
64+
return ValidateLeiosSignature(
65+
"LeiosPrototypeVote: VoteSignature",
66+
v.VoteSignature,
67+
)
68+
}
69+
4070
func (v LeiosVote) MarshalCBOR() ([]byte, error) {
4171
if raw := v.Cbor(); len(raw) > 0 {
4272
return raw, nil

ledger/common/leios_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,20 @@ func TestLeiosVoteRejectsInvalidSignatureLength(t *testing.T) {
102102
assert.Contains(t, err.Error(), "VoteSignature")
103103
}
104104

105+
func TestLeiosPrototypeVoteRejectsInvalidSignatureLength(t *testing.T) {
106+
encoded, err := cbor.Encode([]any{
107+
NewBlake2b256([]byte{0x01}),
108+
uint64(42),
109+
[]byte{0xaa},
110+
})
111+
require.NoError(t, err)
112+
113+
var decoded LeiosPrototypeVote
114+
_, err = cbor.Decode(encoded, &decoded)
115+
require.Error(t, err)
116+
assert.Contains(t, err.Error(), "VoteSignature")
117+
}
118+
105119
func TestLeiosEbCertificateCborRoundTrip(t *testing.T) {
106120
cert := LeiosEbCertificate{
107121
SlotNo: 99,

protocol/leiosnotify/messages.go

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -128,18 +128,23 @@ func NewMsgBlockTxsOffer(point pcommon.Point) *MsgBlockTxsOffer {
128128
//
129129
// - Votes holds vote IDs ([slot, voter_id], 2 elements) when the peer offers
130130
// IDs to be fetched (dingo's offer-and-fetch design).
131-
// - FullVotes holds complete votes ([slot, eb_hash, voter_id, signature], 4
132-
// elements) when the peer pushes votes directly (the prototype dialect).
131+
// - FullVotes holds legacy complete votes
132+
// ([slot, eb_hash, voter_id, signature], 4 elements).
133+
// - PrototypeVotes holds current prototype votes
134+
// ([announcing_rb_hash, voter_id, signature], 3 elements).
133135
//
134-
// After decoding, exactly one of Votes / FullVotes is populated depending on
135-
// the known per-vote CBOR array length. Encoding prefers FullVotes when
136-
// present.
136+
// After decoding, exactly one of Votes, FullVotes, or PrototypeVotes is
137+
// populated depending on the known per-vote CBOR array length. Encoding
138+
// prefers PrototypeVotes, then FullVotes, when present.
137139
type MsgVotesOffer struct {
138140
protocol.MessageBase
139-
Votes []MsgVotesOfferVote
140-
FullVotes []lcommon.LeiosVote
141+
Votes []MsgVotesOfferVote
142+
FullVotes []lcommon.LeiosVote
143+
PrototypeVotes []lcommon.LeiosPrototypeVote
141144
}
142145

146+
type PrototypeVote = lcommon.LeiosPrototypeVote
147+
143148
type MsgVotesOfferVote = lcommon.LeiosVoteId
144149

145150
func NewMsgVotesOffer(votes []MsgVotesOfferVote) *MsgVotesOffer {
@@ -164,10 +169,21 @@ func NewMsgVotesOfferFull(votes []lcommon.LeiosVote) *MsgVotesOffer {
164169
return m
165170
}
166171

172+
// NewMsgVotesOfferPrototype builds a current-prototype pushed vote offer.
173+
func NewMsgVotesOfferPrototype(votes []PrototypeVote) *MsgVotesOffer {
174+
return &MsgVotesOffer{
175+
MessageBase: protocol.MessageBase{MessageType: MessageTypeVotesOffer},
176+
PrototypeVotes: votes,
177+
}
178+
}
179+
167180
func (m *MsgVotesOffer) MarshalCBOR() ([]byte, error) {
168181
if raw := m.Cbor(); len(raw) > 0 {
169182
return raw, nil
170183
}
184+
if len(m.PrototypeVotes) > 0 {
185+
return cbor.Encode([]any{m.MessageType, m.PrototypeVotes})
186+
}
171187
if len(m.FullVotes) > 0 {
172188
return cbor.Encode([]any{m.MessageType, m.FullVotes})
173189
}
@@ -189,6 +205,7 @@ func (m *MsgVotesOffer) UnmarshalCBOR(data []byte) error {
189205
m.MessageType = envelope.MessageType
190206
m.Votes = nil
191207
m.FullVotes = nil
208+
m.PrototypeVotes = nil
192209
for idx, voteRaw := range envelope.Votes {
193210
// Peek at the element count to distinguish a vote ID
194211
// ([slot, voter_id]) from a full vote
@@ -226,23 +243,19 @@ func (m *MsgVotesOffer) UnmarshalCBOR(data []byte) error {
226243
}
227244
m.FullVotes = append(m.FullVotes, vote)
228245
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 {
246+
// The current prototype signs and diffuses the announcing ranking
247+
// block hash, not the endorser-block point.
248+
var rbHash []byte
249+
if _, err := cbor.Decode(elems[0], &rbHash); err != nil {
237250
return fmt.Errorf(
238-
"%s: votes offer: decode vote %d endorser block hash: %w",
251+
"%s: votes offer: decode vote %d announcing RB hash: %w",
239252
ProtocolName, idx, err,
240253
)
241254
}
242-
if len(ebHash) != lcommon.Blake2b256Size {
255+
if len(rbHash) != lcommon.Blake2b256Size {
243256
return fmt.Errorf(
244-
"%s: votes offer: vote %d endorser block hash is %d bytes, expected %d",
245-
ProtocolName, idx, len(ebHash), lcommon.Blake2b256Size,
257+
"%s: votes offer: vote %d announcing RB hash is %d bytes, expected %d",
258+
ProtocolName, idx, len(rbHash), lcommon.Blake2b256Size,
246259
)
247260
}
248261
var voterId uint64
@@ -265,10 +278,10 @@ func (m *MsgVotesOffer) UnmarshalCBOR(data []byte) error {
265278
ProtocolName, idx, len(sig), lcommon.LeiosBlsSignatureSize,
266279
)
267280
}
268-
m.FullVotes = append(m.FullVotes, lcommon.LeiosVote{
269-
EndorserBlockHash: lcommon.NewBlake2b256(ebHash),
270-
VoterId: voterId,
271-
VoteSignature: sig,
281+
m.PrototypeVotes = append(m.PrototypeVotes, PrototypeVote{
282+
AnnouncingRbHash: lcommon.NewBlake2b256(rbHash),
283+
VoterId: voterId,
284+
VoteSignature: sig,
272285
})
273286
default:
274287
return fmt.Errorf(

protocol/leiosnotify/messages_test.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,8 @@ func TestMsgBlockAnnouncementEmpty(t *testing.T) {
236236
assert.NotNil(t, decodedMsg)
237237
}
238238

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.
239+
// TestMsgVotesOfferThreeElementVote verifies the current prototype vote shape:
240+
// [announcing_rb_hash, voter_id, signature].
243241
func TestMsgVotesOfferThreeElementVote(t *testing.T) {
244242
ebHash := bytes.Repeat([]byte{0xAB}, lcommon.Blake2b256Size)
245243
sig := bytes.Repeat([]byte{0xCD}, lcommon.LeiosBlsSignatureSize)
@@ -253,11 +251,11 @@ func TestMsgVotesOfferThreeElementVote(t *testing.T) {
253251

254252
var m MsgVotesOffer
255253
require.NoError(t, m.UnmarshalCBOR(msgCbor))
256-
require.Len(t, m.FullVotes, 1)
254+
require.Len(t, m.PrototypeVotes, 1)
257255
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())
256+
require.Empty(t, m.FullVotes)
257+
v := m.PrototypeVotes[0]
258+
assert.Equal(t, ebHash, v.AnnouncingRbHash.Bytes())
261259
assert.Equal(t, uint64(7), v.VoterId)
262260
assert.Equal(t, sig, v.VoteSignature)
263261

@@ -273,6 +271,19 @@ func TestMsgVotesOfferThreeElementVote(t *testing.T) {
273271
require.ErrorContains(t, m2.UnmarshalCBOR(badMsg), "signature is 2 bytes")
274272
}
275273

274+
func TestMsgVotesOfferPrototypeRoundTrip(t *testing.T) {
275+
vote := PrototypeVote{
276+
AnnouncingRbHash: lcommon.NewBlake2b256(bytes.Repeat([]byte{0xAB}, 32)),
277+
VoterId: 7,
278+
VoteSignature: bytes.Repeat([]byte{0xCD}, lcommon.LeiosBlsSignatureSize),
279+
}
280+
raw, err := NewMsgVotesOfferPrototype([]PrototypeVote{vote}).MarshalCBOR()
281+
require.NoError(t, err)
282+
var decoded MsgVotesOffer
283+
require.NoError(t, decoded.UnmarshalCBOR(raw))
284+
require.Equal(t, []PrototypeVote{vote}, decoded.PrototypeVotes)
285+
}
286+
276287
func TestMsgVotesOfferUnknownVoteShape(t *testing.T) {
277288
voteCbor, err := cbor.Encode([]any{uint64(100)})
278289
require.NoError(t, err)

0 commit comments

Comments
 (0)