Skip to content

Commit d308be0

Browse files
authored
fix: use correct TX size calc for min-fee in Alonzo and later (#967)
* subtract 1 byte from TX size in Alonzo+ when calculating fee * add error handling around CBOR encode that should "never fail" * rename 'IsTxValid' to 'TxIsValid' for consistency * change nil check to length check Signed-off-by: Aurora Gaffney <[email protected]>
1 parent d60aa9d commit d308be0

File tree

12 files changed

+96
-33
lines changed

12 files changed

+96
-33
lines changed

ledger/allegra/allegra.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,10 @@ func (t *AllegraTransaction) Cbor() []byte {
318318
tmpObj = append(tmpObj, nil)
319319
}
320320
// This should never fail, since we're only encoding a list and a bool value
321-
cborData, _ = cbor.Encode(&tmpObj)
321+
cborData, err := cbor.Encode(&tmpObj)
322+
if err != nil {
323+
panic("CBOR encoding that should never fail has failed: " + err.Error())
324+
}
322325
return cborData
323326
}
324327

ledger/alonzo/alonzo.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func (b *AlonzoBlock) Transactions() []common.Transaction {
109109
Body: b.TransactionBodies[idx],
110110
WitnessSet: b.TransactionWitnessSets[idx],
111111
TxMetadata: b.TransactionMetadataSet[uint(idx)],
112-
IsTxValid: !invalidTxMap[uint(idx)],
112+
TxIsValid: !invalidTxMap[uint(idx)],
113113
}
114114
}
115115
return ret
@@ -358,7 +358,7 @@ type AlonzoTransaction struct {
358358
cbor.DecodeStoreCbor
359359
Body AlonzoTransactionBody
360360
WitnessSet AlonzoTransactionWitnessSet
361-
IsTxValid bool
361+
TxIsValid bool
362362
TxMetadata *cbor.LazyValue
363363
}
364364

@@ -459,7 +459,7 @@ func (t AlonzoTransaction) Metadata() *cbor.LazyValue {
459459
}
460460

461461
func (t AlonzoTransaction) IsValid() bool {
462-
return t.IsTxValid
462+
return t.TxIsValid
463463
}
464464

