diff --git a/ledger/allegra/allegra.go b/ledger/allegra/allegra.go index d13ea67c..30d896ef 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 { @@ -238,7 +238,7 @@ type AllegraTransaction struct { cbor.DecodeStoreCbor Body AllegraTransactionBody WitnessSet shelley.ShelleyTransactionWitnessSet - TxMetadata *cbor.LazyValue + TxMetadata common.TransactionMetadataSet } func (t *AllegraTransaction) UnmarshalCBOR(cborData []byte) error { @@ -336,7 +336,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 } @@ -398,7 +398,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 5ce28b8a..2a537bd1 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 } @@ -577,7 +577,7 @@ type AlonzoTransaction struct { Body AlonzoTransactionBody WitnessSet AlonzoTransactionWitnessSet TxIsValid bool - TxMetadata *cbor.LazyValue + TxMetadata common.TransactionMetadataSet } func (t *AlonzoTransaction) UnmarshalCBOR(cborData []byte) error { @@ -679,7 +679,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 } @@ -739,7 +739,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 7cb65d7b..24abf134 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 } @@ -710,7 +710,7 @@ type BabbageTransaction struct { Body BabbageTransactionBody WitnessSet BabbageTransactionWitnessSet TxIsValid bool - TxMetadata *cbor.LazyValue + TxMetadata common.TransactionMetadataSet } func (t *BabbageTransaction) UnmarshalCBOR(cborData []byte) error { @@ -812,7 +812,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 } @@ -879,7 +879,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 25939879..622af1e0 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 { @@ -271,7 +271,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 37d43b5e..9dc66f14 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" @@ -26,13 +29,44 @@ type Transaction interface { TransactionBody Type() int Cbor() []byte - 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 @@ -249,3 +283,209 @@ func TransactionBodyToUtxorpc(tx TransactionBody) *utxorpc.Tx { return ret } + +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 0feb953f..fd10d921 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 } @@ -513,7 +513,7 @@ type ConwayTransaction struct { Body ConwayTransactionBody WitnessSet ConwayTransactionWitnessSet TxIsValid bool - TxMetadata *cbor.LazyValue + TxMetadata common.TransactionMetadataSet } func (t *ConwayTransaction) UnmarshalCBOR(cborData []byte) error { @@ -615,7 +615,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 } @@ -682,7 +682,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 6562d3be..5789d8bc 100644 --- a/ledger/mary/mary.go +++ b/ledger/mary/mary.go @@ -51,7 +51,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 { @@ -245,7 +245,7 @@ type MaryTransaction struct { cbor.DecodeStoreCbor Body MaryTransactionBody WitnessSet shelley.ShelleyTransactionWitnessSet - TxMetadata *cbor.LazyValue + TxMetadata common.TransactionMetadataSet } func (t *MaryTransaction) UnmarshalCBOR(cborData []byte) error { @@ -347,7 +347,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 } @@ -397,7 +397,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 3b37bf19..54f9db57 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 { @@ -504,7 +504,7 @@ type ShelleyTransaction struct { cbor.DecodeStoreCbor Body ShelleyTransactionBody WitnessSet ShelleyTransactionWitnessSet - TxMetadata *cbor.LazyValue + TxMetadata common.TransactionMetadataSet } func (t *ShelleyTransaction) UnmarshalCBOR(cborData []byte) error { @@ -602,7 +602,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 } @@ -661,7 +661,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) }