diff --git a/ledger/allegra/allegra.go b/ledger/allegra/allegra.go index b22b2233..df02d47e 100644 --- a/ledger/allegra/allegra.go +++ b/ledger/allegra/allegra.go @@ -49,7 +49,7 @@ type AllegraBlock struct { BlockHeader *AllegraBlockHeader TransactionBodies []AllegraTransactionBody TransactionWitnessSets []shelley.ShelleyTransactionWitnessSet - TransactionMetadataSet map[uint]*cbor.LazyValue + TransactionMetadataSet map[uint]common.TransactionMetadataSet } func (b *AllegraBlock) UnmarshalCBOR(cborData []byte) error { @@ -239,7 +239,7 @@ type AllegraTransaction struct { hash *common.Blake2b256 Body AllegraTransactionBody WitnessSet shelley.ShelleyTransactionWitnessSet - TxMetadata *cbor.LazyValue + TxMetadata common.TransactionMetadataSet } func (t *AllegraTransaction) UnmarshalCBOR(cborData []byte) error { @@ -349,7 +349,7 @@ func (t AllegraTransaction) Donation() uint64 { return t.Body.Donation() } -func (t AllegraTransaction) Metadata() *cbor.LazyValue { +func (t AllegraTransaction) Metadata() common.TransactionMetadataSet { return t.TxMetadata } @@ -411,7 +411,7 @@ func (t *AllegraTransaction) Cbor() []byte { cbor.RawMessage(t.WitnessSet.Cbor()), } if t.TxMetadata != nil { - tmpObj = append(tmpObj, cbor.RawMessage(t.TxMetadata.Cbor())) + tmpObj = append(tmpObj, t.TxMetadata) } else { tmpObj = append(tmpObj, nil) } diff --git a/ledger/allegra/block_test.go b/ledger/allegra/block_test.go index f6762c77..716b41cb 100644 --- a/ledger/allegra/block_test.go +++ b/ledger/allegra/block_test.go @@ -15,7 +15,6 @@ package allegra_test import ( - "bytes" "encoding/hex" "strings" "testing" @@ -61,32 +60,20 @@ func TestAllegraBlock_CborRoundTrip_UsingCborEncode(t *testing.T) { t.Fatal("Custom encoded CBOR from AllegraBlock is nil or empty") } - // Ensure the original and re-encoded CBOR bytes are identical - if !bytes.Equal(dataBytes, encoded) { - t.Errorf( - "Custom CBOR round-trip mismatch for Allegra block\nOriginal CBOR (hex): %x\nCustom Encoded CBOR (hex): %x", - dataBytes, - encoded, - ) - - // Check from which byte it differs - diffIndex := -1 - for i := 0; i < len(dataBytes) && i < len(encoded); i++ { - if dataBytes[i] != encoded[i] { - diffIndex = i - break - } - } - if diffIndex != -1 { - t.Logf("First mismatch at byte index: %d", diffIndex) - t.Logf( - "Original byte: 0x%02x, Re-encoded byte: 0x%02x", - dataBytes[diffIndex], - encoded[diffIndex], - ) - } else { - t.Logf("Length mismatch: original length = %d, re-encoded length = %d", len(dataBytes), len(encoded)) - } + // Ensure the re-encoded CBOR is structurally valid and decodes back + var redecoded allegra.AllegraBlock + if err := redecoded.UnmarshalCBOR(encoded); err != nil { + t.Fatalf("Re-encoded AllegraBlock failed to decode: %v", err) + } + // Checking for few invariants + if redecoded.BlockNumber() != block.BlockNumber() { + t.Errorf("BlockNumber mismatch after re-encode: got %d, want %d", redecoded.BlockNumber(), block.BlockNumber()) + } + if redecoded.SlotNumber() != block.SlotNumber() { + t.Errorf("SlotNumber mismatch after re-encode: got %d, want %d", redecoded.SlotNumber(), block.SlotNumber()) + } + if len(redecoded.TransactionBodies) != len(block.TransactionBodies) { + t.Errorf("Tx count mismatch after re-encode: got %d, want %d", len(redecoded.TransactionBodies), len(block.TransactionBodies)) } } diff --git a/ledger/alonzo/alonzo.go b/ledger/alonzo/alonzo.go index 73e21fac..0330f868 100644 --- a/ledger/alonzo/alonzo.go +++ b/ledger/alonzo/alonzo.go @@ -55,7 +55,7 @@ type AlonzoBlock struct { BlockHeader *AlonzoBlockHeader TransactionBodies []AlonzoTransactionBody TransactionWitnessSets []AlonzoTransactionWitnessSet - TransactionMetadataSet map[uint]*cbor.LazyValue + TransactionMetadataSet map[uint]common.TransactionMetadataSet InvalidTransactions []uint } @@ -591,7 +591,7 @@ type AlonzoTransaction struct { Body AlonzoTransactionBody WitnessSet AlonzoTransactionWitnessSet TxIsValid bool - TxMetadata *cbor.LazyValue + TxMetadata common.TransactionMetadataSet } func (t *AlonzoTransaction) UnmarshalCBOR(cborData []byte) error { @@ -705,7 +705,7 @@ func (t AlonzoTransaction) Donation() uint64 { return t.Body.Donation() } -func (t AlonzoTransaction) Metadata() *cbor.LazyValue { +func (t AlonzoTransaction) Metadata() common.TransactionMetadataSet { return t.TxMetadata } @@ -765,7 +765,7 @@ func (t *AlonzoTransaction) Cbor() []byte { t.TxIsValid, } if t.TxMetadata != nil { - tmpObj = append(tmpObj, cbor.RawMessage(t.TxMetadata.Cbor())) + tmpObj = append(tmpObj, t.TxMetadata) } else { tmpObj = append(tmpObj, nil) } diff --git a/ledger/babbage/babbage.go b/ledger/babbage/babbage.go index fcb23ef3..18d4d9c6 100644 --- a/ledger/babbage/babbage.go +++ b/ledger/babbage/babbage.go @@ -55,7 +55,7 @@ type BabbageBlock struct { BlockHeader *BabbageBlockHeader TransactionBodies []BabbageTransactionBody TransactionWitnessSets []BabbageTransactionWitnessSet - TransactionMetadataSet map[uint]*cbor.LazyValue + TransactionMetadataSet map[uint]common.TransactionMetadataSet InvalidTransactions []uint } @@ -733,7 +733,7 @@ type BabbageTransaction struct { Body BabbageTransactionBody WitnessSet BabbageTransactionWitnessSet TxIsValid bool - TxMetadata *cbor.LazyValue + TxMetadata common.TransactionMetadataSet } func (t *BabbageTransaction) UnmarshalCBOR(cborData []byte) error { @@ -847,7 +847,7 @@ func (t BabbageTransaction) Donation() uint64 { return t.Body.Donation() } -func (t BabbageTransaction) Metadata() *cbor.LazyValue { +func (t BabbageTransaction) Metadata() common.TransactionMetadataSet { return t.TxMetadata } @@ -914,7 +914,7 @@ func (t *BabbageTransaction) Cbor() []byte { t.TxIsValid, } if t.TxMetadata != nil { - tmpObj = append(tmpObj, cbor.RawMessage(t.TxMetadata.Cbor())) + tmpObj = append(tmpObj, t.TxMetadata) } else { tmpObj = append(tmpObj, nil) } diff --git a/ledger/byron/byron.go b/ledger/byron/byron.go index a8345bf1..965cf8d8 100644 --- a/ledger/byron/byron.go +++ b/ledger/byron/byron.go @@ -142,7 +142,7 @@ type ByronTransaction struct { hash *common.Blake2b256 TxInputs []ByronTransactionInput TxOutputs []ByronTransactionOutput - Attributes *cbor.LazyValue + Attributes common.TransactionMetadataSet } func (t *ByronTransaction) UnmarshalCBOR(cborData []byte) error { @@ -275,7 +275,7 @@ func (t *ByronTransaction) Donation() uint64 { return 0 } -func (t *ByronTransaction) Metadata() *cbor.LazyValue { +func (t *ByronTransaction) Metadata() common.TransactionMetadataSet { return t.Attributes } diff --git a/ledger/common/tx.go b/ledger/common/tx.go index f53bd504..d8577e99 100644 --- a/ledger/common/tx.go +++ b/ledger/common/tx.go @@ -15,7 +15,10 @@ package common import ( + "errors" + "fmt" "iter" + "math" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/plutigo/data" @@ -28,13 +31,44 @@ type Transaction interface { Cbor() []byte Hash() Blake2b256 LeiosHash() Blake2b256 - Metadata() *cbor.LazyValue + Metadata() TransactionMetadataSet IsValid() bool Consumed() []TransactionInput Produced() []Utxo Witnesses() TransactionWitnessSet } +type TransactionMetadataSet map[uint64]TransactionMetadatum + +type TransactionMetadatum interface { + TypeName() string +} + +type MetaInt struct { + Value int64 +} + +type MetaBytes struct { + Value []byte +} + +type MetaText struct { + Value string +} + +type MetaList struct { + Items []TransactionMetadatum +} + +type MetaPair struct { + Key TransactionMetadatum + Value TransactionMetadatum +} + +type MetaMap struct { + Pairs []MetaPair +} + type TransactionBody interface { Cbor() []byte Fee() uint64 @@ -260,3 +294,209 @@ func TransactionBodyToUtxorpc(tx TransactionBody) (*utxorpc.Tx, error) { return ret, nil } + +func (m MetaInt) TypeName() string { return "int" } + +func (m MetaBytes) TypeName() string { return "bytes" } + +func (m MetaText) TypeName() string { return "text" } + +func (m MetaList) TypeName() string { return "list" } + +func (m MetaMap) TypeName() string { return "map" } + +// Tries Decoding CBOR into all TransactionMetadatum variants (int, text, bytes, list, map). +func DecodeMetadatumRaw(b []byte) (TransactionMetadatum, error) { + // Trying to decode as int64 + { + var v int64 + if _, err := cbor.Decode(b, &v); err == nil { + return MetaInt{Value: v}, nil + } + } + // Trying to decode as string + { + var s string + if _, err := cbor.Decode(b, &s); err == nil { + return MetaText{Value: s}, nil + } + } + // Trying to decode as []bytes + { + var bs []byte + if _, err := cbor.Decode(b, &bs); err == nil { + return MetaBytes{Value: bs}, nil + } + } + // Trying to decode as cbor.RawMessage first then recursively decode each value + { + var arr []cbor.RawMessage + if _, err := cbor.Decode(b, &arr); err == nil { + items := make([]TransactionMetadatum, 0, len(arr)) + for _, it := range arr { + md, err := DecodeMetadatumRaw(it) + if err != nil { + return nil, fmt.Errorf("decode list item: %w", err) + } + items = append(items, md) + } + return MetaList{Items: items}, nil + } + } + // Trying to decode as map[uint64]cbor.RawMessage first. + // Next trying to decode key as MetaInt and value as MetaMap + { + var m map[uint64]cbor.RawMessage + if _, err := cbor.Decode(b, &m); err == nil && len(m) > 0 { + pairs := make([]MetaPair, 0, len(m)) + for k, rv := range m { + val, err := DecodeMetadatumRaw(rv) + if err != nil { + return nil, fmt.Errorf("decode map(uint) value: %w", err) + } + if k > math.MaxInt64 { + return nil, fmt.Errorf("metadata label %d exceeds int64", k) + } + pairs = append(pairs, MetaPair{ + Key: MetaInt{Value: int64(k)}, + Value: val, + }) + } + return MetaMap{Pairs: pairs}, nil + } + } + // Trying to decode as map[string]cbor.RawMessage first. + // Next trying to decode key as MetaText and value as MetaMap + { + var m map[string]cbor.RawMessage + if _, err := cbor.Decode(b, &m); err == nil && len(m) > 0 { + pairs := make([]MetaPair, 0, len(m)) + for k, rv := range m { + val, err := DecodeMetadatumRaw(rv) + if err != nil { + return nil, fmt.Errorf("decode map(text) value: %w", err) + } + pairs = append(pairs, MetaPair{ + Key: MetaText{Value: k}, + Value: val, + }) + } + return MetaMap{Pairs: pairs}, nil + } + } + + return nil, errors.New("unsupported metadatum shape") +} + +// Decodes the transaction metadata set. +func (s *TransactionMetadataSet) UnmarshalCBOR(cborData []byte) error { + // Trying to decode as map[uint64]cbor.RawMessage. + // Calling DecodeMetadatumRaw for each entry call to get the typed value. + { + var tmp map[uint64]cbor.RawMessage + if _, err := cbor.Decode(cborData, &tmp); err == nil { + out := make(TransactionMetadataSet, len(tmp)) + for k, v := range tmp { + md, err := DecodeMetadatumRaw(v) + if err != nil { + return fmt.Errorf("decode metadata value for index %d: %w", k, err) + } + out[k] = md + } + *s = out + return nil + } + } + // Trying to decode as []cbor.RawMessage. + // Each element in array is decoded by calling DecodeMetadatumRaw + { + var arr []cbor.RawMessage + if _, err := cbor.Decode(cborData, &arr); err == nil { + out := make(TransactionMetadataSet) + for i, raw := range arr { + var probe any + // Skipping null values as well after decoding to cbor.RawMessage + if _, err := cbor.Decode(raw, &probe); err == nil && probe == nil { + continue + } + md, err := DecodeMetadatumRaw(raw) + if err != nil { + return fmt.Errorf("decode metadata list item %d: %w", i, err) + } + out[uint64(i)] = md // #nosec G115 + } + *s = out + return nil + } + } + return errors.New("unsupported TransactionMetadataSet encoding") +} + +// Encodes the transaction metadata set as a CBOR map +func (s TransactionMetadataSet) MarshalCBOR() ([]byte, error) { + if s == nil { + return cbor.Encode(&map[uint64]any{}) + } + contiguous := true + var maxKey uint64 + for k := range s { + if k > maxKey { + maxKey = k + } + } + // expectedCount64 is the length the array + expectedCount64 := maxKey + 1 + if expectedCount64 > uint64(math.MaxInt) { + return nil, errors.New("metadata set too large to encode as array") + } + expectedCount := int(expectedCount64) // #nosec G115 + if len(s) != expectedCount { + contiguous = false + } else { + for i := uint64(0); i < expectedCount64; i++ { + if _, ok := s[i]; !ok { + contiguous = false + break + } + } + } + if contiguous { + arr := make([]any, expectedCount) + for i := uint64(0); i < expectedCount64; i++ { + arr[i] = metadatumToInterface(s[i]) + } + return cbor.Encode(&arr) + } + // Otherwise Encode as a map. + tmpMap := make(map[uint64]any, len(s)) + for k, v := range s { + tmpMap[k] = metadatumToInterface(v) + } + return cbor.Encode(&tmpMap) +} + +// converting typed metadatum back into regular go values where the CBOR library can encode +func metadatumToInterface(m TransactionMetadatum) any { + switch t := m.(type) { + case MetaInt: + return t.Value + case MetaBytes: + return []byte(t.Value) + case MetaText: + return t.Value + case MetaList: + out := make([]any, 0, len(t.Items)) + for _, it := range t.Items { + out = append(out, metadatumToInterface(it)) + } + return out + case MetaMap: + mm := make(map[any]any, len(t.Pairs)) + for _, p := range t.Pairs { + mm[metadatumToInterface(p.Key)] = metadatumToInterface(p.Value) + } + return mm + default: + return nil + } +} diff --git a/ledger/conway/conway.go b/ledger/conway/conway.go index 55662be2..c396cca4 100644 --- a/ledger/conway/conway.go +++ b/ledger/conway/conway.go @@ -55,7 +55,7 @@ type ConwayBlock struct { BlockHeader *ConwayBlockHeader TransactionBodies []ConwayTransactionBody TransactionWitnessSets []ConwayTransactionWitnessSet - TransactionMetadataSet map[uint]*cbor.LazyValue + TransactionMetadataSet map[uint]common.TransactionMetadataSet InvalidTransactions []uint } @@ -518,7 +518,7 @@ type ConwayTransaction struct { Body ConwayTransactionBody WitnessSet ConwayTransactionWitnessSet TxIsValid bool - TxMetadata *cbor.LazyValue + TxMetadata common.TransactionMetadataSet } func (t *ConwayTransaction) UnmarshalCBOR(cborData []byte) error { @@ -632,7 +632,7 @@ func (t ConwayTransaction) Donation() uint64 { return t.Body.Donation() } -func (t ConwayTransaction) Metadata() *cbor.LazyValue { +func (t ConwayTransaction) Metadata() common.TransactionMetadataSet { return t.TxMetadata } @@ -699,7 +699,7 @@ func (t *ConwayTransaction) Cbor() []byte { t.TxIsValid, } if t.TxMetadata != nil { - tmpObj = append(tmpObj, cbor.RawMessage(t.TxMetadata.Cbor())) + tmpObj = append(tmpObj, t.TxMetadata) } else { tmpObj = append(tmpObj, nil) } diff --git a/ledger/mary/mary.go b/ledger/mary/mary.go index 898661fe..883b8a5c 100644 --- a/ledger/mary/mary.go +++ b/ledger/mary/mary.go @@ -52,7 +52,7 @@ type MaryBlock struct { BlockHeader *MaryBlockHeader TransactionBodies []MaryTransactionBody TransactionWitnessSets []shelley.ShelleyTransactionWitnessSet - TransactionMetadataSet map[uint]*cbor.LazyValue + TransactionMetadataSet map[uint]common.TransactionMetadataSet } func (b *MaryBlock) UnmarshalCBOR(cborData []byte) error { @@ -247,7 +247,7 @@ type MaryTransaction struct { hash *common.Blake2b256 Body MaryTransactionBody WitnessSet shelley.ShelleyTransactionWitnessSet - TxMetadata *cbor.LazyValue + TxMetadata common.TransactionMetadataSet } func (t *MaryTransaction) UnmarshalCBOR(cborData []byte) error { @@ -361,7 +361,7 @@ func (t MaryTransaction) Donation() uint64 { return t.Body.Donation() } -func (t MaryTransaction) Metadata() *cbor.LazyValue { +func (t MaryTransaction) Metadata() common.TransactionMetadataSet { return t.TxMetadata } @@ -411,7 +411,7 @@ func (t *MaryTransaction) Cbor() []byte { cbor.RawMessage(t.WitnessSet.Cbor()), } if t.TxMetadata != nil { - tmpObj = append(tmpObj, cbor.RawMessage(t.TxMetadata.Cbor())) + tmpObj = append(tmpObj, t.TxMetadata) } else { tmpObj = append(tmpObj, nil) } diff --git a/ledger/shelley/shelley.go b/ledger/shelley/shelley.go index 929df113..d72d9324 100644 --- a/ledger/shelley/shelley.go +++ b/ledger/shelley/shelley.go @@ -53,7 +53,7 @@ type ShelleyBlock struct { BlockHeader *ShelleyBlockHeader TransactionBodies []ShelleyTransactionBody TransactionWitnessSets []ShelleyTransactionWitnessSet - TransactionMetadataSet map[uint]*cbor.LazyValue + TransactionMetadataSet map[uint]common.TransactionMetadataSet } func (b *ShelleyBlock) UnmarshalCBOR(cborData []byte) error { @@ -540,7 +540,7 @@ type ShelleyTransaction struct { hash *common.Blake2b256 Body ShelleyTransactionBody WitnessSet ShelleyTransactionWitnessSet - TxMetadata *cbor.LazyValue + TxMetadata common.TransactionMetadataSet } func (t *ShelleyTransaction) UnmarshalCBOR(cborData []byte) error { @@ -650,7 +650,7 @@ func (t ShelleyTransaction) Donation() uint64 { return t.Body.Donation() } -func (t ShelleyTransaction) Metadata() *cbor.LazyValue { +func (t ShelleyTransaction) Metadata() common.TransactionMetadataSet { return t.TxMetadata } @@ -709,7 +709,7 @@ func (t *ShelleyTransaction) Cbor() []byte { cbor.RawMessage(t.WitnessSet.Cbor()), } if t.TxMetadata != nil { - tmpObj = append(tmpObj, cbor.RawMessage(t.TxMetadata.Cbor())) + tmpObj = append(tmpObj, t.TxMetadata) } else { tmpObj = append(tmpObj, nil) }