Skip to content

Commit 9962db5

Browse files
wolf31o2GitHub Copilot
andcommitted
feat: implement CBOR marshal/unmarshal for ScriptsNotPaidUtxo error
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 39d18fd commit 9962db5

File tree

2 files changed

+286
-3
lines changed

2 files changed

+286
-3
lines changed

ledger/error.go

Lines changed: 91 additions & 3 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 (
@@ -534,14 +537,99 @@ func (e *InsufficientCollateral) Error() string {
534537
)
535538
}
536539

540+
// ScriptsNotPaidUtxo represents the ScriptsNotPaidUTxO error from cardano-ledger.
541+
// Haskell: ScriptsNotPaidUTxO !(UTxO era) where UTxO era = Map TxIn TxOut
542+
// CBOR: [14, utxo_map]
537543
type ScriptsNotPaidUtxo struct {
538544
UtxoFailureErrorBase
539-
// TODO: determine content/structure of this value (#847)
540-
Value cbor.Value
545+
Utxos []common.Utxo // Each Utxo contains Id (input) and Output
546+
}
547+
548+
func (e *ScriptsNotPaidUtxo) MarshalCBOR() ([]byte, error) {
549+
utxoMap := make(
550+
map[common.TransactionInput]common.TransactionOutput,
551+
len(e.Utxos),
552+
)
553+
for _, u := range e.Utxos {
554+
// Return error for nil entries instead of silently skipping
555+
if u.Id == nil || u.Output == nil {
556+
return nil, errors.New("ScriptsNotPaidUtxo: cannot marshal UTxO with nil Id or Output")
557+
}
558+
utxoMap[u.Id] = u.Output
559+
}
560+
arr := []any{UtxoFailureScriptsNotPaidUtxo, utxoMap}
561+
return cbor.Encode(arr)
562+
}
563+
564+
func (e *ScriptsNotPaidUtxo) UnmarshalCBOR(data []byte) error {
565+
type tScriptsNotPaidUtxo struct {
566+
cbor.StructAsArray
567+
ConstructorIdx uint64
568+
UtxoMapCbor cbor.RawMessage
569+
}
570+
var tmp tScriptsNotPaidUtxo
571+
if _, err := cbor.Decode(data, &tmp); err != nil {
572+
return fmt.Errorf("failed to decode ScriptsNotPaidUtxo: %w", err)
573+
}
574+
575+
if tmp.ConstructorIdx != UtxoFailureScriptsNotPaidUtxo {
576+
return fmt.Errorf(
577+
"ScriptsNotPaidUtxo: expected constructor index %d, got %d",
578+
UtxoFailureScriptsNotPaidUtxo,
579+
tmp.ConstructorIdx,
580+
)
581+
}
582+
583+
// For era-agnostic decoding, we need to handle the map structure carefully
584+
// Since we can't use cbor.RawMessage as map keys, we'll decode to a concrete type first
585+
// and then convert to era-agnostic types. Try different era input types until one works.
586+
587+
// Try Shelley-family transaction inputs first (most common from Shelley onwards)
588+
var shelleyUtxoMap map[shelley.ShelleyTransactionInput]cbor.RawMessage
589+
if _, err := cbor.Decode(tmp.UtxoMapCbor, &shelleyUtxoMap); err == nil {
590+
// Successfully decoded as Shelley-family inputs
591+
e.Utxos = make([]common.Utxo, 0, len(shelleyUtxoMap))
592+
for input, outputCbor := range shelleyUtxoMap {
593+
// Decode output using era-agnostic function (handles all eras)
594+
output, err := NewTransactionOutputFromCbor(outputCbor)
595+
if err != nil {
596+
return fmt.Errorf("failed to decode transaction output: %w", err)
597+
}
598+
599+
e.Utxos = append(e.Utxos, common.Utxo{
600+
Id: input,
601+
Output: output,
602+
})
603+
}
604+
return nil
605+
}
606+
607+
// Try Byron transaction inputs (for Byron era)
608+
var byronUtxoMap map[byron.ByronTransactionInput]cbor.RawMessage
609+
if _, err := cbor.Decode(tmp.UtxoMapCbor, &byronUtxoMap); err == nil {
610+
// Successfully decoded as Byron inputs
611+
e.Utxos = make([]common.Utxo, 0, len(byronUtxoMap))
612+
for input, outputCbor := range byronUtxoMap {
613+
// Decode output using era-agnostic function (handles all eras)
614+
output, err := NewTransactionOutputFromCbor(outputCbor)
615+
if err != nil {
616+
return fmt.Errorf("failed to decode transaction output: %w", err)
617+
}
618+
619+
e.Utxos = append(e.Utxos, common.Utxo{
620+
Id: input,
621+
Output: output,
622+
})
623+
}
624+
return nil
625+
}
626+
627+
// If both failed, return an error
628+
return errors.New("failed to decode UTxO map as either Shelley-family or Byron transaction inputs")
541629
}
542630

