@@ -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
0 commit comments