Skip to content

Commit 9adf670

Browse files
authored
fix(leios): block encoding for prototype-2026w27 (#1857)
Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
1 parent 2fb769f commit 9adf670

3 files changed

Lines changed: 237 additions & 45 deletions

File tree

ledger/dijkstra/dijkstra.go

Lines changed: 88 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -192,30 +192,67 @@ func (b *DijkstraBlock) CalculatedBlockBodyHash() common.Blake2b256 {
192192
return b.BlockBody.Hash()
193193
}
194194

195-
// DijkstraLeiosCertificate matches the generated Dijkstra CDDL in
196-
// IntersectMBO/cardano-ledger at c47305fcf47bd77437b837d0dfb9cb4181bfbc77:
195+
// DijkstraLeiosCertificate is the in-body Leios certificate added in the
196+
// ouroboros-leios prototype-2026w27 release (IntersectMBO/cardano-ledger #5872):
197197
//
198-
// block = [..., leios_cert : leios_cert / nil, peras_cert : peras_cert / nil]
199-
// leios_cert = []
200-
// peras_cert = []
198+
// block_body = [..., leios_certificate : leios_certificate / nil, peras_certificate : peras_certificate / nil]
199+
// leios_certificate = [ signers : bytes ; committee signer bitfield
200+
// , aggregated_signature : leios_signature ]
201+
// leios_signature = bytes .size 48
201202
//
202-
// The Leios and Peras slots are always present and nullable. When non-null,
203-
// the current Dijkstra CDDL placeholder is an empty CBOR list, not the CIP-0164
204-
// stake-committee EB certificate payload.
203+
// It certifies the endorser block announced by an earlier ranking block; the
204+
// ranking block whose header sets leios_certified carries it in its body. Per
205+
// CIP-0164 a block that carries a Leios certificate carries no Dijkstra-era
206+
// transactions.
205207
type DijkstraLeiosCertificate struct {
206208
cbor.DecodeStoreCbor
209+
Signers []byte
210+
AggregatedSignature []byte
207211
}
208212

209213
func (c *DijkstraLeiosCertificate) UnmarshalCBOR(cborData []byte) error {
210-
return decodeDijkstraEmptyCertificate(
211-
cborData,
212-
"Dijkstra Leios certificate",
213-
&c.DecodeStoreCbor,
214-
)
214+
var items []cbor.RawMessage
215+
if _, err := cbor.Decode(cborData, &items); err != nil {
216+
return fmt.Errorf("decode Dijkstra Leios certificate: %w", err)
217+
}
218+
if len(items) != 2 {
219+
return fmt.Errorf(
220+
"dijkstra Leios certificate must have 2 fields, got %d",
221+
len(items),
222+
)
223+
}
224+
var signers []byte
225+
if _, err := cbor.Decode(items[0], &signers); err != nil {
226+
return fmt.Errorf(
227+
"decode Dijkstra Leios certificate signers: %w",
228+
err,
229+
)
230+
}
231+
var aggSig []byte
232+
if _, err := cbor.Decode(items[1], &aggSig); err != nil {
233+
return fmt.Errorf(
234+
"decode Dijkstra Leios certificate aggregated signature: %w",
235+
err,
236+
)
237+
}
238+
if len(aggSig) != common.LeiosBlsSignatureSize {
239+
return fmt.Errorf(
240+
"invalid Dijkstra Leios certificate aggregated signature length: expected %d, got %d",
241+
common.LeiosBlsSignatureSize,
242+
len(aggSig),
243+
)
244+
}
245+
c.Signers = signers
246+
c.AggregatedSignature = aggSig
247+
c.SetCbor(cborData)
248+
return nil
215249
}
216250

217251
func (c DijkstraLeiosCertificate) MarshalCBOR() ([]byte, error) {
218-
return marshalDijkstraEmptyCertificate(c.DecodeStoreCbor)
252+
if raw := c.Cbor(); len(raw) > 0 {
253+
return raw, nil
254+
}
255+
return cbor.Encode([]any{c.Signers, c.AggregatedSignature})
219256
}
220257

221258
type DijkstraPerasCertificate struct {
@@ -622,12 +659,14 @@ const babbageHeaderBodyFieldCount = 10
622659
type DijkstraBlockHeader struct {
623660
babbage.BabbageBlockHeader
624661
// LeiosHeaderExtension holds the Dijkstra/Leios block-header fields that
625-
// follow Babbage's protocol_version field. The leios-prototype testnet
626-
// began emitting an extra trailing element (a [hash, uint] pair) once the
627-
// Leios header extension activated mid-Dijkstra; earlier Dijkstra blocks
628-
// carry the plain 10-field Babbage header body, for which this is nil. The
629-
// elements are retained verbatim so the header round-trips and hashes
630-
// identically to the bytes received on the wire.
662+
// follow Babbage's protocol_version field. As of the ouroboros-leios
663+
// prototype-2026w27 release (IntersectMBO/cardano-ledger #5889) these are
664+
// two fields: leios_certified (bool) and leios_announcement
665+
// ([announced_eb : hash32, announced_eb_size : uint .size 4] / nil). Earlier
666+
// Dijkstra blocks carry the plain 10-field Babbage header body, for which
667+
// this is nil. The elements are retained verbatim so the header round-trips
668+
// and hashes identically to the bytes received on the wire; use
669+
// LeiosCertified and LeiosAnnouncement for typed access.
631670
LeiosHeaderExtension []cbor.RawMessage
632671
}
633672

@@ -709,22 +748,42 @@ func (h *DijkstraBlockHeader) Era() common.Era {
709748
return EraDijkstra
710749
}
711750

712-
// LeiosEndorserBlockRef returns the endorser block referenced by this ranking
713-
// block via its Leios header extension. The extension's first element is the
714-
// endorser-block announcement [eb_hash, eb_size] (the same shape leios-notify
715-
// uses), identifying the endorser block whose transactions this ranking block
716-
// endorses. ok is false for pre-Leios-extension Dijkstra headers (which carry
717-
// no extension) or if the extension is not the expected [hash32, uint] shape.
718-
func (h *DijkstraBlockHeader) LeiosEndorserBlockRef() (
751+
// LeiosCertified reports whether this ranking block certifies the endorser
752+
// block announced by an earlier ranking block. It is the leios_certified header
753+
// field added in the ouroboros-leios prototype-2026w27 release
754+
// (IntersectMBO/cardano-ledger #5889); when true, the block body carries the
755+
// leios_certificate. present is false for pre-Leios-extension Dijkstra headers,
756+
// which carry no extension.
757+
func (h *DijkstraBlockHeader) LeiosCertified() (certified bool, present bool) {
758+
if len(h.LeiosHeaderExtension) == 0 {
759+
return false, false
760+
}
761+
if _, err := cbor.Decode(h.LeiosHeaderExtension[0], &certified); err != nil {
762+
return false, false
763+
}
764+
return certified, true
765+
}
766+
767+
// LeiosAnnouncement returns the endorser block this ranking block announces via
768+
// its Leios header extension. It is the leios_announcement header field added in
769+
// the ouroboros-leios prototype-2026w27 release (IntersectMBO/cardano-ledger
770+
// #5889):
771+
//
772+
// leios_announcement = [announced_eb : hash32, announced_eb_size : uint .size 4]
773+
//
774+
// ok is false when the block announces no endorser block (leios_announcement is
775+
// nil), for pre-Leios-extension Dijkstra headers, or if the field is not the
776+
// expected [hash32, uint] shape.
777+
func (h *DijkstraBlockHeader) LeiosAnnouncement() (
719778
hash common.Blake2b256,
720779
size uint64,
721780
ok bool,
722781
) {
723-
if len(h.LeiosHeaderExtension) == 0 {
782+
if len(h.LeiosHeaderExtension) < 2 {
724783
return common.Blake2b256{}, 0, false
725784
}
726785
var pair []cbor.RawMessage
727-
if _, err := cbor.Decode(h.LeiosHeaderExtension[0], &pair); err != nil ||
786+
if _, err := cbor.Decode(h.LeiosHeaderExtension[1], &pair); err != nil ||
728787
len(pair) != 2 {
729788
return common.Blake2b256{}, 0, false
730789
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright 2026 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package dijkstra
16+
17+
import (
18+
"testing"
19+
20+
"github.com/blinklabs-io/gouroboros/cbor"
21+
"github.com/blinklabs-io/gouroboros/ledger/common"
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
// prototype-2026w27 (IntersectMBO/cardano-ledger #5872, #5889):
27+
//
28+
// leios_certificate = [ signers : bytes, aggregated_signature : bytes .size 48 ]
29+
// leios_announcement = [ announced_eb : hash32, announced_eb_size : uint .size 4 ]
30+
// header extension = [ leios_certified : bool, leios_announcement / nil ]
31+
32+
func TestDijkstraLeiosCertificateRoundTrip(t *testing.T) {
33+
signers := []byte{0xf0, 0x0f}
34+
sig := make([]byte, common.LeiosBlsSignatureSize)
35+
for i := range sig {
36+
sig[i] = byte(i)
37+
}
38+
certCbor, err := cbor.Encode([]any{signers, sig})
39+
require.NoError(t, err)
40+
var cert DijkstraLeiosCertificate
41+
_, err = cbor.Decode(certCbor, &cert)
42+
require.NoError(t, err)
43+
assert.Equal(t, signers, cert.Signers)
44+
assert.Equal(t, sig, cert.AggregatedSignature)
45+
out, err := cert.MarshalCBOR()
46+
require.NoError(t, err)
47+
assert.Equal(t, certCbor, out)
48+
}
49+
50+
func TestDijkstraLeiosCertificateRejectsBadSignatureLength(t *testing.T) {
51+
shortSig := make([]byte, common.LeiosBlsSignatureSize-1)
52+
certCbor, err := cbor.Encode([]any{[]byte{0x01}, shortSig})
53+
require.NoError(t, err)
54+
var cert DijkstraLeiosCertificate
55+
_, err = cbor.Decode(certCbor, &cert)
56+
require.Error(t, err)
57+
}
58+
59+
func TestDijkstraLeiosCertificateRejectsWrongFieldCount(t *testing.T) {
60+
sig := make([]byte, common.LeiosBlsSignatureSize)
61+
certCbor, err := cbor.Encode([]any{[]byte{0x01}, sig, uint64(3)})
62+
require.NoError(t, err)
63+
var cert DijkstraLeiosCertificate
64+
_, err = cbor.Decode(certCbor, &cert)
65+
require.Error(t, err)
66+
}
67+
68+
func encodeAnnouncement(t *testing.T, hash []byte, size uint64) cbor.RawMessage {
69+
t.Helper()
70+
raw, err := cbor.Encode([]any{hash, size})
71+
require.NoError(t, err)
72+
return cbor.RawMessage(raw)
73+
}
74+
75+
func encodeBool(t *testing.T, v bool) cbor.RawMessage {
76+
t.Helper()
77+
raw, err := cbor.Encode(v)
78+
require.NoError(t, err)
79+
return cbor.RawMessage(raw)
80+
}
81+
82+
func TestDijkstraLeiosHeaderExtensionAccessors(t *testing.T) {
83+
hash := make([]byte, common.Blake2b256Size)
84+
for i := range hash {
85+
hash[i] = byte(0xa0 + i%16)
86+
}
87+
const size = uint64(4096)
88+
89+
// Certified block that also announces an endorser block.
90+
h := &DijkstraBlockHeader{
91+
LeiosHeaderExtension: []cbor.RawMessage{
92+
encodeBool(t, true),
93+
encodeAnnouncement(t, hash, size),
94+
},
95+
}
96+
certified, present := h.LeiosCertified()
97+
assert.True(t, present)
98+
assert.True(t, certified)
99+
gotHash, gotSize, ok := h.LeiosAnnouncement()
100+
assert.True(t, ok)
101+
assert.Equal(t, hash, gotHash.Bytes())
102+
assert.Equal(t, size, gotSize)
103+
104+
// Not certified, no announcement (leios_announcement = nil).
105+
hNil := &DijkstraBlockHeader{
106+
LeiosHeaderExtension: []cbor.RawMessage{
107+
encodeBool(t, false),
108+
cbor.RawMessage([]byte{0xf6}), // CBOR null
109+
},
110+
}
111+
certified, present = hNil.LeiosCertified()
112+
assert.True(t, present)
113+
assert.False(t, certified)
114+
_, _, ok = hNil.LeiosAnnouncement()
115+
assert.False(t, ok)
116+
117+
// Pre-Leios-extension header: no extension at all.
118+
hLegacy := &DijkstraBlockHeader{}
119+
_, present = hLegacy.LeiosCertified()
120+
assert.False(t, present)
121+
_, _, ok = hLegacy.LeiosAnnouncement()
122+
assert.False(t, ok)
123+
}

ledger/dijkstra/dijkstra_test.go

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -184,14 +184,19 @@ func TestDijkstraBlockBodyRejectsInvalidTransactionIndexOutOfRange(t *testing.T)
184184
}
185185

186186
func TestDijkstraBlockMarshalUsesSevenItemEnvelope(t *testing.T) {
187+
sig := make([]byte, common.LeiosBlsSignatureSize)
188+
leiosCert := &DijkstraLeiosCertificate{
189+
Signers: []byte{0x01},
190+
AggregatedSignature: sig,
191+
}
187192
block := DijkstraBlock{
188193
BlockBody: DijkstraBlockBody{
189194
TransactionBodies: []DijkstraTransactionBody{
190195
{TxFee: 1},
191196
},
192197
TransactionWitnessSets: []DijkstraTransactionWitnessSet{{}},
193198
InvalidTransactions: []uint{},
194-
LeiosCertificate: &DijkstraLeiosCertificate{},
199+
LeiosCertificate: leiosCert,
195200
PerasCertificate: &DijkstraPerasCertificate{},
196201
},
197202
}
@@ -203,13 +208,17 @@ func TestDijkstraBlockMarshalUsesSevenItemEnvelope(t *testing.T) {
203208
_, err = cbor.Decode(blockCbor, &raw)
204209
require.NoError(t, err)
205210
require.Len(t, raw, 7)
206-
// When a Leios cert is present, the prototype encodes empty transaction
207-
// components and resolves the EB closure through LeiosDB.
211+
// Per CIP-0164 a block that carries a Leios certificate carries no
212+
// Dijkstra-era transactions, so the transaction components encode empty.
208213
require.Equal(t, []byte{0x80}, []byte(raw[1]))
209214
require.Equal(t, []byte{0x80}, []byte(raw[2]))
210215
require.Equal(t, []byte{0xa0}, []byte(raw[3]))
211216
require.Equal(t, []byte{0x80}, []byte(raw[4]))
212-
require.Equal(t, []byte{0x80}, []byte(raw[5]))
217+
// raw[5] is the real Leios certificate [signers, aggregated_signature].
218+
expectedCert, err := cbor.Encode([]any{[]byte{0x01}, sig})
219+
require.NoError(t, err)
220+
require.Equal(t, expectedCert, []byte(raw[5]))
221+
// raw[6] is the Peras certificate placeholder (empty list).
213222
require.Equal(t, []byte{0x80}, []byte(raw[6]))
214223
}
215224

@@ -304,23 +313,24 @@ func TestDijkstraBlockBodyHashIncludesLeiosAndPerasCertSlots(t *testing.T) {
304313
require.NotEqual(t, withoutCerts.Hash(), withCerts.Hash())
305314
}
306315

307-
func TestDijkstraLeiosCertificateMatchesCddlPlaceholder(t *testing.T) {
308-
raw, err := cbor.Encode([]any{})
316+
// prototype-2026w27 replaced the empty-list leios_cert placeholder with a real
317+
// two-field certificate (IntersectMBO/cardano-ledger #5872); the old empty-list
318+
// form is no longer valid. Full round-trip coverage is in
319+
// dijkstra_leios_w27_test.go.
320+
func TestDijkstraLeiosCertificateRejectsEmptyPlaceholder(t *testing.T) {
321+
empty, err := cbor.Encode([]any{})
309322
require.NoError(t, err)
310-
require.Equal(t, []byte{0x80}, raw)
323+
require.Equal(t, []byte{0x80}, empty)
311324

312325
var cert DijkstraLeiosCertificate
313-
require.NoError(t, cert.UnmarshalCBOR(raw))
314-
require.Equal(t, raw, cert.Cbor())
315-
316-
encoded, err := cbor.Encode(&cert)
317-
require.NoError(t, err)
318-
require.Equal(t, raw, encoded)
326+
require.Error(t, cert.UnmarshalCBOR(empty))
319327

320-
nonEmpty, err := cbor.Encode([]any{uint64(99)})
328+
sig := make([]byte, common.LeiosBlsSignatureSize)
329+
realCert, err := cbor.Encode([]any{[]byte{0x03}, sig})
321330
require.NoError(t, err)
322-
err = cert.UnmarshalCBOR(nonEmpty)
323-
require.ErrorContains(t, err, "empty list")
331+
require.NoError(t, cert.UnmarshalCBOR(realCert))
332+
require.Equal(t, []byte{0x03}, cert.Signers)
333+
require.Len(t, cert.AggregatedSignature, common.LeiosBlsSignatureSize)
324334
}
325335

326336
func TestDijkstraBlockRoundTripWithBodyHash(t *testing.T) {

0 commit comments

Comments
 (0)