Skip to content

Commit 9f51f74

Browse files
committed
feat(ledger): validate metadata during rule checks
Signed-off-by: Chris Gianelloni <[email protected]>
1 parent 7524697 commit 9f51f74

File tree

20 files changed

+543
-12
lines changed

20 files changed

+543
-12
lines changed

ledger/allegra/allegra.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ type AllegraTransaction struct {
259259
Body AllegraTransactionBody
260260
WitnessSet shelley.ShelleyTransactionWitnessSet
261261
TxMetadata common.TransactionMetadatum
262+
RawAuxData []byte // Raw auxiliary data bytes (includes scripts)
262263
}
263264

264265
func (t *AllegraTransaction) UnmarshalCBOR(cborData []byte) error {
@@ -282,8 +283,10 @@ func (t *AllegraTransaction) UnmarshalCBOR(cborData []byte) error {
282283
return fmt.Errorf("failed to decode transaction witness set: %w", err)
283284
}
284285
// Handle metadata (component 3, index 2) - always present, but may be CBOR nil
285-
// DecodeAuxiliaryDataToMetadata already preserves raw bytes via DecodeMetadatumRaw
286-
if len(txArray) > 2 && len(txArray[2]) > 0 {
286+
// Store raw auxiliary data bytes (including any scripts)
287+
if len(txArray) > 2 && len(txArray[2]) > 0 && txArray[2][0] != 0xF6 { // 0xF6 is CBOR null
288+
t.RawAuxData = []byte(txArray[2])
289+
// Also extract metadata
287290
metadata, err := common.DecodeAuxiliaryDataToMetadata(txArray[2])
288291
if err == nil && metadata != nil {
289292
t.TxMetadata = metadata
@@ -393,6 +396,10 @@ func (t *AllegraTransaction) Metadata() common.TransactionMetadatum {
393396
return t.TxMetadata
394397
}
395398

399+
func (t *AllegraTransaction) RawAuxiliaryData() []byte {
400+
return t.RawAuxData
401+
}
402+
396403
func (t AllegraTransaction) IsValid() bool {
397404
return true
398405
}

ledger/allegra/errors.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package allegra
1616

1717
import (
1818
"fmt"
19+
20+
"github.com/blinklabs-io/gouroboros/ledger/common"
1921
)
2022

2123
type OutsideValidityIntervalUtxoError struct {
@@ -30,3 +32,38 @@ func (e OutsideValidityIntervalUtxoError) Error() string {
3032
e.Slot,
3133
)
3234
}
35+
36+
type MissingTransactionMetadataError struct {
37+
Hash common.Blake2b256
38+
}
39+
40+
func (e MissingTransactionMetadataError) Error() string {
41+
return fmt.Sprintf(
42+
"missing transaction metadata: body declares hash %x but no metadata provided",
43+
e.Hash,
44+
)
45+
}
46+
47+
type MissingTransactionAuxiliaryDataHashError struct {
48+
Hash common.Blake2b256
49+
}
50+
51+
func (e MissingTransactionAuxiliaryDataHashError) Error() string {
52+
return fmt.Sprintf(
53+
"missing transaction auxiliary data hash: metadata provided with hash %x but body has no hash",
54+
e.Hash,
55+
)
56+
}
57+
58+
type ConflictingMetadataHashError struct {
59+
Supplied common.Blake2b256
60+
Expected common.Blake2b256
61+
}
62+
63+
func (e ConflictingMetadataHashError) Error() string {
64+
return fmt.Sprintf(
65+
"conflicting metadata hash: body declares %x, actual metadata hash is %x",
66+
e.Supplied,
67+
e.Expected,
68+
)
69+
}

ledger/allegra/rules.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
)
2323

2424
var UtxoValidationRules = []common.UtxoValidationRuleFunc{
25+
UtxoValidateMetadata,
2526
UtxoValidateOutsideValidityIntervalUtxo,
2627
UtxoValidateInputSetEmptyUtxo,
2728
UtxoValidateFeeTooSmallUtxo,
@@ -167,3 +168,50 @@ func UtxoValidateMaxTxSizeUtxo(
167168
tmpPparams,
168169
)
169170
}
171+
172+
// UtxoValidateMetadata validates that auxiliary data (metadata) matches the hash in transaction body
173+
// This is a cheap structural check that should run before expensive ledger lookups
174+
func UtxoValidateMetadata(
175+
tx common.Transaction,
176+
slot uint64,
177+
ls common.LedgerState,
178+
pp common.ProtocolParameters,
179+
) error {
180+
bodyAuxDataHash := tx.AuxDataHash()
181+
txAuxData := tx.Metadata()
182+
rawAuxData := tx.RawAuxiliaryData()
183+
184+
// Case 1: Neither body hash nor aux data present - OK
185+
if bodyAuxDataHash == nil && txAuxData == nil && len(rawAuxData) == 0 {
186+
return nil
187+
}
188+
189+
// Case 2: Body has hash but no aux data provided - error
190+
if bodyAuxDataHash != nil && txAuxData == nil && len(rawAuxData) == 0 {
191+
return MissingTransactionMetadataError{
192+
Hash: *bodyAuxDataHash,
193+
}
194+
}
195+
196+
// Case 3: Aux data provided but body has no hash - error
197+
if bodyAuxDataHash == nil && len(rawAuxData) > 0 {
198+
actualHash := common.Blake2b256Hash(rawAuxData)
199+
return MissingTransactionAuxiliaryDataHashError{
200+
Hash: actualHash,
201+
}
202+
}
203+
204+
// Case 4: Both present - verify hash matches
205+
// Use raw auxiliary data (includes scripts) for hashing, not just metadata
206+
if bodyAuxDataHash != nil && len(rawAuxData) > 0 {
207+
actualHash := common.Blake2b256Hash(rawAuxData)
208+
if *bodyAuxDataHash != actualHash {
209+
return ConflictingMetadataHashError{
210+
Supplied: *bodyAuxDataHash,
211+
Expected: actualHash,
212+
}
213+
}
214+
}
215+
216+
return nil
217+
}

ledger/alonzo/alonzo.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,7 @@ type AlonzoTransaction struct {
655655
WitnessSet AlonzoTransactionWitnessSet
656656
TxIsValid bool
657657
TxMetadata common.TransactionMetadatum
658+
RawAuxData []byte // Raw auxiliary data bytes (includes scripts)
658659
}
659660

660661
func (t *AlonzoTransaction) UnmarshalCBOR(cborData []byte) error {
@@ -688,8 +689,8 @@ func (t *AlonzoTransaction) UnmarshalCBOR(cborData []byte) error {
688689
}
689690

690691
// Handle metadata (component 4, always present - either data or CBOR nil)
691-
// DecodeAuxiliaryDataToMetadata already preserves raw bytes via DecodeMetadatumRaw
692-
if len(txArray) > 3 && len(txArray[3]) > 0 {
692+
if len(txArray) > 3 && len(txArray[3]) > 0 && txArray[3][0] != 0xF6 { // 0xF6 is CBOR null
693+
t.RawAuxData = []byte(txArray[3])
693694
metadata, err := common.DecodeAuxiliaryDataToMetadata(txArray[3])
694695
if err == nil && metadata != nil {
695696
t.TxMetadata = metadata
@@ -704,6 +705,10 @@ func (t *AlonzoTransaction) Metadata() common.TransactionMetadatum {
704705
return t.TxMetadata
705706
}
706707

708+
func (t *AlonzoTransaction) RawAuxiliaryData() []byte {
709+
return t.RawAuxData
710+
}
711+
707712
func (AlonzoTransaction) Type() int {
708713
return TxTypeAlonzo
709714
}

ledger/alonzo/errors.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,38 @@ type NoCollateralInputsError struct{}
6464
func (NoCollateralInputsError) Error() string {
6565
return "no collateral inputs"
6666
}
67+
68+
type MissingTransactionMetadataError struct {
69+
Hash common.Blake2b256
70+
}
71+
72+
func (e MissingTransactionMetadataError) Error() string {
73+
return fmt.Sprintf(
74+
"missing transaction metadata: body declares hash %x but no metadata provided",
75+
e.Hash,
76+
)
77+
}
78+
79+
type MissingTransactionAuxiliaryDataHashError struct {
80+
Hash common.Blake2b256
81+
}
82+
83+
func (e MissingTransactionAuxiliaryDataHashError) Error() string {
84+
return fmt.Sprintf(
85+
"missing transaction auxiliary data hash: metadata provided with hash %x but body has no hash",
86+
e.Hash,
87+
)
88+
}
89+
90+
type ConflictingMetadataHashError struct {
91+
Supplied common.Blake2b256
92+
Expected common.Blake2b256
93+
}
94+
95+
func (e ConflictingMetadataHashError) Error() string {
96+
return fmt.Sprintf(
97+
"conflicting metadata hash: body declares %x, actual metadata hash is %x",
98+
e.Supplied,
99+
e.Expected,
100+
)
101+
}

ledger/alonzo/rules.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
)
2626

2727
var UtxoValidationRules = []common.UtxoValidationRuleFunc{
28+
UtxoValidateMetadata,
2829
UtxoValidateOutsideValidityIntervalUtxo,
2930
UtxoValidateInputSetEmptyUtxo,
3031
UtxoValidateFeeTooSmallUtxo,
@@ -428,3 +429,48 @@ func MinCoinTxOut(
428429
minCoinTxOut := uint64(tmpPparams.MinUtxoValue)
429430
return minCoinTxOut, nil
430431
}
432+
433+
// UtxoValidateMetadata validates that auxiliary data (metadata) matches the hash in transaction body
434+
// This is a cheap structural check that should run before expensive ledger lookups
435+
func UtxoValidateMetadata(
436+
tx common.Transaction,
437+
slot uint64,
438+
ls common.LedgerState,
439+
pp common.ProtocolParameters,
440+
) error {
441+
bodyAuxDataHash := tx.AuxDataHash()
442+
txAuxData := tx.Metadata()
443+
444+
// Case 1: Neither body hash nor aux data present - OK
445+
if bodyAuxDataHash == nil && txAuxData == nil {
446+
return nil
447+
}
448+
449+
// Case 2: Body has hash but no aux data provided - error
450+
if bodyAuxDataHash != nil && txAuxData == nil {
451+
return MissingTransactionMetadataError{
452+
Hash: *bodyAuxDataHash,
453+
}
454+
}
455+
456+
// Case 3: Aux data provided but body has no hash - error
457+
if bodyAuxDataHash == nil && txAuxData != nil {
458+
actualHash := common.Blake2b256Hash(txAuxData.Cbor())
459+
return MissingTransactionAuxiliaryDataHashError{
460+
Hash: actualHash,
461+
}
462+
}
463+
464+
// Case 4: Both present - verify hash matches
465+
if bodyAuxDataHash != nil && txAuxData != nil {
466+
actualHash := common.Blake2b256Hash(txAuxData.Cbor())
467+
if *bodyAuxDataHash != actualHash {
468+
return ConflictingMetadataHashError{
469+
Supplied: *bodyAuxDataHash,
470+
Expected: actualHash,
471+
}
472+
}
473+
}
474+
475+
return nil
476+
}

ledger/babbage/babbage.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,7 @@ type BabbageTransaction struct {
886886
WitnessSet BabbageTransactionWitnessSet
887887
TxIsValid bool
888888
TxMetadata common.TransactionMetadatum
889+
RawAuxData []byte // Raw auxiliary data bytes (includes scripts)
889890
}
890891

891892
func (t *BabbageTransaction) UnmarshalCBOR(cborData []byte) error {
@@ -919,8 +920,8 @@ func (t *BabbageTransaction) UnmarshalCBOR(cborData []byte) error {
919920
}
920921

921922
// Handle metadata (component 4, always present - either data or CBOR nil)
922-
// DecodeAuxiliaryDataToMetadata already preserves raw bytes via DecodeMetadatumRaw
923-
if len(txArray[3]) > 0 {
923+
if len(txArray[3]) > 0 && txArray[3][0] != 0xF6 { // 0xF6 is CBOR null
924+
t.RawAuxData = []byte(txArray[3])
924925
metadata, err := common.DecodeAuxiliaryDataToMetadata(txArray[3])
925926
if err == nil && metadata != nil {
926927
t.TxMetadata = metadata
@@ -935,6 +936,10 @@ func (t *BabbageTransaction) Metadata() common.TransactionMetadatum {
935936
return t.TxMetadata
936937
}
937938

939+
func (t *BabbageTransaction) RawAuxiliaryData() []byte {
940+
return t.RawAuxData
941+
}
942+
938943
func (BabbageTransaction) Type() int {
939944
return TxTypeBabbage
940945
}

ledger/babbage/errors.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package babbage
1616

1717
import (
1818
"fmt"
19+
20+
"github.com/blinklabs-io/gouroboros/ledger/common"
1921
)
2022

2123
type TooManyCollateralInputsError struct {
@@ -43,3 +45,38 @@ func (e IncorrectTotalCollateralFieldError) Error() string {
4345
e.TotalCollateral,
4446
)
4547
}
48+
49+
type MissingTransactionMetadataError struct {
50+
Hash common.Blake2b256
51+
}
52+
53+
func (e MissingTransactionMetadataError) Error() string {
54+
return fmt.Sprintf(
55+
"missing transaction metadata: body declares hash %x but no metadata provided",
56+
e.Hash,
57+
)
58+
}
59+
60+
type MissingTransactionAuxiliaryDataHashError struct {
61+
Hash common.Blake2b256
62+
}
63+
64+
func (e MissingTransactionAuxiliaryDataHashError) Error() string {
65+
return fmt.Sprintf(
66+
"missing transaction auxiliary data hash: metadata provided with hash %x but body has no hash",
67+
e.Hash,
68+
)
69+
}
70+
71+
type ConflictingMetadataHashError struct {
72+
Supplied common.Blake2b256
73+
Expected common.Blake2b256
74+
}
75+
76+
func (e ConflictingMetadataHashError) Error() string {
77+
return fmt.Sprintf(
78+
"conflicting metadata hash: body declares %x, actual metadata hash is %x",
79+
e.Supplied,
80+
e.Expected,
81+
)
82+
}

ledger/babbage/rules.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
)
2727

2828
var UtxoValidationRules = []common.UtxoValidationRuleFunc{
29+
UtxoValidateMetadata,
2930
UtxoValidateOutsideValidityIntervalUtxo,
3031
UtxoValidateInputSetEmptyUtxo,
3132
UtxoValidateFeeTooSmallUtxo,
@@ -499,3 +500,48 @@ func MinCoinTxOut(
499500
minCoinTxOut := tmpPparams.AdaPerUtxoByte * uint64(len(txOutBytes))
500501
return minCoinTxOut, nil
501502
}
503+
504+
// UtxoValidateMetadata validates that auxiliary data (metadata) matches the hash in transaction body
505+
// This is a cheap structural check that should run before expensive ledger lookups
506+
func UtxoValidateMetadata(
507+
tx common.Transaction,
508+
slot uint64,
509+
ls common.LedgerState,
510+
pp common.ProtocolParameters,
511+
) error {
512+
bodyAuxDataHash := tx.AuxDataHash()
513+
txAuxData := tx.Metadata()
514+
515+
// Case 1: Neither body hash nor aux data present - OK
516+
if bodyAuxDataHash == nil && txAuxData == nil {
517+
return nil
518+
}
519+
520+
// Case 2: Body has hash but no aux data provided - error
521+
if bodyAuxDataHash != nil && txAuxData == nil {
522+
return MissingTransactionMetadataError{
523+
Hash: *bodyAuxDataHash,
524+
}
525+
}
526+
527+
// Case 3: Aux data provided but body has no hash - error
528+
if bodyAuxDataHash == nil && txAuxData != nil {
529+
actualHash := common.Blake2b256Hash(txAuxData.Cbor())
530+
return MissingTransactionAuxiliaryDataHashError{
531+
Hash: actualHash,
532+
}
533+
}
534+
535+
// Case 4: Both present - verify hash matches
536+
if bodyAuxDataHash != nil && txAuxData != nil {
537+
actualHash := common.Blake2b256Hash(txAuxData.Cbor())
538+
if *bodyAuxDataHash != actualHash {
539+
return ConflictingMetadataHashError{
540+
Supplied: *bodyAuxDataHash,
541+
Expected: actualHash,
542+
}
543+
}
544+
}
545+
546+
return nil
547+
}

0 commit comments

Comments
 (0)