465465
func (t AlonzoTransaction) Consumed() []common.TransactionInput {
@@ -496,7 +496,7 @@ func (t AlonzoTransaction) Witnesses() common.TransactionWitnessSet {
496496
func (t *AlonzoTransaction) Cbor() []byte {
497497
// Return stored CBOR if we have any
498498
cborData := t.DecodeStoreCbor.Cbor()
499-
if cborData != nil {
499+
if len(cborData) > 0 {
500500
return cborData[:]
501501
}
502502
// Return immediately if the body CBOR is also empty, which implies an empty TX object
@@ -508,15 +508,18 @@ func (t *AlonzoTransaction) Cbor() []byte {
508508
tmpObj := []any{
509509
cbor.RawMessage(t.Body.Cbor()),
510510
cbor.RawMessage(t.WitnessSet.Cbor()),
511-
t.IsValid,
511+
t.TxIsValid,
512512
}
513513
if t.TxMetadata != nil {
514514
tmpObj = append(tmpObj, cbor.RawMessage(t.TxMetadata.Cbor()))
515515
} else {
516516
tmpObj = append(tmpObj, nil)
517517
}
518518
// This should never fail, since we're only encoding a list and a bool value
519-
cborData, _ = cbor.Encode(&tmpObj)
519+
cborData, err := cbor.Encode(&tmpObj)
520+
if err != nil {
521+
panic("CBOR encoding that should never fail has failed: " + err.Error())
522+
}
520523
return cborData
521524
}
522525

ledger/alonzo/rules.go

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,16 +133,17 @@ func UtxoValidateFeeTooSmallUtxo(
133133
ls common.LedgerState,
134134
pp common.ProtocolParameters,
135135
) error {
136-
tmpPparams, ok := pp.(*AlonzoProtocolParameters)
137-
if !ok {
138-
return errors.New("pparams are not expected type")
136+
minFee, err := MinFeeTx(tx, pp)
137+
if err != nil {
138+
return err
139+
}
140+
if tx.Fee() >= minFee {
141+
return nil
142+
}
143+
return shelley.FeeTooSmallUtxoError{
144+
Provided: tx.Fee(),
145+
Min: minFee,
139146
}
140-
return shelley.UtxoValidateFeeTooSmallUtxo(
141-
tx,
142-
slot,
143-
ls,
144-
&tmpPparams.ShelleyProtocolParameters,
145-
)
146147
}
147148

148149
// UtxoValidateInsufficientCollateral ensures that there is sufficient collateral provided
@@ -328,3 +329,32 @@ func UtxoValidateMaxTxSizeUtxo(
328329
&tmpPparams.ShelleyProtocolParameters,
329330
)
330331
}
332+
333+
// MinFeeTx calculates the minimum required fee for a transaction based on protocol parameters
334+
func MinFeeTx(
335+
tx common.Transaction,
336+
pparams common.ProtocolParameters,
337+
) (uint64, error) {
338+
tmpPparams, ok := pparams.(*AlonzoProtocolParameters)
339+
if !ok {
340+
return 0, errors.New("pparams are not expected type")
341+
}
342+
txBytes := tx.Cbor()
343+
if len(txBytes) == 0 {
344+
var err error
345+
txBytes, err = cbor.Encode(tx)
346+
if err != nil {
347+
return 0, err
348+
}
349+
}
350+
// We calculate fee based on the pre-Alonzo TX format, which does not include the 'valid' field
351+
// Instead of having a separate helper to build the CBOR in the custom format, we subtract 1 since
352+
// a boolean is always represented by a single byte in CBOR
353+
txSize := max(0, len(txBytes)-1)
354+
minFee := uint64(
355+
// The TX size can never be negative, so casting to uint is safe
356+
//nolint:gosec
357+
(tmpPparams.MinFeeA * uint(txSize)) + tmpPparams.MinFeeB,
358+
)
359+
return minFee, nil
360+
}

ledger/alonzo/rules_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,8 @@ func TestUtxoValidateFeeTooSmallUtxo(t *testing.T) {
208208
var testExactFee uint64 = 74
209209
var testBelowFee uint64 = 73
210210
var testAboveFee uint64 = 75
211-
testTxCbor, _ := hex.DecodeString("abcdef")
211+
// NOTE: this is length 4, but 3 will be used in the calculations
212+
testTxCbor, _ := hex.DecodeString("abcdef01")
212213
testTx := &alonzo.AlonzoTransaction{
213214
Body: alonzo.AlonzoTransactionBody{
214215
MaryTransactionBody: mary.MaryTransactionBody{

ledger/babbage/babbage.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func (b *BabbageBlock) Transactions() []common.Transaction {
111111
Body: b.TransactionBodies[idx],
112112
WitnessSet: b.TransactionWitnessSets[idx],
113113
TxMetadata: b.TransactionMetadataSet[uint(idx)],
114-
IsTxValid: !invalidTxMap[uint(idx)],
114+
TxIsValid: !invalidTxMap[uint(idx)],
115115
}
116116
}
117117
return ret
@@ -511,7 +511,7 @@ type BabbageTransaction struct {
511511
cbor.DecodeStoreCbor
512512
Body BabbageTransactionBody
513513
WitnessSet BabbageTransactionWitnessSet
514-
IsTxValid bool
514+
TxIsValid bool
515515
TxMetadata *cbor.LazyValue
516516
}
517517

@@ -612,7 +612,7 @@ func (t BabbageTransaction) Metadata() *cbor.LazyValue {
612612
}
613613

614614
func (t BabbageTransaction) IsValid() bool {
615-
return t.IsTxValid
615+
return t.TxIsValid
616616
}
617617

618618
func (t BabbageTransaction) Consumed() []common.TransactionInput {
@@ -668,15 +668,18 @@ func (t *BabbageTransaction) Cbor() []byte {
668668
tmpObj := []any{
669669
cbor.RawMessage(t.Body.Cbor()),
670670
cbor.RawMessage(t.WitnessSet.Cbor()),
671-
t.IsValid,
671+
t.TxIsValid,
672672
}
673673
if t.TxMetadata != nil {
674674
tmpObj = append(tmpObj, cbor.RawMessage(t.TxMetadata.Cbor()))
675675
} else {
676676
tmpObj = append(tmpObj, nil)
677677
}
678678
// This should never fail, since we're only encoding a list and a bool value
679-
cborData, _ = cbor.Encode(&tmpObj)
679+
cborData, err := cbor.Encode(&tmpObj)
680+
if err != nil {
681+
panic("CBOR encoding that should never fail has failed: " + err.Error())
682+
}
680683
return cborData
681684
}
682685

ledger/babbage/rules.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,14 @@ func MinFeeTx(
463463
return 0, err
464464
}
465465
}
466+
// We calculate fee based on the pre-Alonzo TX format, which does not include the 'valid' field
467+
// Instead of having a separate helper to build the CBOR in the custom format, we subtract 1 since
468+
// a boolean is always represented by a single byte in CBOR
469+
txSize := max(0, len(txBytes)-1)
466470
minFee := uint64(
467-
(tmpPparams.MinFeeA * uint(len(txBytes))) + tmpPparams.MinFeeB,
471+
// The TX size can never be negative, so casting to uint is safe
472+
//nolint:gosec
473+
(tmpPparams.MinFeeA * uint(txSize)) + tmpPparams.MinFeeB,
468474
)
469475
return minFee, nil
470476
}

ledger/babbage/rules_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,8 @@ func TestUtxoValidateFeeTooSmallUtxo(t *testing.T) {
213213
var testExactFee uint64 = 74
214214
var testBelowFee uint64 = 73
215215
var testAboveFee uint64 = 75
216-
testTxCbor, _ := hex.DecodeString("abcdef")
216+
// NOTE: this is length 4, but 3 will be used in the calculations
217+
testTxCbor, _ := hex.DecodeString("abcdef01")
217218
testTx := &babbage.BabbageTransaction{
218219
Body: babbage.BabbageTransactionBody{
219220
AlonzoTransactionBody: alonzo.AlonzoTransactionBody{

ledger/conway/conway.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func (b *ConwayBlock) Transactions() []common.Transaction {
109109
Body: b.TransactionBodies[idx],
110110
WitnessSet: b.TransactionWitnessSets[idx],
111111
TxMetadata: b.TransactionMetadataSet[uint(idx)],
112-
IsTxValid: !invalidTxMap[uint(idx)],
112+
TxIsValid: !invalidTxMap[uint(idx)],
113113
}
114114
}
115115
return ret
@@ -313,7 +313,7 @@ type ConwayTransaction struct {
313313
cbor.DecodeStoreCbor
314314
Body ConwayTransactionBody
315315
WitnessSet ConwayTransactionWitnessSet
316-
IsTxValid bool
316+
TxIsValid bool
317317
TxMetadata *cbor.LazyValue
318318
}
319319

@@ -414,7 +414,7 @@ func (t ConwayTransaction) Metadata() *cbor.LazyValue {
414414
}
415415

416416
func (t ConwayTransaction) IsValid() bool {
417-
return t.IsTxValid
417+
return t.TxIsValid
418418
}
419419

420420
func (t ConwayTransaction) Consumed() []common.TransactionInput {
@@ -470,15 +470,18 @@ func (t *ConwayTransaction) Cbor() []byte {
470470
tmpObj := []any{
471471
cbor.RawMessage(t.Body.Cbor()),
472472
cbor.RawMessage(t.WitnessSet.Cbor()),
473-
t.IsValid,
473+
t.TxIsValid,
474474
}
475475
if t.TxMetadata != nil {
476476
tmpObj = append(tmpObj, cbor.RawMessage(t.TxMetadata.Cbor()))
477477
} else {
478478
tmpObj = append(tmpObj, nil)
479479
}
480480
// This should never fail, since we're only encoding a list and a bool value
481-
cborData, _ = cbor.Encode(&tmpObj)
481+
cborData, err := cbor.Encode(&tmpObj)
482+
if err != nil {
483+
panic("CBOR encoding that should never fail has failed: " + err.Error())
484+
}
482485
return cborData
483486
}
484487

ledger/conway/rules.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,8 +464,14 @@ func MinFeeTx(
464464
return 0, err
465465
}
466466
}
467+
// We calculate fee based on the pre-Alonzo TX format, which does not include the 'valid' field
468+
// Instead of having a separate helper to build the CBOR in the custom format, we subtract 1 since
469+
// a boolean is always represented by a single byte in CBOR
470+
txSize := max(0, len(txBytes)-1)
467471
minFee := uint64(
468-
(tmpPparams.MinFeeA * uint(len(txBytes))) + tmpPparams.MinFeeB,
472+
// The TX size can never be negative, so casting to uint is safe
473+
//nolint:gosec
474+
(tmpPparams.MinFeeA * uint(txSize)) + tmpPparams.MinFeeB,
469475
)
470476
return minFee, nil
471477
}

ledger/conway/rules_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,8 @@ func TestUtxoValidateFeeTooSmallUtxo(t *testing.T) {
208208
var testExactFee uint64 = 74
209209
var testBelowFee uint64 = 73
210210
var testAboveFee uint64 = 75
211-
testTxCbor, _ := hex.DecodeString("abcdef")
211+
// NOTE: this is length 4, but 3 will be used in the calculations
212+
testTxCbor, _ := hex.DecodeString("abcdef01")
212213
testTx := &conway.ConwayTransaction{
213214
Body: conway.ConwayTransactionBody{
214215
BabbageTransactionBody: babbage.BabbageTransactionBody{

0 commit comments

Comments
 (0)