Skip to content

Commit bd49806

Browse files
wolf31o2GitHub Copilot
andauthored
feat: implement CBOR marshal/unmarshal for ScriptsNotPaidUtxo error (#1228)
Add custom CBOR encoding/decoding for ScriptsNotPaidUtxo that matches the Haskell cardano-ledger specification. The implementation uses `[constructor_index, utxo_map]` format with multi-strategy decoding to handle complex CBOR map structures. - Add `MarshalCBOR` converting slice to map format - Add `UnmarshalCBOR` with fallback decoding strategies - Add comprehensive test with mock interfaces - Maintain compatibility with existing error handling Signed-off-by: Chris Gianelloni <[email protected]> Co-authored-by: GitHub Copilot <[email protected]>
1 parent 8198c5c commit bd49806

File tree

2 files changed

+737
-34
lines changed

2 files changed

+737
-34
lines changed

ledger/error.go

Lines changed: 214 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import (
2121
"strings"
2222

2323
"github.com/blinklabs-io/gouroboros/cbor"
24+
"github.com/blinklabs-io/gouroboros/ledger/byron"
25+
"github.com/blinklabs-io/gouroboros/ledger/common"
26+
"github.com/blinklabs-io/gouroboros/ledger/shelley"
2427
)
2528

2629
const (
@@ -42,20 +45,88 @@ const (
4245
UtxoFailureWrongNetworkWithdrawal = 9
4346
UtxoFailureOutputBootAddrAttrsTooBig = 10
4447
UtxoFailureTriesToForgeAda = 11
45-
UtxoFailureOutputTooBigUtxo = 12
46-
UtxoFailureInsufficientCollateral = 13
47-
UtxoFailureScriptsNotPaidUtxo = 14
48-
UtxoFailureExUnitsTooBigUtxo = 15
49-
UtxoFailureCollateralContainsNonAda = 16
48+
UtxoFailureInsufficientCollateral = 12
5049
UtxoFailureWrongNetworkInTxBody = 17
5150
UtxoFailureOutsideForecast = 18
5251
UtxoFailureTooManyCollateralInputs = 19
5352
UtxoFailureNoCollateralInputs = 20
5453
)
5554

55+
// Era-specific constants for errors that differ between Cardano eras
56+
const (
57+
// Alonzo era error constants
58+
UtxoFailureOutputTooBigUtxoAlonzo = 12
59+
UtxoFailureScriptsNotPaidUtxoAlonzo = 14
60+
UtxoFailureExUnitsTooBigUtxoAlonzo = 15
61+
UtxoFailureCollateralContainsNonAdaAlonzo = 16
62+
63+
// Babbage era error constants (same as Alonzo for these errors)
64+
UtxoFailureOutputTooBigUtxoBabbage = 12
65+
UtxoFailureScriptsNotPaidUtxoBabbage = 14
66+
UtxoFailureExUnitsTooBigUtxoBabbage = 15
67+
UtxoFailureCollateralContainsNonAdaBabbage = 16
68+
69+
// Conway era error constants
70+
UtxoFailureOutputTooBigUtxoConway = 11
71+
UtxoFailureScriptsNotPaidUtxoConway = 13
72+
UtxoFailureExUnitsTooBigUtxoConway = 14
73+
UtxoFailureCollateralContainsNonAdaConway = 15
74+
)
75+
5676
// Helper type to make the code a little cleaner
5777
type NewErrorFromCborFunc func([]byte) (error, error)
5878

79+
// getEraSpecificUtxoFailureConstants returns the correct error constants for the given era
80+
func getEraSpecificUtxoFailureConstants(eraId uint8) (map[int]any, int, int, int, int) {
81+
baseMap := map[int]any{
82+
UtxoFailureBadInputsUtxo: &BadInputsUtxo{},
83+
UtxoFailureOutsideValidityIntervalUtxo: &OutsideValidityIntervalUtxo{},
84+
UtxoFailureMaxTxSizeUtxo: &MaxTxSizeUtxo{},
85+
UtxoFailureInputSetEmpty: &InputSetEmptyUtxo{},
86+
UtxoFailureFeeTooSmallUtxo: &FeeTooSmallUtxo{},
87+
UtxoFailureValueNotConservedUtxo: &ValueNotConservedUtxo{},
88+
UtxoFailureOutputTooSmallUtxo: &OutputTooSmallUtxo{},
89+
UtxoFailureUtxosFailure: &UtxosFailure{},
90+
UtxoFailureWrongNetwork: &WrongNetwork{},
91+
UtxoFailureWrongNetworkWithdrawal: &WrongNetworkWithdrawal{},
92+
UtxoFailureOutputBootAddrAttrsTooBig: &OutputBootAddrAttrsTooBig{},
93+
UtxoFailureTriesToForgeAda: &TriesToForgeADA{},
94+
UtxoFailureInsufficientCollateral: &InsufficientCollateral{},
95+
UtxoFailureWrongNetworkInTxBody: &WrongNetworkInTxBody{},
96+
UtxoFailureOutsideForecast: &OutsideForecast{},
97+
UtxoFailureTooManyCollateralInputs: &TooManyCollateralInputs{},
98+
UtxoFailureNoCollateralInputs: &NoCollateralInputs{},
99+
}
100+
101+
switch eraId {
102+
case EraIdAlonzo:
103+
baseMap[UtxoFailureOutputTooBigUtxoAlonzo] = &OutputTooBigUtxo{}
104+
baseMap[UtxoFailureScriptsNotPaidUtxoAlonzo] = &ScriptsNotPaidUtxo{}
105+
baseMap[UtxoFailureExUnitsTooBigUtxoAlonzo] = &ExUnitsTooBigUtxo{}
106+
baseMap[UtxoFailureCollateralContainsNonAdaAlonzo] = &CollateralContainsNonADA{}
107+
return baseMap, UtxoFailureOutputTooBigUtxoAlonzo, UtxoFailureScriptsNotPaidUtxoAlonzo, UtxoFailureExUnitsTooBigUtxoAlonzo, UtxoFailureCollateralContainsNonAdaAlonzo
108+
case EraIdBabbage:
109+
baseMap[UtxoFailureOutputTooBigUtxoBabbage] = &OutputTooBigUtxo{}
110+
baseMap[UtxoFailureScriptsNotPaidUtxoBabbage] = &ScriptsNotPaidUtxo{}
111+
baseMap[UtxoFailureExUnitsTooBigUtxoBabbage] = &ExUnitsTooBigUtxo{}
112+
baseMap[UtxoFailureCollateralContainsNonAdaBabbage] = &CollateralContainsNonADA{}
113+
return baseMap, UtxoFailureOutputTooBigUtxoBabbage, UtxoFailureScriptsNotPaidUtxoBabbage, UtxoFailureExUnitsTooBigUtxoBabbage, UtxoFailureCollateralContainsNonAdaBabbage
114+
case EraIdConway:
115+
baseMap[UtxoFailureOutputTooBigUtxoConway] = &OutputTooBigUtxo{}
116+
baseMap[UtxoFailureScriptsNotPaidUtxoConway] = &ScriptsNotPaidUtxo{}
117+
baseMap[UtxoFailureExUnitsTooBigUtxoConway] = &ExUnitsTooBigUtxo{}
118+
baseMap[UtxoFailureCollateralContainsNonAdaConway] = &CollateralContainsNonADA{}
119+
return baseMap, UtxoFailureOutputTooBigUtxoConway, UtxoFailureScriptsNotPaidUtxoConway, UtxoFailureExUnitsTooBigUtxoConway, UtxoFailureCollateralContainsNonAdaConway
120+
default:
121+
// For other eras (Byron, Shelley, Allegra, Mary), use Conway constants as fallback
122+
baseMap[UtxoFailureOutputTooBigUtxoConway] = &OutputTooBigUtxo{}
123+
baseMap[UtxoFailureScriptsNotPaidUtxoConway] = &ScriptsNotPaidUtxo{}
124+
baseMap[UtxoFailureExUnitsTooBigUtxoConway] = &ExUnitsTooBigUtxo{}
125+
baseMap[UtxoFailureCollateralContainsNonAdaConway] = &CollateralContainsNonADA{}
126+
return baseMap, UtxoFailureOutputTooBigUtxoConway, UtxoFailureScriptsNotPaidUtxoConway, UtxoFailureExUnitsTooBigUtxoConway, UtxoFailureCollateralContainsNonAdaConway
127+
}
128+
}
129+
59130
func NewGenericErrorFromCbor(cborData []byte) (error, error) {
60131
newErr := &GenericError{}
61132
if _, err := cbor.Decode(cborData, newErr); err != nil {
@@ -262,32 +333,8 @@ func (e *UtxoFailure) UnmarshalCBOR(data []byte) error {
262333
return err
263334
}
264335
e.Era = tmpData.Era
265-
newErr, err := cbor.DecodeById(
266-
tmpData.Err,
267-
map[int]any{
268-
UtxoFailureBadInputsUtxo: &BadInputsUtxo{},
269-
UtxoFailureOutsideValidityIntervalUtxo: &OutsideValidityIntervalUtxo{},
270-
UtxoFailureMaxTxSizeUtxo: &MaxTxSizeUtxo{},
271-
UtxoFailureInputSetEmpty: &InputSetEmptyUtxo{},
272-
UtxoFailureFeeTooSmallUtxo: &FeeTooSmallUtxo{},
273-
UtxoFailureValueNotConservedUtxo: &ValueNotConservedUtxo{},
274-
UtxoFailureOutputTooSmallUtxo: &OutputTooSmallUtxo{},
275-
UtxoFailureUtxosFailure: &UtxosFailure{},
276-
UtxoFailureWrongNetwork: &WrongNetwork{},
277-
UtxoFailureWrongNetworkWithdrawal: &WrongNetworkWithdrawal{},
278-
UtxoFailureOutputBootAddrAttrsTooBig: &OutputBootAddrAttrsTooBig{},
279-
UtxoFailureTriesToForgeAda: &TriesToForgeADA{},
280-
UtxoFailureOutputTooBigUtxo: &OutputTooBigUtxo{},
281-
UtxoFailureInsufficientCollateral: &InsufficientCollateral{},
282-
UtxoFailureScriptsNotPaidUtxo: &ScriptsNotPaidUtxo{},
283-
UtxoFailureExUnitsTooBigUtxo: &ExUnitsTooBigUtxo{},
284-
UtxoFailureCollateralContainsNonAda: &CollateralContainsNonADA{},
285-
UtxoFailureWrongNetworkInTxBody: &WrongNetworkInTxBody{},
286-
UtxoFailureOutsideForecast: &OutsideForecast{},
287-
UtxoFailureTooManyCollateralInputs: &TooManyCollateralInputs{},
288-
UtxoFailureNoCollateralInputs: &NoCollateralInputs{},
289-
},
290-
)
336+
errorMap, _, _, _, _ := getEraSpecificUtxoFailureConstants(tmpData.Era)
337+
newErr, err := cbor.DecodeById(tmpData.Err, errorMap)
291338
if err != nil {
292339
newErr, err = NewGenericErrorFromCbor(tmpData.Err)
293340
if err != nil {
@@ -534,14 +581,147 @@ func (e *InsufficientCollateral) Error() string {
534581
)
535582
}
536583

584+
// ScriptsNotPaidUtxo represents the ScriptsNotPaidUTxO error from cardano-ledger.
585+
// Haskell: ScriptsNotPaidUTxO !(UTxO era) where UTxO era = Map TxIn TxOut
586+
// CBOR: [14, utxo_map]
537587
type ScriptsNotPaidUtxo struct {
538588
UtxoFailureErrorBase
539-
// TODO: determine content/structure of this value (#847)
540-
Value cbor.Value
589+
Utxos []common.Utxo // Each Utxo contains Id (input) and Output
590+
}
591+
592+
func (e *ScriptsNotPaidUtxo) MarshalCBOR() ([]byte, error) {
593+
// Use era-specific constant - we'll use Conway as default since it has the most recent structure
594+
// In practice, this should be set when the error is created, but we provide a sensible fallback
595+
constantToUse := UtxoFailureScriptsNotPaidUtxoConway
596+
if e.Type != 0 {
597+
constantToUse = int(e.Type)
598+
}
599+
// Bounds check to prevent integer overflow
600+
if constantToUse < 0 || constantToUse > 255 {
601+
return nil, fmt.Errorf("ScriptsNotPaidUtxo: invalid constructor index %d (must be 0-255)", constantToUse)
602+
}
603+
e.Type = uint8(constantToUse)
604+
605+
utxoMap := make(
606+
map[common.TransactionInput]common.TransactionOutput,
607+
len(e.Utxos),
608+
)
609+
for _, u := range e.Utxos {
610+
// Return error for nil entries instead of silently skipping
611+
if u.Id == nil || u.Output == nil {
612+
return nil, errors.New(
613+
"ScriptsNotPaidUtxo: cannot marshal UTxO with nil Id or Output",
614+
)
615+
}
616+
utxoMap[u.Id] = u.Output
617+
}
618+
arr := []any{constantToUse, utxoMap}
619+
return cbor.Encode(arr)
620+
}
621+
622+
func (e *ScriptsNotPaidUtxo) UnmarshalCBOR(data []byte) error {
623+
type tScriptsNotPaidUtxo struct {
624+
cbor.StructAsArray
625+
ConstructorIdx uint64
626+
UtxoMapCbor cbor.RawMessage
627+
}
628+
var tmp tScriptsNotPaidUtxo
629+
if _, err := cbor.Decode(data, &tmp); err != nil {
630+
return fmt.Errorf("failed to decode ScriptsNotPaidUtxo: %w", err)
631+
}
632+
633+
// Check if the constructor index matches any valid era-specific constant
634+
validConstructors := []int{
635+
UtxoFailureScriptsNotPaidUtxoAlonzo,
636+
UtxoFailureScriptsNotPaidUtxoBabbage,
637+
UtxoFailureScriptsNotPaidUtxoConway,
638+
}
639+
640+
isValid := false
641+
for _, valid := range validConstructors {
642+
// Bounds check to prevent integer overflow
643+
if valid < 0 || valid > 65535 {
644+
continue // Skip invalid constants
645+
}
646+
if tmp.ConstructorIdx == uint64(valid) {
647+
isValid = true
648+
break
649+
}
650+
}
651+
652+
if !isValid {
653+
return fmt.Errorf(
654+
"ScriptsNotPaidUtxo: expected one of constructor indices %v, got %d",
655+
validConstructors,
656+
tmp.ConstructorIdx,
657+
)
658+
}
659+
660+
// Set the struct tag to match the decoded constructor
661+
// Bounds check to prevent integer overflow
662+
if tmp.ConstructorIdx > 255 {
663+
return fmt.Errorf("ScriptsNotPaidUtxo: constructor index %d exceeds uint8 range (0-255)", tmp.ConstructorIdx)
664+
}
665+
e.Type = uint8(tmp.ConstructorIdx)
666+
667+
// For era-agnostic decoding, we need to handle the map structure carefully
668+
// Since we can't use cbor.RawMessage as map keys, we'll decode to a concrete type first
669+
// and then convert to era-agnostic types. Try different era input types until one works.
670+
671+
// Try Shelley-family transaction inputs first (most common from Shelley onwards)
672+
var shelleyUtxoMap map[shelley.ShelleyTransactionInput]cbor.RawMessage
673+
if _, err := cbor.Decode(tmp.UtxoMapCbor, &shelleyUtxoMap); err == nil {
674+
// Successfully decoded as Shelley-family inputs
675+
e.Utxos = make([]common.Utxo, 0, len(shelleyUtxoMap))
676+
for input, outputCbor := range shelleyUtxoMap {
677+
// Decode output using era-agnostic function (handles all eras)
678+
output, err := NewTransactionOutputFromCbor(outputCbor)
679+
if err != nil {
680+
return fmt.Errorf(
681+
"failed to decode transaction output: %w",
682+
err,
683+
)
684+
}
685+
686+
e.Utxos = append(e.Utxos, common.Utxo{
687+
Id: input,
688+
Output: output,
689+
})
690+
}
691+
return nil
692+
}
693+
694+
// Try Byron transaction inputs (for Byron era)
695+
var byronUtxoMap map[byron.ByronTransactionInput]cbor.RawMessage
696+
if _, err := cbor.Decode(tmp.UtxoMapCbor, &byronUtxoMap); err == nil {
697+
// Successfully decoded as Byron inputs
698+
e.Utxos = make([]common.Utxo, 0, len(byronUtxoMap))
699+
for input, outputCbor := range byronUtxoMap {
700+
// Decode output using era-agnostic function (handles all eras)
701+
output, err := NewTransactionOutputFromCbor(outputCbor)
702+
if err != nil {
703+
return fmt.Errorf(
704+
"failed to decode transaction output: %w",
705+
err,
706+
)
707+
}
708+
709+
e.Utxos = append(e.Utxos, common.Utxo{
710+
Id: input,
711+
Output: output,
712+
})
713+
}
714+
return nil
715+
}
716+
717+
// If both failed, return an error
718+
return errors.New(
719+
"failed to decode UTxO map as either Shelley-family or Byron transaction inputs",
720+
)
541721
}
542722

543723
func (e *ScriptsNotPaidUtxo) Error() string {
544-
return fmt.Sprintf("ScriptsNotPaidUtxo (%v)", e.Value.Value())
724+
return fmt.Sprintf("ScriptsNotPaidUtxo (%d UTxOs)", len(e.Utxos))
545725
}
546726

547727
type ExUnitsTooBigUtxo struct {

0 commit comments

Comments
 (0)