Skip to content

Commit 809484c

Browse files
committed
fix(ledger): match Leios/Musashi CDDL exactly at a24a2d69b
Signed-off-by: Chris Gianelloni <wolf31o2@blinklabs.io>
1 parent 9adf670 commit 809484c

11 files changed

Lines changed: 1596 additions & 407 deletions

ledger/common/common.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,168 @@ func extractByronOutputOffsets(
11791179
}
11801180
}
11811181

1182+
// isDijkstraBlock checks whether a decoded block array represents a Dijkstra
1183+
// (prototype-2026w27) block. A Dijkstra block is a 2-element array
1184+
// [header, block_body] where block_body is a 4-element array
1185+
// [invalid_transactions/nil, transactions, leios_certificate/nil,
1186+
// peras_certificate/nil] and each transaction is a 3-element array
1187+
// [transaction_body, transaction_witness_set, auxiliary_data/nil].
1188+
//
1189+
// The 2-element top level distinguishes Dijkstra from Byron (3-element) and
1190+
// Shelley+ (4+ element) blocks. The nested shape check guards against
1191+
// misclassifying an unrelated 2-element value (e.g. a Byron epoch-boundary
1192+
// block, whose second element is a bytestring array, not a 4-element array).
1193+
func isDijkstraBlock(blockArray []cbor.RawMessage) bool {
1194+
if len(blockArray) != 2 {
1195+
return false
1196+
}
1197+
var bodyParts []cbor.RawMessage
1198+
if _, err := cbor.Decode([]byte(blockArray[1]), &bodyParts); err != nil {
1199+
return false
1200+
}
1201+
if len(bodyParts) != 4 {
1202+
return false
1203+
}
1204+
// bodyParts[1] is the transactions array.
1205+
var txs []cbor.RawMessage
1206+
if _, err := cbor.Decode([]byte(bodyParts[1]), &txs); err != nil {
1207+
return false
1208+
}
1209+
// If there are transactions, verify the first one is a 3-element array.
1210+
if len(txs) > 0 {
1211+
var tx []cbor.RawMessage
1212+
if _, err := cbor.Decode([]byte(txs[0]), &tx); err != nil {
1213+
return false
1214+
}
1215+
if len(tx) != 3 {
1216+
return false
1217+
}
1218+
}
1219+
return true
1220+
}
1221+
1222+
// extractDijkstraTransactionOffsets extracts transaction offsets from a
1223+
// Dijkstra (prototype-2026w27) block. The block is [header, block_body] with
1224+
// block_body = [invalid_transactions/nil, transactions, leios_certificate/nil,
1225+
// peras_certificate/nil] and each transaction a complete
1226+
// [transaction_body, transaction_witness_set, auxiliary_data/nil] array (not
1227+
// the pre-Dijkstra parallel body/witness/metadata segments).
1228+
func extractDijkstraTransactionOffsets(
1229+
cborData []byte,
1230+
blockArray []cbor.RawMessage,
1231+
) (*BlockTransactionOffsets, error) {
1232+
topArrayHeader := cborArrayHeaderSize(len(blockArray))
1233+
1234+
// blockArray[0] = header, blockArray[1] = block_body
1235+
blockBodyOffset := topArrayHeader + uint32(len(blockArray[0])) // #nosec G115 -- Cardano block segments are <<4GiB
1236+
1237+
var bodyParts []cbor.RawMessage
1238+
if _, err := cbor.Decode([]byte(blockArray[1]), &bodyParts); err != nil {
1239+
return nil, fmt.Errorf("failed to decode Dijkstra block body: %w", err)
1240+
}
1241+
if len(bodyParts) != 4 {
1242+
return nil, fmt.Errorf(
1243+
"dijkstra block body has %d elements, expected 4",
1244+
len(bodyParts),
1245+
)
1246+
}
1247+
1248+
// bodyParts[1] = transactions ([* transaction])
1249+
var txs []cbor.RawMessage
1250+
if _, err := cbor.Decode([]byte(bodyParts[1]), &txs); err != nil {
1251+
return nil, fmt.Errorf("failed to decode Dijkstra transactions: %w", err)
1252+
}
1253+
if len(txs) == 0 {
1254+
return &BlockTransactionOffsets{Transactions: []TransactionLocation{}}, nil
1255+
}
1256+
1257+
// Absolute offset of the transactions array: skip the block_body array
1258+
// header and the invalid_transactions element (bodyParts[0]).
1259+
var bodyArrayHeader uint32
1260+
if int(blockBodyOffset) < len(cborData) && cborData[blockBodyOffset] == 0x9f {
1261+
bodyArrayHeader = 1 // indefinite-length array
1262+
} else {
1263+
bodyArrayHeader = cborArrayHeaderSize(len(bodyParts))
1264+
}
1265+
txsArrayOffset := blockBodyOffset + bodyArrayHeader + uint32(len(bodyParts[0])) // #nosec G115 -- Cardano block segments are <<4GiB
1266+
1267+
var txsArrayHeader uint32
1268+
if int(txsArrayOffset) < len(cborData) && cborData[txsArrayOffset] == 0x9f {
1269+
txsArrayHeader = 1 // indefinite-length array
1270+
} else {
1271+
txsArrayHeader = cborArrayHeaderSize(len(txs))
1272+
}
1273+
1274+
result := &BlockTransactionOffsets{
1275+
Transactions: make([]TransactionLocation, len(txs)),
1276+
}
1277+
1278+
// Walk each transaction [transaction_body, transaction_witness_set, aux/nil].
1279+
txPos := txsArrayOffset + txsArrayHeader
1280+
for i, rawTx := range txs {
1281+
var txParts []cbor.RawMessage
1282+
if _, err := cbor.Decode([]byte(rawTx), &txParts); err != nil {
1283+
return nil, fmt.Errorf(
1284+
"failed to decode Dijkstra transaction %d: %w", i, err,
1285+
)
1286+
}
1287+
if len(txParts) != 3 {
1288+
return nil, fmt.Errorf(
1289+
"dijkstra transaction %d has %d elements, expected 3",
1290+
i, len(txParts),
1291+
)
1292+
}
1293+
1294+
var txArrayHeader uint32
1295+
if int(txPos) < len(cborData) && cborData[txPos] == 0x9f {
1296+
txArrayHeader = 1 // indefinite-length array
1297+
} else {
1298+
txArrayHeader = cborArrayHeaderSize(len(txParts))
1299+
}
1300+
1301+
bodyStart := txPos + txArrayHeader
1302+
bodyLen := uint32(len(txParts[0])) // #nosec G115 -- Cardano block segments are <<4GiB
1303+
witnessStart := bodyStart + bodyLen
1304+
witnessLen := uint32(len(txParts[1])) // #nosec G115 -- Cardano block segments are <<4GiB
1305+
auxStart := witnessStart + witnessLen
1306+
auxLen := uint32(len(txParts[2])) // #nosec G115 -- Cardano block segments are <<4GiB
1307+
1308+
result.Transactions[i].Body = ByteRange{
1309+
Offset: bodyStart,
1310+
Length: bodyLen,
1311+
}
1312+
result.Transactions[i].Witness = ByteRange{
1313+
Offset: witnessStart,
1314+
Length: witnessLen,
1315+
}
1316+
// auxiliary_data is CBOR null (0xf6) when absent; only record real
1317+
// metadata so the zero ByteRange keeps its "no metadata" meaning.
1318+
if !(len(txParts[2]) == 1 && txParts[2][0] == 0xf6) {
1319+
result.Transactions[i].Metadata = ByteRange{
1320+
Offset: auxStart,
1321+
Length: auxLen,
1322+
}
1323+
}
1324+
1325+
// Populate output and witness-component offsets, matching the Shelley+
1326+
// path so downstream DOFF indexing works identically.
1327+
extractOutputOffsets(
1328+
[]byte(txParts[0]),
1329+
bodyStart,
1330+
&result.Transactions[i],
1331+
)
1332+
extractWitnessComponentOffsets(
1333+
[]byte(txParts[1]),
1334+
witnessStart,
1335+
&result.Transactions[i],
1336+
)
1337+
1338+
txPos += uint32(len(rawTx)) // #nosec G115 -- Cardano block segments are <<4GiB
1339+
}
1340+
1341+
return result, nil
1342+
}
1343+
11821344
// ExtractTransactionOffsets extracts byte offsets for all transactions in a block.
11831345
// This enables efficient CBOR extraction from block data without full deserialization.
11841346
//
@@ -1193,6 +1355,15 @@ func ExtractTransactionOffsets(cborData []byte) (*BlockTransactionOffsets, error
11931355
if _, err := cbor.Decode(cborData, &blockArray); err != nil {
11941356
return nil, fmt.Errorf("failed to decode block array: %w", err)
11951357
}
1358+
1359+
// Detect Dijkstra (prototype-2026w27) blocks. Unlike pre-Dijkstra eras,
1360+
// these are a 2-element array [header, block_body] where transactions live
1361+
// inline inside block_body rather than as parallel top-level segments, so
1362+
// they must be recognized before the generic short-block early return.
1363+
if isDijkstraBlock(blockArray) {
1364+
return extractDijkstraTransactionOffsets(cborData, blockArray)
1365+
}
1366+
11961367
if len(blockArray) < 3 {
11971368
// Block doesn't have separated components (e.g., Byron EBB)
11981369
// Return empty slice instead of nil to prevent nil pointer dereference

ledger/common/streaming_decode_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package common_test
1717
import (
1818
"testing"
1919

20+
"github.com/blinklabs-io/gouroboros/cbor"
2021
"github.com/blinklabs-io/gouroboros/ledger/common"
2122
"github.com/stretchr/testify/assert"
2223
"github.com/stretchr/testify/require"
@@ -143,3 +144,85 @@ func TestExtractWitnessCbor(t *testing.T) {
143144
require.NoError(t, err)
144145
assert.Equal(t, []byte{0x03, 0x04, 0x05}, witness)
145146
}
147+
148+
// TestExtractTransactionOffsetsDijkstra verifies that ExtractTransactionOffsets
149+
// recognizes a Dijkstra (prototype-2026w27) 2-element block
150+
// [header, block_body] and extracts one offset entry per inline transaction,
151+
// with each extracted body/witness/metadata slice round-tripping to the exact
152+
// CBOR of the corresponding transaction element.
153+
func TestExtractTransactionOffsetsDijkstra(t *testing.T) {
154+
// tx0: no metadata; tx1: carries auxiliary_data (metadata) and an output.
155+
tx0 := []any{
156+
map[uint64]any{2: uint64(10)}, // body: {fee: 10}
157+
map[uint64]any{}, // empty witness set
158+
nil, // no auxiliary_data
159+
}
160+
tx1 := []any{
161+
map[uint64]any{ // body: {outputs: [out], fee: 20}
162+
1: []any{[]any{[]byte{0xde, 0xad}, uint64(5)}},
163+
2: uint64(20),
164+
},
165+
map[uint64]any{0: []any{}}, // witness set with empty vkey witness list
166+
map[uint64]any{uint64(674): "meta"}, // auxiliary_data (metadata)
167+
}
168+
// A realistic 2-element header placeholder ([body, signature]); its contents
169+
// are irrelevant to offset extraction, only its byte length matters.
170+
header := []any{[]byte{0x01, 0x02, 0x03}, []byte{0x04}}
171+
blockBody := []any{nil, []any{tx0, tx1}, nil, nil}
172+
blockCbor, err := cbor.Encode([]any{header, blockBody})
173+
require.NoError(t, err)
174+
175+
offsets, err := common.ExtractTransactionOffsets(blockCbor)
176+
require.NoError(t, err)
177+
require.Len(t, offsets.Transactions, 2)
178+
179+
// Independently decode to obtain the authoritative sub-element bytes.
180+
var top []cbor.RawMessage
181+
_, err = cbor.Decode(blockCbor, &top)
182+
require.NoError(t, err)
183+
require.Len(t, top, 2)
184+
var bodyParts []cbor.RawMessage
185+
_, err = cbor.Decode([]byte(top[1]), &bodyParts)
186+
require.NoError(t, err)
187+
require.Len(t, bodyParts, 4)
188+
var txs []cbor.RawMessage
189+
_, err = cbor.Decode([]byte(bodyParts[1]), &txs)
190+
require.NoError(t, err)
191+
require.Len(t, txs, 2)
192+
193+
for i, rawTx := range txs {
194+
var txParts []cbor.RawMessage
195+
_, err = cbor.Decode([]byte(rawTx), &txParts)
196+
require.NoError(t, err)
197+
require.Len(t, txParts, 3)
198+
loc := offsets.Transactions[i]
199+
200+
gotBody := blockCbor[loc.Body.Offset : loc.Body.Offset+loc.Body.Length]
201+
assert.Equal(t, []byte(txParts[0]), gotBody, "tx %d body bytes", i)
202+
203+
gotWit := blockCbor[loc.Witness.Offset : loc.Witness.Offset+loc.Witness.Length]
204+
assert.Equal(t, []byte(txParts[1]), gotWit, "tx %d witness bytes", i)
205+
}
206+
207+
// tx0 has no auxiliary_data -> zero Metadata range.
208+
assert.Zero(t, offsets.Transactions[0].Metadata.Length)
209+
// tx1 carries metadata; the recorded range must round-trip to its aux bytes.
210+
var tx1Parts []cbor.RawMessage
211+
_, err = cbor.Decode([]byte(txs[1]), &tx1Parts)
212+
require.NoError(t, err)
213+
m := offsets.Transactions[1].Metadata
214+
require.NotZero(t, m.Length)
215+
assert.Equal(t, []byte(tx1Parts[2]), blockCbor[m.Offset:m.Offset+m.Length])
216+
217+
// tx1's single output must also round-trip.
218+
require.Len(t, offsets.Transactions[1].Outputs, 1)
219+
var body1Parts map[uint64]cbor.RawMessage
220+
_, err = cbor.Decode([]byte(tx1Parts[0]), &body1Parts)
221+
require.NoError(t, err)
222+
var outputs []cbor.RawMessage
223+
_, err = cbor.Decode([]byte(body1Parts[1]), &outputs)
224+
require.NoError(t, err)
225+
require.Len(t, outputs, 1)
226+
o := offsets.Transactions[1].Outputs[0]
227+
assert.Equal(t, []byte(outputs[0]), blockCbor[o.Offset:o.Offset+o.Length])
228+
}

ledger/dijkstra.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ type (
2525
DijkstraBlockBody = dijkstra.DijkstraBlockBody
2626
DijkstraBlockHeader = dijkstra.DijkstraBlockHeader
2727
DijkstraLeiosCertificate = dijkstra.DijkstraLeiosCertificate
28-
DijkstraPerasCertificate = dijkstra.DijkstraPerasCertificate
2928
DijkstraTransaction = dijkstra.DijkstraTransaction
3029
DijkstraTransactionBody = dijkstra.DijkstraTransactionBody
3130
DijkstraTransactionOutput = dijkstra.DijkstraTransactionOutput

0 commit comments

Comments
 (0)