543631
func (e *ScriptsNotPaidUtxo) Error() string {
544-
return fmt.Sprintf("ScriptsNotPaidUtxo (%v)", e.Value.Value())
632+
return fmt.Sprintf("ScriptsNotPaidUtxo (%d UTxOs)", len(e.Utxos))
545633
}
546634

547635
type ExUnitsTooBigUtxo struct {

ledger/error_test.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package ledger
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/blinklabs-io/gouroboros/ledger/byron"
8+
"github.com/blinklabs-io/gouroboros/ledger/common"
9+
"github.com/blinklabs-io/gouroboros/ledger/shelley"
10+
)
11+
12+
func TestScriptsNotPaidUtxo_MarshalUnmarshalCBOR(t *testing.T) {
13+
// Create multiple UTxOs with different data to test for corruption
14+
addr1, err := common.NewAddress("addr1qytna5k2fq9ler0fuk45j7zfwv7t2zwhp777nvdjqqfr5tz8ztpwnk8zq5ngetcz5k5mckgkajnygtsra9aej2h3ek5seupmvd")
15+
if err != nil {
16+
t.Fatalf("Failed to create address 1: %v", err)
17+
}
18+
addr2, err := common.NewAddress("addr1qyln2c2cx5jc4hw768pwz60n5245462dvp4auqcw09rl2xz07huw84puu6cea3qe0ce3apks7hjckqkh5ad4uax0l9ws0q9xty")
19+
if err != nil {
20+
t.Fatalf("Failed to create address 2: %v", err)
21+
}
22+
addr3, err := common.NewAddress("addr1z8snz7c4974vzdpxu65ruphl3zjdvtxw8strf2c2tmqnxz2j2c79gy9l76sdg0xwhd7r0c0kna0tycz4y5s6mlenh8pq0xmsha")
23+
if err != nil {
24+
t.Fatalf("Failed to create address 3: %v", err)
25+
}
26+
27+
utxos := []common.Utxo{
28+
{
29+
Id: shelley.NewShelleyTransactionInput("deadbeef00000000000000000000000000000000000000000000000000000000", 0),
30+
Output: &shelley.ShelleyTransactionOutput{
31+
OutputAddress: addr1,
32+
OutputAmount: 1000,
33+
},
34+
},
35+
{
36+
Id: shelley.NewShelleyTransactionInput("cafebabe11111111111111111111111111111111111111111111111111111111", 1),
37+
Output: &shelley.ShelleyTransactionOutput{
38+
OutputAddress: addr2,
39+
OutputAmount: 2500,
40+
},
41+
},
42+
{
43+
Id: shelley.NewShelleyTransactionInput("feedface22222222222222222222222222222222222222222222222222222222", 2),
44+
Output: &shelley.ShelleyTransactionOutput{
45+
OutputAddress: addr3,
46+
OutputAmount: 7500,
47+
},
48+
},
49+
}
50+
51+
// Marshal to CBOR
52+
original := &ScriptsNotPaidUtxo{
53+
Utxos: utxos,
54+
}
55+
originalCborData, err := original.MarshalCBOR()
56+
if err != nil {
57+
t.Fatalf("MarshalCBOR failed: %v", err)
58+
}
59+
60+
// Unmarshal back
61+
var decoded ScriptsNotPaidUtxo
62+
if err := decoded.UnmarshalCBOR(originalCborData); err != nil {
63+
t.Fatalf("UnmarshalCBOR failed: %v", err)
64+
}
65+
66+
// Validate count
67+
if len(decoded.Utxos) != len(utxos) {
68+
t.Fatalf("Expected %d UTxOs, got %d", len(utxos), len(decoded.Utxos))
69+
}
70+
71+
// Validate each UTxO's data integrity
72+
for i, originalUtxo := range utxos {
73+
found := false
74+
for _, decodedUtxo := range decoded.Utxos {
75+
// Check if this is the matching UTxO by comparing transaction input
76+
if decodedUtxo.Id.Id() == originalUtxo.Id.Id() && decodedUtxo.Id.Index() == originalUtxo.Id.Index() {
77+
found = true
78+
79+
// Validate transaction output data using interface methods
80+
originalOutput := originalUtxo.Output.(*shelley.ShelleyTransactionOutput)
81+
82+
// Check the amount using the interface method
83+
if decodedUtxo.Output.Amount() != originalOutput.OutputAmount {
84+
t.Errorf("UTxO %d: Amount mismatch - expected %d, got %d",
85+
i, originalOutput.OutputAmount, decodedUtxo.Output.Amount())
86+
}
87+
88+
// Check the address using the interface method
89+
if decodedUtxo.Output.Address().String() != originalOutput.OutputAddress.String() {
90+
t.Errorf("UTxO %d: Address mismatch - expected %s, got %s",
91+
i, originalOutput.OutputAddress.String(), decodedUtxo.Output.Address().String())
92+
}
93+
break
94+
}
95+
}
96+
if !found {
97+
t.Errorf("UTxO %d not found in decoded data: %s#%d",
98+
i, originalUtxo.Id.Id().String(), originalUtxo.Id.Index())
99+
}
100+
}
101+
102+
// Test round-trip CBOR fidelity by re-marshaling and comparing bytes
103+
remarshaled, err := decoded.MarshalCBOR()
104+
if err != nil {
105+
t.Fatalf("Re-marshaling failed: %v", err)
106+
}
107+
108+
if !bytes.Equal(originalCborData, remarshaled) {
109+
t.Errorf("Round-trip CBOR fidelity failed - bytes don't match")
110+
t.Logf("Original: %x", originalCborData)
111+
t.Logf("Remarshaled: %x", remarshaled)
112+
}
113+
114+
t.Logf("Successfully validated %d UTxOs with full data integrity and round-trip fidelity", len(utxos))
115+
}
116+
117+
func TestScriptsNotPaidUtxo_MarshalUnmarshalCBOR_AllEras(t *testing.T) {
118+
// Create test addresses
119+
addr1, err := common.NewAddress("addr1qytna5k2fq9ler0fuk45j7zfwv7t2zwhp777nvdjqqfr5tz8ztpwnk8zq5ngetcz5k5mckgkajnygtsra9aej2h3ek5seupmvd")
120+
if err != nil {
121+
t.Fatalf("Failed to create address 1: %v", err)
122+
}
123+
addr2, err := common.NewAddress("addr1qyln2c2cx5jc4hw768pwz60n5245462dvp4auqcw09rl2xz07huw84puu6cea3qe0ce3apks7hjckqkh5ad4uax0l9ws0q9xty")
124+
if err != nil {
125+
t.Fatalf("Failed to create address 2: %v", err)
126+
}
127+
128+
// Test with Byron transaction inputs
129+
byronInput1 := byron.NewByronTransactionInput("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789ab", 0)
130+
byronInput2 := byron.NewByronTransactionInput("fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210fe", 1)
131+
132+
// Create test UTxOs with Byron inputs
133+
byronUtxos := []common.Utxo{
134+
{
135+
Id: byronInput1,
136+
Output: &shelley.ShelleyTransactionOutput{OutputAddress: addr1, OutputAmount: 1000000},
137+
},
138+
{
139+
Id: byronInput2,
140+
Output: &shelley.ShelleyTransactionOutput{OutputAddress: addr2, OutputAmount: 2500000},
141+
},
142+
}
143+
144+
// Test Byron inputs
145+
byronError := &ScriptsNotPaidUtxo{Utxos: byronUtxos}
146+
byronCborData, err := byronError.MarshalCBOR()
147+
if err != nil {
148+
t.Fatalf("Byron marshal failed: %v", err)
149+
}
150+
151+
var decodedByron ScriptsNotPaidUtxo
152+
if err := decodedByron.UnmarshalCBOR(byronCborData); err != nil {
153+
t.Fatalf("Byron unmarshal failed: %v", err)
154+
}
155+
156+
// Validate Byron decoding
157+
if len(decodedByron.Utxos) != 2 {
158+
t.Errorf("Expected 2 Byron UTxOs, got %d", len(decodedByron.Utxos))
159+
}
160+
161+
// Test Shelley inputs
162+
shelleyInput1 := shelley.NewShelleyTransactionInput("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", 0)
163+
shelleyInput2 := shelley.NewShelleyTransactionInput("fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321", 1)
164+
165+
shelleyUtxos := []common.Utxo{
166+
{
167+
Id: shelleyInput1,
168+
Output: &shelley.ShelleyTransactionOutput{OutputAddress: addr1, OutputAmount: 1500000},
169+
},
170+
{
171+
Id: shelleyInput2,
172+
Output: &shelley.ShelleyTransactionOutput{OutputAddress: addr2, OutputAmount: 3000000},
173+
},
174+
}
175+
176+
// Test Shelley inputs
177+
shelleyError := &ScriptsNotPaidUtxo{Utxos: shelleyUtxos}
178+
shelleyCborData, err := shelleyError.MarshalCBOR()
179+
if err != nil {
180+
t.Fatalf("Shelley marshal failed: %v", err)
181+
}
182+
183+
var decodedShelley ScriptsNotPaidUtxo
184+
if err := decodedShelley.UnmarshalCBOR(shelleyCborData); err != nil {
185+
t.Fatalf("Shelley unmarshal failed: %v", err)
186+
}
187+
188+
// Validate Shelley decoding
189+
if len(decodedShelley.Utxos) != 2 {
190+
t.Errorf("Expected 2 Shelley UTxOs, got %d", len(decodedShelley.Utxos))
191+
}
192+
193+
t.Logf("Successfully validated era-agnostic CBOR handling: Byron (%d UTxOs) and Shelley (%d UTxOs)",
194+
len(decodedByron.Utxos), len(decodedShelley.Utxos))
195+
}

0 commit comments

Comments
 (0)