diff --git a/ledger/allegra/errors.go b/ledger/allegra/errors.go new file mode 100644 index 00000000..f91168d2 --- /dev/null +++ b/ledger/allegra/errors.go @@ -0,0 +1,32 @@ +// Copyright 2025 Blink Labs Software +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package allegra + +import ( + "fmt" +) + +type OutsideValidityIntervalUtxoError struct { + ValidityIntervalStart uint64 + Slot uint64 +} + +func (e OutsideValidityIntervalUtxoError) Error() string { + return fmt.Sprintf( + "outside validity interval: start %d, slot %d", + e.ValidityIntervalStart, + e.Slot, + ) +} diff --git a/ledger/allegra/rules.go b/ledger/allegra/rules.go new file mode 100644 index 00000000..96831e18 --- /dev/null +++ b/ledger/allegra/rules.go @@ -0,0 +1,99 @@ +// Copyright 2025 Blink Labs Software +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package allegra + +import ( + "fmt" + + "github.com/blinklabs-io/gouroboros/ledger/common" + "github.com/blinklabs-io/gouroboros/ledger/shelley" +) + +var UtxoValidationRules = []common.UtxoValidationRuleFunc{ + UtxoValidateOutsideValidityIntervalUtxo, + UtxoValidateInputSetEmptyUtxo, + UtxoValidateFeeTooSmallUtxo, + UtxoValidateBadInputsUtxo, + UtxoValidateWrongNetwork, + UtxoValidateWrongNetworkWithdrawal, + UtxoValidateValueNotConservedUtxo, + UtxoValidateOutputTooSmallUtxo, + UtxoValidateOutputBootAddrAttrsTooBig, + UtxoValidateMaxTxSizeUtxo, +} + +// UtxoValidateOutsideValidityIntervalUtxo ensures that the current tip slot has reached the specified validity interval +func UtxoValidateOutsideValidityIntervalUtxo(tx common.Transaction, slot uint64, _ common.LedgerState, _ common.ProtocolParameters) error { + validityIntervalStart := tx.ValidityIntervalStart() + if validityIntervalStart == 0 || slot >= validityIntervalStart { + return nil + } + return OutsideValidityIntervalUtxoError{ + ValidityIntervalStart: validityIntervalStart, + Slot: slot, + } +} + +func UtxoValidateInputSetEmptyUtxo(tx common.Transaction, slot uint64, ls common.LedgerState, pp common.ProtocolParameters) error { + return shelley.UtxoValidateInputSetEmptyUtxo(tx, slot, ls, pp) +} + +func UtxoValidateFeeTooSmallUtxo(tx common.Transaction, slot uint64, ls common.LedgerState, pp common.ProtocolParameters) error { + tmpPparams, ok := pp.(*AllegraProtocolParameters) + if !ok { + return fmt.Errorf("pparams are not expected type") + } + return shelley.UtxoValidateFeeTooSmallUtxo(tx, slot, ls, &tmpPparams.ShelleyProtocolParameters) +} + +func UtxoValidateBadInputsUtxo(tx common.Transaction, slot uint64, ls common.LedgerState, pp common.ProtocolParameters) error { + return shelley.UtxoValidateBadInputsUtxo(tx, slot, ls, pp) +} + +func UtxoValidateWrongNetwork(tx common.Transaction, slot uint64, ls common.LedgerState, pp common.ProtocolParameters) error { + return shelley.UtxoValidateWrongNetwork(tx, slot, ls, pp) +} + +func UtxoValidateWrongNetworkWithdrawal(tx common.Transaction, slot uint64, ls common.LedgerState, pp common.ProtocolParameters) error { + return shelley.UtxoValidateWrongNetworkWithdrawal(tx, slot, ls, pp) +} + +func UtxoValidateValueNotConservedUtxo(tx common.Transaction, slot uint64, ls common.LedgerState, pp common.ProtocolParameters) error { + tmpPparams, ok := pp.(*AllegraProtocolParameters) + if !ok { + return fmt.Errorf("pparams are not expected type") + } + return shelley.UtxoValidateValueNotConservedUtxo(tx, slot, ls, &tmpPparams.ShelleyProtocolParameters) +} + +func UtxoValidateOutputTooSmallUtxo(tx common.Transaction, slot uint64, ls common.LedgerState, pp common.ProtocolParameters) error { + tmpPparams, ok := pp.(*AllegraProtocolParameters) + if !ok { + return fmt.Errorf("pparams are not expected type") + } + return shelley.UtxoValidateOutputTooSmallUtxo(tx, slot, ls, &tmpPparams.ShelleyProtocolParameters) +} + +func UtxoValidateOutputBootAddrAttrsTooBig(tx common.Transaction, slot uint64, ls common.LedgerState, pp common.ProtocolParameters) error { + return shelley.UtxoValidateOutputBootAddrAttrsTooBig(tx, slot, ls, pp) +} + +func UtxoValidateMaxTxSizeUtxo(tx common.Transaction, slot uint64, ls common.LedgerState, pp common.ProtocolParameters) error { + tmpPparams, ok := pp.(*AllegraProtocolParameters) + if !ok { + return fmt.Errorf("pparams are not expected type") + } + return shelley.UtxoValidateMaxTxSizeUtxo(tx, slot, ls, &tmpPparams.ShelleyProtocolParameters) +} diff --git a/ledger/allegra/rules_test.go b/ledger/allegra/rules_test.go new file mode 100644 index 00000000..4121161d --- /dev/null +++ b/ledger/allegra/rules_test.go @@ -0,0 +1,833 @@ +// Copyright 2025 Blink Labs Software +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package allegra_test + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "testing" + + "github.com/blinklabs-io/gouroboros/ledger/allegra" + "github.com/blinklabs-io/gouroboros/ledger/common" + "github.com/blinklabs-io/gouroboros/ledger/shelley" + + "github.com/stretchr/testify/assert" +) + +type testLedgerState struct { + networkId uint + utxos []common.Utxo +} + +func (ls testLedgerState) NetworkId() uint { + return ls.networkId +} + +func (ls testLedgerState) UtxoById(id common.TransactionInput) (common.Utxo, error) { + for _, tmpUtxo := range ls.utxos { + if id.Index() != tmpUtxo.Id.Index() { + continue + } + if string(id.Id().Bytes()) != string(tmpUtxo.Id.Id().Bytes()) { + continue + } + return tmpUtxo, nil + } + return common.Utxo{}, fmt.Errorf("not found") +} + +func TestUtxoValidateOutsideValidityIntervalUtxo(t *testing.T) { + var testSlot uint64 = 555666777 + var testZeroSlot uint64 = 0 + testTx := &allegra.AllegraTransaction{ + Body: allegra.AllegraTransactionBody{ + TxValidityIntervalStart: testSlot, + }, + } + testLedgerState := testLedgerState{} + testProtocolParams := &allegra.AllegraProtocolParameters{} + var testBeforeSlot uint64 = 555666700 + var testAfterSlot uint64 = 555666799 + // Test helper function + testRun := func(t *testing.T, name string, testSlot uint64, validateFunc func(*testing.T, error)) { + t.Run( + name, + func(t *testing.T) { + err := allegra.UtxoValidateOutsideValidityIntervalUtxo( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + validateFunc(t, err) + }, + ) + } + // Slot after validity interval start + testRun( + t, + "slot after validity interval start", + testAfterSlot, + func(t *testing.T, err error) { + if err != nil { + t.Errorf( + "UtxoValidateOutsideValidityIntervalUtxo should succeed when provided a slot (%d) after the specified validity interval start (%d)\n got error: %v", + testAfterSlot, + testTx.ValidityIntervalStart(), + err, + ) + } + }, + ) + // Slot equal to validity interval start + testRun( + t, + "slot equal to validity interval start", + testSlot, + func(t *testing.T, err error) { + if err != nil { + t.Errorf( + "UtxoValidateOutsideValidityIntervalUtxo should succeed when provided a slot (%d) equal to the specified validity interval start (%d)\n got error: %v", + testSlot, + testTx.ValidityIntervalStart(), + err, + ) + } + }, + ) + // Slot before validity interval start + testRun( + t, + "slot before validity interval start", + testBeforeSlot, + func(t *testing.T, err error) { + if err == nil { + t.Errorf( + "UtxoValidateOutsideValidityIntervalUtxo should fail when provided a slot (%d) before the specified validity interval start (%d)", + testBeforeSlot, + testTx.ValidityIntervalStart(), + ) + return + } + testErrType := allegra.OutsideValidityIntervalUtxoError{} + assert.IsType( + t, + testErrType, + err, + "did not get expected error type: got %T, wanted %T", + err, + testErrType, + ) + }, + ) + // Zero TTL + testTx.Body.TxValidityIntervalStart = testZeroSlot + testRun( + t, + "zero validity interval start", + testSlot, + func(t *testing.T, err error) { + if err != nil { + t.Errorf( + "UtxoValidateOutsideValidityIntervalUtxo should succeed when provided a zero validity interval start\n got error: %v", + err, + ) + } + }, + ) +} + +func TestUtxoValidateInputSetEmptyUtxo(t *testing.T) { + testTx := &allegra.AllegraTransaction{ + Body: allegra.AllegraTransactionBody{ + ShelleyTransactionBody: shelley.ShelleyTransactionBody{ + TxInputs: shelley.NewShelleyTransactionInputSet( + // Non-empty input set + []shelley.ShelleyTransactionInput{ + {}, + }, + ), + }, + }, + } + testLedgerState := testLedgerState{} + testSlot := uint64(0) + testProtocolParams := &allegra.AllegraProtocolParameters{} + // Non-empty + t.Run( + "non-empty input set", + func(t *testing.T) { + err := allegra.UtxoValidateInputSetEmptyUtxo( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err != nil { + t.Errorf( + "UtxoValidateInputSetEmptyUtxo should succeed when provided a non-empty input set\n got error: %v", + err, + ) + } + }, + ) + // Empty + testTx.Body.TxInputs.SetItems(nil) + t.Run( + "empty input set", + func(t *testing.T) { + err := allegra.UtxoValidateInputSetEmptyUtxo( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err == nil { + t.Errorf( + "UtxoValidateInputSetEmptyUtxo should fail when provided an empty input set\n got error: %v", + err, + ) + return + } + testErrType := shelley.InputSetEmptyUtxoError{} + assert.IsType( + t, + testErrType, + err, + "did not get expected error type: got %T, wanted %T", + err, + testErrType, + ) + }, + ) +} + +func TestUtxoValidateFeeTooSmallUtxo(t *testing.T) { + var testExactFee uint64 = 74 + var testBelowFee uint64 = 73 + var testAboveFee uint64 = 75 + testTxCbor, _ := hex.DecodeString("abcdef") + testTx := &allegra.AllegraTransaction{ + Body: allegra.AllegraTransactionBody{ + ShelleyTransactionBody: shelley.ShelleyTransactionBody{ + TxFee: testExactFee, + }, + }, + } + testTx.SetCbor(testTxCbor) + testProtocolParams := &allegra.AllegraProtocolParameters{ + ShelleyProtocolParameters: shelley.ShelleyProtocolParameters{ + MinFeeA: 7, + MinFeeB: 53, + }, + } + testLedgerState := testLedgerState{} + testSlot := uint64(0) + // Test helper function + testRun := func(t *testing.T, name string, testFee uint64, validateFunc func(*testing.T, error)) { + t.Run( + name, + func(t *testing.T) { + tmpTestTx := testTx + tmpTestTx.Body.TxFee = testFee + err := allegra.UtxoValidateFeeTooSmallUtxo( + tmpTestTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + validateFunc(t, err) + }, + ) + } + // Fee too low + testRun( + t, + "fee too low", + testBelowFee, + func(t *testing.T, err error) { + if err == nil { + t.Errorf( + "UtxoValidateFeeTooSmallUtxo should fail when provided too low of a fee", + ) + return + } + testErrType := shelley.FeeTooSmallUtxoError{} + assert.IsType( + t, + testErrType, + err, + "did not get expected error type: got %T, wanted %T", + err, + testErrType, + ) + + }, + ) + // Exact fee + testRun( + t, + "exact fee", + testExactFee, + func(t *testing.T, err error) { + if err != nil { + t.Errorf( + "UtxoValidateFeeTooSmallUtxo should succeed when provided an exact fee\n got error: %v", + err, + ) + } + }, + ) + // Above min fee + testRun( + t, + "above min fee", + testAboveFee, + func(t *testing.T, err error) { + if err != nil { + t.Errorf( + "UtxoValidateFeeTooSmallUtxo should succeed when provided above the min fee\n got error: %v", + err, + ) + } + }, + ) +} + +func TestUtxoValidateBadInputsUtxo(t *testing.T) { + testInputTxId := "d228b482a1aae768e4a796380f49e021d9c21f70d3c12cb186b188dedfc0ee22" + testGoodInput := shelley.NewShelleyTransactionInput( + testInputTxId, + 0, + ) + testBadInput := shelley.NewShelleyTransactionInput( + testInputTxId, + 1, + ) + testTx := &allegra.AllegraTransaction{ + Body: allegra.AllegraTransactionBody{}, + } + testLedgerState := testLedgerState{ + utxos: []common.Utxo{ + { + Id: testGoodInput, + }, + }, + } + testSlot := uint64(0) + testProtocolParams := &allegra.AllegraProtocolParameters{} + // Good input + t.Run( + "good input", + func(t *testing.T) { + testTx.Body.TxInputs = shelley.NewShelleyTransactionInputSet( + []shelley.ShelleyTransactionInput{testGoodInput}, + ) + err := allegra.UtxoValidateBadInputsUtxo( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err != nil { + t.Errorf( + "UtxoValidateBadInputsUtxo should succeed when provided a good input\n got error: %v", + err, + ) + } + }, + ) + // Bad input + t.Run( + "bad input", + func(t *testing.T) { + testTx.Body.TxInputs = shelley.NewShelleyTransactionInputSet( + []shelley.ShelleyTransactionInput{testBadInput}, + ) + err := allegra.UtxoValidateBadInputsUtxo( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err == nil { + t.Errorf( + "UtxoValidateBadInputsUtxo should fail when provided a bad input", + ) + return + } + testErrType := shelley.BadInputsUtxoError{} + assert.IsType( + t, + testErrType, + err, + "did not get expected error type: got %T, wanted %T", + err, + testErrType, + ) + }, + ) +} + +func TestUtxoValidateWrongNetwork(t *testing.T) { + testCorrectNetworkAddr, _ := common.NewAddress("addr1qytna5k2fq9ler0fuk45j7zfwv7t2zwhp777nvdjqqfr5tz8ztpwnk8zq5ngetcz5k5mckgkajnygtsra9aej2h3ek5seupmvd") + testWrongNetworkAddr, _ := common.NewAddress("addr_test1qqx80sj9nwxdnglmzdl95v2k40d9422au0klwav8jz2dj985v0wma0mza32f8z6pv2jmkn7cen50f9vn9jmp7dd0njcqqpce07") + testTx := &allegra.AllegraTransaction{ + Body: allegra.AllegraTransactionBody{ + ShelleyTransactionBody: shelley.ShelleyTransactionBody{ + TxOutputs: []shelley.ShelleyTransactionOutput{ + { + OutputAmount: 123456, + }, + }, + }, + }, + } + testLedgerState := testLedgerState{ + networkId: common.AddressNetworkMainnet, + } + testSlot := uint64(0) + testProtocolParams := &allegra.AllegraProtocolParameters{} + // Correct network + t.Run( + "correct network", + func(t *testing.T) { + testTx.Body.TxOutputs[0].OutputAddress = testCorrectNetworkAddr + err := allegra.UtxoValidateBadInputsUtxo( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err != nil { + t.Errorf( + "UtxoValidateWrongNetwork should succeed when provided an address with the correct network ID\n got error: %v", + err, + ) + } + }, + ) + // Wrong network + t.Run( + "wrong network", + func(t *testing.T) { + testTx.Body.TxOutputs[0].OutputAddress = testWrongNetworkAddr + err := allegra.UtxoValidateWrongNetwork( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err == nil { + t.Errorf( + "UtxoValidateWrongNetwork should fail when provided an address with the wrong network ID", + ) + return + } + testErrType := shelley.WrongNetworkError{} + assert.IsType( + t, + testErrType, + err, + "did not get expected error type: got %T, wanted %T", + err, + testErrType, + ) + }, + ) +} + +func TestUtxoValidateWrongNetworkWithdrawal(t *testing.T) { + testCorrectNetworkAddr, _ := common.NewAddress("addr1qytna5k2fq9ler0fuk45j7zfwv7t2zwhp777nvdjqqfr5tz8ztpwnk8zq5ngetcz5k5mckgkajnygtsra9aej2h3ek5seupmvd") + testWrongNetworkAddr, _ := common.NewAddress("addr_test1qqx80sj9nwxdnglmzdl95v2k40d9422au0klwav8jz2dj985v0wma0mza32f8z6pv2jmkn7cen50f9vn9jmp7dd0njcqqpce07") + testTx := &allegra.AllegraTransaction{ + Body: allegra.AllegraTransactionBody{ + ShelleyTransactionBody: shelley.ShelleyTransactionBody{ + TxWithdrawals: map[*common.Address]uint64{}, + }, + }, + } + testLedgerState := testLedgerState{ + networkId: common.AddressNetworkMainnet, + } + testSlot := uint64(0) + testProtocolParams := &allegra.AllegraProtocolParameters{} + // Correct network + t.Run( + "correct network", + func(t *testing.T) { + testTx.Body.TxWithdrawals[&testCorrectNetworkAddr] = 123456 + err := allegra.UtxoValidateWrongNetworkWithdrawal( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err != nil { + t.Errorf( + "UtxoValidateWrongNetworkWithdrawal should succeed when provided an address with the correct network ID\n got error: %v", + err, + ) + } + }, + ) + // Wrong network + t.Run( + "wrong network", + func(t *testing.T) { + testTx.Body.TxWithdrawals[&testWrongNetworkAddr] = 123456 + err := allegra.UtxoValidateWrongNetworkWithdrawal( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err == nil { + t.Errorf( + "UtxoValidateWrongNetworkWIthdrawal should fail when provided an address with the wrong network ID", + ) + return + } + testErrType := shelley.WrongNetworkWithdrawalError{} + assert.IsType( + t, + testErrType, + err, + "did not get expected error type: got %T, wanted %T", + err, + testErrType, + ) + }, + ) +} + +func TestUtxoValidateValueNotConservedUtxo(t *testing.T) { + testInputTxId := "d228b482a1aae768e4a796380f49e021d9c21f70d3c12cb186b188dedfc0ee22" + var testInputAmount uint64 = 555666777 + var testFee uint64 = 123456 + testOutputExactAmount := testInputAmount - testFee + testOutputUnderAmount := testOutputExactAmount - 999 + testOutputOverAmount := testOutputExactAmount + 999 + testTx := &allegra.AllegraTransaction{ + Body: allegra.AllegraTransactionBody{ + ShelleyTransactionBody: shelley.ShelleyTransactionBody{ + TxFee: testFee, + TxInputs: shelley.NewShelleyTransactionInputSet( + []shelley.ShelleyTransactionInput{ + shelley.NewShelleyTransactionInput(testInputTxId, 0), + }, + ), + TxOutputs: []shelley.ShelleyTransactionOutput{ + // Empty placeholder output + {}, + }, + }, + }, + } + testLedgerState := testLedgerState{ + utxos: []common.Utxo{ + { + Id: shelley.NewShelleyTransactionInput(testInputTxId, 0), + Output: shelley.ShelleyTransactionOutput{ + OutputAmount: testInputAmount, + }, + }, + }, + } + testSlot := uint64(0) + testProtocolParams := &allegra.AllegraProtocolParameters{} + // Exact amount + t.Run( + "exact amount", + func(t *testing.T) { + testTx.Body.TxOutputs[0].OutputAmount = testOutputExactAmount + err := allegra.UtxoValidateValueNotConservedUtxo( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err != nil { + t.Errorf( + "UtxoValidateValueNotConservedUtxo should succeed when inputs and outputs are balanced\n got error: %v", + err, + ) + } + }, + ) + // Output too low + t.Run( + "output too low", + func(t *testing.T) { + testTx.Body.TxOutputs[0].OutputAmount = testOutputUnderAmount + err := allegra.UtxoValidateValueNotConservedUtxo( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err == nil { + t.Errorf( + "UtxoValidateValueNotConservedUtxo should fail when the output amount is too low", + ) + return + } + testErrType := shelley.ValueNotConservedUtxoError{} + assert.IsType( + t, + testErrType, + err, + "did not get expected error type: got %T, wanted %T", + err, + testErrType, + ) + }, + ) + // Output too high + t.Run( + "output too high", + func(t *testing.T) { + testTx.Body.TxOutputs[0].OutputAmount = testOutputOverAmount + err := allegra.UtxoValidateValueNotConservedUtxo( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err == nil { + t.Errorf( + "UtxoValidateValueNotConservedUtxo should fail when the output amount is too high", + ) + return + } + testErrType := shelley.ValueNotConservedUtxoError{} + assert.IsType( + t, + testErrType, + err, + "did not get expected error type: got %T, wanted %T", + err, + testErrType, + ) + }, + ) +} + +func TestUtxoValidateOutputTooSmallUtxo(t *testing.T) { + var testOutputAmountGood uint64 = 1234567 + var testOutputAmountBad uint64 = 123 + testTx := &allegra.AllegraTransaction{ + Body: allegra.AllegraTransactionBody{ + ShelleyTransactionBody: shelley.ShelleyTransactionBody{ + TxOutputs: []shelley.ShelleyTransactionOutput{ + // Empty placeholder output + {}, + }, + }, + }, + } + testLedgerState := testLedgerState{} + testSlot := uint64(0) + testProtocolParams := &allegra.AllegraProtocolParameters{ + ShelleyProtocolParameters: shelley.ShelleyProtocolParameters{ + MinUtxoValue: 100000, + }, + } + // Good + t.Run( + "sufficient coin", + func(t *testing.T) { + testTx.Body.TxOutputs[0].OutputAmount = testOutputAmountGood + err := allegra.UtxoValidateOutputTooSmallUtxo( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err != nil { + t.Errorf( + "UtxoValidateOutputTooSmallUtxo should succeed when outputs have sufficient coin\n got error: %v", + err, + ) + } + }, + ) + // Bad + t.Run( + "insufficient coin", + func(t *testing.T) { + testTx.Body.TxOutputs[0].OutputAmount = testOutputAmountBad + err := allegra.UtxoValidateOutputTooSmallUtxo( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err == nil { + t.Errorf( + "UtxoValidateOutputTooSmallUtxo should fail when the output amount is too low", + ) + return + } + testErrType := shelley.OutputTooSmallUtxoError{} + assert.IsType( + t, + testErrType, + err, + "did not get expected error type: got %T, wanted %T", + err, + testErrType, + ) + }, + ) +} + +func TestUtxoValidateOutputBootAddrAttrsTooBig(t *testing.T) { + testGoodAddr, _ := common.NewAddress("addr1qytna5k2fq9ler0fuk45j7zfwv7t2zwhp777nvdjqqfr5tz8ztpwnk8zq5ngetcz5k5mckgkajnygtsra9aej2h3ek5seupmvd") + // Generate random pubkey + testBadAddrPubkey := make([]byte, 28) + if _, err := rand.Read(testBadAddrPubkey); err != nil { + t.Fatalf("could not read random bytes") + } + // Generate random large attribute payload + testBadAddrAttrPayload := make([]byte, 100) + if _, err := rand.Read(testBadAddrAttrPayload); err != nil { + t.Fatalf("could not read random bytes") + } + testBadAddr, _ := common.NewByronAddressFromParts( + common.ByronAddressTypePubkey, + testBadAddrPubkey, + common.ByronAddressAttributes{ + Payload: testBadAddrAttrPayload, + }, + ) + testTx := &allegra.AllegraTransaction{ + Body: allegra.AllegraTransactionBody{ + ShelleyTransactionBody: shelley.ShelleyTransactionBody{ + TxOutputs: []shelley.ShelleyTransactionOutput{ + // Empty placeholder + {}, + }, + }, + }, + } + testLedgerState := testLedgerState{} + testSlot := uint64(0) + testProtocolParams := &allegra.AllegraProtocolParameters{} + // Good + t.Run( + "Shelley address", + func(t *testing.T) { + testTx.Body.TxOutputs[0].OutputAddress = testGoodAddr + err := allegra.UtxoValidateOutputBootAddrAttrsTooBig( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err != nil { + t.Errorf( + "UtxoValidateOutputBootAddrAttrsTooBig should succeed when outputs have sufficient coin\n got error: %v", + err, + ) + } + }, + ) + // Bad + t.Run( + "Byron address with large attribute payload", + func(t *testing.T) { + testTx.Body.TxOutputs[0].OutputAddress = testBadAddr + err := allegra.UtxoValidateOutputBootAddrAttrsTooBig( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err == nil { + t.Errorf( + "UtxoValidateOutputBootAddrAttrsTooBig should fail when the output address has large Byron attributes payload", + ) + return + } + testErrType := shelley.OutputBootAddrAttrsTooBigError{} + assert.IsType( + t, + testErrType, + err, + "did not get expected error type: got %T, wanted %T", + err, + testErrType, + ) + }, + ) +} + +func TestUtxoValidateMaxTxSizeUtxo(t *testing.T) { + var testMaxTxSizeSmall uint = 2 + var testMaxTxSizeLarge uint = 64 * 1024 + testTx := &allegra.AllegraTransaction{} + testLedgerState := testLedgerState{} + testSlot := uint64(0) + testProtocolParams := &allegra.AllegraProtocolParameters{} + // Transaction under limit + t.Run( + "transaction is under limit", + func(t *testing.T) { + testProtocolParams.MaxTxSize = testMaxTxSizeLarge + err := allegra.UtxoValidateMaxTxSizeUtxo( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err != nil { + t.Errorf( + "UtxoValidateMaxTxSizeUtxo should succeed when the TX size is under the limit\n got error: %v", + err, + ) + } + }, + ) + // Transaction too large + t.Run( + "transaction is too large", + func(t *testing.T) { + testProtocolParams.MaxTxSize = testMaxTxSizeSmall + err := allegra.UtxoValidateMaxTxSizeUtxo( + testTx, + testSlot, + testLedgerState, + testProtocolParams, + ) + if err == nil { + t.Errorf( + "UtxoValidateMaxTxSizeUtxo should fail when the TX size is too large", + ) + return + } + testErrType := shelley.MaxTxSizeUtxoError{} + assert.IsType( + t, + testErrType, + err, + "did not get expected error type: got %T, wanted %T", + err, + testErrType, + ) + }, + ) +} diff --git a/ledger/common/tx.go b/ledger/common/tx.go index e497f38a..73c662aa 100644 --- a/ledger/common/tx.go +++ b/ledger/common/tx.go @@ -23,6 +23,7 @@ import ( type Transaction interface { TransactionBody Type() int + Cbor() []byte Metadata() *cbor.LazyValue IsValid() bool Consumed() []TransactionInput diff --git a/ledger/shelley/rules.go b/ledger/shelley/rules.go index 5fe90681..002fa262 100644 --- a/ledger/shelley/rules.go +++ b/ledger/shelley/rules.go @@ -205,15 +205,11 @@ func UtxoValidateOutputBootAddrAttrsTooBig(tx common.Transaction, slot uint64, l // UtxoValidateMaxTxSizeUtxo ensures that a transaction does not exceed the max size func UtxoValidateMaxTxSizeUtxo(tx common.Transaction, slot uint64, ls common.LedgerState, pp common.ProtocolParameters) error { - tmpTx, ok := tx.(*ShelleyTransaction) - if !ok { - return fmt.Errorf("transaction is not expected type") - } tmpPparams, ok := pp.(*ShelleyProtocolParameters) if !ok { return fmt.Errorf("pparams are not expected type") } - txBytes, err := cbor.Encode(tmpTx) + txBytes, err := cbor.Encode(tx) if err != nil { return err } @@ -228,15 +224,11 @@ func UtxoValidateMaxTxSizeUtxo(tx common.Transaction, slot uint64, ls common.Led // MinFeeTx calculates the minimum required fee for a transaction based on protocol parameters func MinFeeTx(tx common.Transaction, pparams common.ProtocolParameters) (uint64, error) { - tmpTx, ok := tx.(*ShelleyTransaction) - if !ok { - return 0, fmt.Errorf("transaction is not expected type") - } tmpPparams, ok := pparams.(*ShelleyProtocolParameters) if !ok { return 0, fmt.Errorf("pparams are not expected type") } - txBytes := tmpTx.Cbor() + txBytes := tx.Cbor() minFee := uint64((tmpPparams.MinFeeA * uint(len(txBytes))) + tmpPparams.MinFeeB) return minFee, nil }