diff --git a/ledger/common/address.go b/ledger/common/address.go index cdd76ee1..93443b5b 100644 --- a/ledger/common/address.go +++ b/ledger/common/address.go @@ -276,6 +276,25 @@ func (a *Address) MarshalCBOR() ([]byte, error) { return cbor.Encode(addrBytes) } +func (a Address) NetworkId() uint { + if a.addressType == AddressTypeByron { + if a.byronAddressAttr.Network == nil { + return AddressNetworkMainnet + } + return uint(*a.byronAddressAttr.Network) + } else { + return uint(a.networkId) + } +} + +func (a Address) Type() uint8 { + return a.addressType +} + +func (a Address) ByronType() uint64 { + return a.byronAddressType +} + // PaymentAddress returns a new Address with only the payment address portion. This will return nil for anything other than payment and script addresses func (a Address) PaymentAddress() *Address { var addrType uint8 @@ -337,6 +356,10 @@ func (a *Address) StakeKeyHash() Blake2b224 { return Blake2b224(a.stakingAddress[:]) } +func (a *Address) ByronAttr() ByronAddressAttributes { + return a.byronAddressAttr +} + func (a Address) generateHRP() string { var ret string if a.addressType == AddressTypeNoneKey || diff --git a/ledger/common/state.go b/ledger/common/state.go index d46dc059..1c051afe 100644 --- a/ledger/common/state.go +++ b/ledger/common/state.go @@ -20,7 +20,7 @@ import ( // UtxoState defines the interface for querying the UTxO state type UtxoState interface { - UtxosById([]TransactionInput) ([]Utxo, error) + UtxoById(TransactionInput) (Utxo, error) } // CertState defines the interface for querying the certificate state @@ -30,6 +30,7 @@ type CertState interface{} type LedgerState interface { UtxoState CertState + NetworkId() uint } // TipState defines the interface for querying the current tip diff --git a/ledger/common/tx.go b/ledger/common/tx.go index 6edc0e6e..e497f38a 100644 --- a/ledger/common/tx.go +++ b/ledger/common/tx.go @@ -58,6 +58,7 @@ type TransactionBody interface { type TransactionInput interface { Id() Blake2b256 Index() uint32 + String() string Utxorpc() *utxorpc.TxInput } diff --git a/ledger/shelley/errors.go b/ledger/shelley/errors.go index 60e92c6c..a21457c6 100644 --- a/ledger/shelley/errors.go +++ b/ledger/shelley/errors.go @@ -14,7 +14,12 @@ package shelley -import "fmt" +import ( + "fmt" + "strings" + + "github.com/blinklabs-io/gouroboros/ledger/common" +) type ExpiredUtxoError struct { Ttl uint64 @@ -28,3 +33,125 @@ func (e ExpiredUtxoError) Error() string { e.Slot, ) } + +type InputSetEmptyUtxoError struct{} + +func (InputSetEmptyUtxoError) Error() string { + return "input set empty" +} + +type FeeTooSmallUtxoError struct { + Provided uint64 + Min uint64 +} + +func (e FeeTooSmallUtxoError) Error() string { + return fmt.Sprintf( + "fee too small: provided %d, minimum %d", + e.Provided, + e.Min, + ) +} + +type BadInputsUtxoError struct { + Inputs []common.TransactionInput +} + +func (e BadInputsUtxoError) Error() string { + tmpInputs := make([]string, 0, len(e.Inputs)) + for idx, tmpInput := range e.Inputs { + tmpInputs[idx] = tmpInput.String() + } + return fmt.Sprintf( + "bad input(s): %s", + strings.Join(tmpInputs, ", "), + ) +} + +type WrongNetworkError struct { + NetId uint + Addrs []common.Address +} + +func (e WrongNetworkError) Error() string { + tmpAddrs := make([]string, 0, len(e.Addrs)) + for idx, tmpAddr := range e.Addrs { + tmpAddrs[idx] = tmpAddr.String() + } + return fmt.Sprintf( + "wrong network: %s", + strings.Join(tmpAddrs, ", "), + ) +} + +type WrongNetworkWithdrawalError struct { + NetId uint + Addrs []common.Address +} + +func (e WrongNetworkWithdrawalError) Error() string { + tmpAddrs := make([]string, 0, len(e.Addrs)) + for idx, tmpAddr := range e.Addrs { + tmpAddrs[idx] = tmpAddr.String() + } + return fmt.Sprintf( + "wrong network withdrawals: %s", + strings.Join(tmpAddrs, ", "), + ) +} + +type ValueNotConservedUtxoError struct { + Consumed uint64 + Produced uint64 +} + +func (e ValueNotConservedUtxoError) Error() string { + return fmt.Sprintf( + "value not conserved: consumed %d, produced %d", + e.Consumed, + e.Produced, + ) +} + +type OutputTooSmallUtxoError struct { + Outputs []common.TransactionOutput +} + +func (e OutputTooSmallUtxoError) Error() string { + tmpOutputs := make([]string, 0, len(e.Outputs)) + for idx, tmpOutput := range e.Outputs { + tmpOutputs[idx] = fmt.Sprintf("%#v", tmpOutput) + } + return fmt.Sprintf( + "output too small: %s", + strings.Join(tmpOutputs, ", "), + ) +} + +type OutputBootAddrAttrsTooBigError struct { + Outputs []common.TransactionOutput +} + +func (e OutputBootAddrAttrsTooBigError) Error() string { + tmpOutputs := make([]string, 0, len(e.Outputs)) + for idx, tmpOutput := range e.Outputs { + tmpOutputs[idx] = fmt.Sprintf("%#v", tmpOutput) + } + return fmt.Sprintf( + "output bootstrap address attributes too big: %s", + strings.Join(tmpOutputs, ", "), + ) +} + +type MaxTxSizeUtxoError struct { + TxSize uint + MaxTxSize uint +} + +func (e MaxTxSizeUtxoError) Error() string { + return fmt.Sprintf( + "transaction size too large: size %d, max %d", + e.TxSize, + e.MaxTxSize, + ) +} diff --git a/ledger/shelley/rules.go b/ledger/shelley/rules.go index 0f268e8e..9ee9ab1d 100644 --- a/ledger/shelley/rules.go +++ b/ledger/shelley/rules.go @@ -15,11 +15,23 @@ package shelley import ( + "fmt" + + "github.com/blinklabs-io/gouroboros/cbor" common "github.com/blinklabs-io/gouroboros/ledger/common" ) var UtxoValidationRules = []common.UtxoValidationRuleFunc{ UtxoValidateTimeToLive, + UtxoValidateInputSetEmptyUtxo, + UtxoValidateFeeTooSmallUtxo, + UtxoValidateBadInputsUtxo, + UtxoValidateWrongNetwork, + UtxoValidateWrongNetworkWithdrawal, + UtxoValidateValueNotConservedUtxo, + UtxoValidateOutputTooSmallUtxo, + UtxoValidateOutputBootAddrAttrsTooBig, + UtxoValidateMaxTxSizeUtxo, } // UtxoValidateTimeToLive ensures that the current tip slot is not after the specified TTL value @@ -37,3 +49,208 @@ func UtxoValidateTimeToLive(tx common.Transaction, ls common.LedgerState, ts com Slot: tip.Point.Slot, } } + +// UtxoValidateInputSetEmptyUtxo ensures that the input set is not empty +func UtxoValidateInputSetEmptyUtxo(tx common.Transaction, ls common.LedgerState, ts common.TipState, pp common.ProtocolParameters) error { + if len(tx.Inputs()) > 0 { + return nil + } + return InputSetEmptyUtxoError{} +} + +// UtxoValidateFeeTooSmallUtxo ensures that the fee is at least the calculated minimum +func UtxoValidateFeeTooSmallUtxo(tx common.Transaction, ls common.LedgerState, ts common.TipState, pp common.ProtocolParameters) error { + minFee, err := MinFeeTx(tx, pp) + if err != nil { + return err + } + if tx.Fee() >= minFee { + return nil + } + return FeeTooSmallUtxoError{ + Provided: tx.Fee(), + Min: minFee, + } +} + +// UtxoValidateBadInputsUtxo ensures that all inputs are present in the ledger state (have not been spent) +func UtxoValidateBadInputsUtxo(tx common.Transaction, ls common.LedgerState, ts common.TipState, pp common.ProtocolParameters) error { + var badInputs []common.TransactionInput + for _, tmpInput := range tx.Inputs() { + _, err := ls.UtxoById(tmpInput) + if err != nil { + badInputs = append(badInputs, tmpInput) + } + } + if len(badInputs) == 0 { + return nil + } + return BadInputsUtxoError{ + Inputs: badInputs, + } +} + +// UtxoValidateWrongNetwork ensures that all output addresses use the correct network ID +func UtxoValidateWrongNetwork(tx common.Transaction, ls common.LedgerState, ts common.TipState, pp common.ProtocolParameters) error { + networkId := ls.NetworkId() + var badAddrs []common.Address + for _, tmpOutput := range tx.Outputs() { + addr := tmpOutput.Address() + if addr.NetworkId() == networkId { + continue + } + badAddrs = append(badAddrs, addr) + } + if len(badAddrs) == 0 { + return nil + } + return WrongNetworkError{ + NetId: networkId, + Addrs: badAddrs, + } +} + +// UtxoValidateWrongNetworkWithdrawal ensures that all withdrawal addresses use the correct network ID +func UtxoValidateWrongNetworkWithdrawal(tx common.Transaction, ls common.LedgerState, ts common.TipState, pp common.ProtocolParameters) error { + networkId := ls.NetworkId() + var badAddrs []common.Address + for addr := range tx.Withdrawals() { + if addr.NetworkId() == networkId { + continue + } + badAddrs = append(badAddrs, *addr) + } + if len(badAddrs) == 0 { + return nil + } + return WrongNetworkWithdrawalError{ + NetId: networkId, + Addrs: badAddrs, + } +} + +// UtxoValidateValueNotConservedUtxo ensures that the consumed value equals the produced value +func UtxoValidateValueNotConservedUtxo(tx common.Transaction, ls common.LedgerState, ts common.TipState, pp common.ProtocolParameters) error { + // Calculate consumed value + // consumed = value from input(s) + withdrawals + refunds(?) + var consumedValue uint64 + for _, tmpInput := range tx.Inputs() { + tmpUtxo, err := ls.UtxoById(tmpInput) + // Ignore errors fetching the UTxO and exclude it from calculations + if err != nil { + continue + } + consumedValue += tmpUtxo.Output.Amount() + } + for _, tmpWithdrawalAmount := range tx.Withdrawals() { + consumedValue += tmpWithdrawalAmount + } + // Calculate produced value + // produced = value from output(s) + fee + deposits(?) + var producedValue uint64 + for _, tmpOutput := range tx.Outputs() { + producedValue += tmpOutput.Amount() + } + producedValue += tx.Fee() + if consumedValue == producedValue { + return nil + } + return ValueNotConservedUtxoError{ + Consumed: consumedValue, + Produced: producedValue, + } +} + +// UtxoValidateOutputTooSmallUtxo ensures that outputs have at least the minimum value +func UtxoValidateOutputTooSmallUtxo(tx common.Transaction, ls common.LedgerState, ts common.TipState, pp common.ProtocolParameters) error { + minCoin, err := MinCoinTxOut(tx, pp) + if err != nil { + return err + } + var badOutputs []common.TransactionOutput + for _, tmpOutput := range tx.Outputs() { + if tmpOutput.Amount() < minCoin { + badOutputs = append(badOutputs, tmpOutput) + } + } + if len(badOutputs) == 0 { + return nil + } + return OutputTooSmallUtxoError{ + Outputs: badOutputs, + } +} + +// UtxoValidateOutputBootAddrAttrsTooBig ensures that bootstrap (Byron) addresses don't have attributes that are too large +func UtxoValidateOutputBootAddrAttrsTooBig(tx common.Transaction, ls common.LedgerState, ts common.TipState, pp common.ProtocolParameters) error { + var badOutputs []common.TransactionOutput + for _, tmpOutput := range tx.Outputs() { + addr := tmpOutput.Address() + if addr.Type() != common.AddressTypeByron { + continue + } + attr := addr.ByronAttr() + attrBytes, err := cbor.Encode(attr) + if err != nil { + return err + } + if len(attrBytes) <= 64 { + continue + } + badOutputs = append(badOutputs, tmpOutput) + } + if len(badOutputs) == 0 { + return nil + } + return OutputBootAddrAttrsTooBigError{ + Outputs: badOutputs, + } +} + +// UtxoValidateMaxTxSizeUtxo ensures that a transaction does not exceed the max size +func UtxoValidateMaxTxSizeUtxo(tx common.Transaction, ls common.LedgerState, ts common.TipState, 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) + if err != nil { + return err + } + if uint(len(txBytes)) <= tmpPparams.MaxTxSize { + return nil + } + return MaxTxSizeUtxoError{ + TxSize: uint(len(txBytes)), + MaxTxSize: tmpPparams.MaxTxSize, + } +} + +// 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() + minFee := uint64((tmpPparams.MinFeeA * uint(len(txBytes))) + tmpPparams.MinFeeB) + return minFee, nil +} + +// MinCoinTxOut calculates the minimum coin for a transaction output based on protocol parameters +func MinCoinTxOut(_ common.Transaction, pparams common.ProtocolParameters) (uint64, error) { + tmpPparams, ok := pparams.(*ShelleyProtocolParameters) + if !ok { + return 0, fmt.Errorf("pparams are not expected type") + } + minCoinTxOut := uint64(tmpPparams.MinUtxoValue) + return minCoinTxOut, nil +} diff --git a/ledger/shelley/rules_test.go b/ledger/shelley/rules_test.go index 191babb9..fe946cf7 100644 --- a/ledger/shelley/rules_test.go +++ b/ledger/shelley/rules_test.go @@ -15,6 +15,9 @@ package shelley_test import ( + "crypto/rand" + "encoding/hex" + "fmt" "testing" "github.com/blinklabs-io/gouroboros/ledger/common" @@ -25,11 +28,25 @@ import ( ) type testLedgerState struct { - utxos []common.Utxo + networkId uint + utxos []common.Utxo } -func (ls testLedgerState) UtxosById(_ []common.TransactionInput) ([]common.Utxo, error) { - return ls.utxos, nil +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") } type testTipState struct { @@ -147,3 +164,667 @@ func TestUtxoValidateTimeToLive(t *testing.T) { }, ) } + +func TestUtxoValidateInputSetEmptyUtxo(t *testing.T) { + testTx := &shelley.ShelleyTransaction{ + Body: shelley.ShelleyTransactionBody{ + TxInputs: shelley.NewShelleyTransactionInputSet( + // Non-empty input set + []shelley.ShelleyTransactionInput{ + {}, + }, + ), + }, + } + testLedgerState := testLedgerState{} + testTipState := testTipState{} + testProtocolParams := &shelley.ShelleyProtocolParameters{} + // Non-empty + t.Run( + "non-empty input set", + func(t *testing.T) { + err := shelley.UtxoValidateInputSetEmptyUtxo( + testTx, + testLedgerState, + testTipState, + 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 := shelley.UtxoValidateInputSetEmptyUtxo( + testTx, + testLedgerState, + testTipState, + 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 := &shelley.ShelleyTransaction{ + Body: shelley.ShelleyTransactionBody{ + TxFee: testExactFee, + }, + } + testTx.SetCbor(testTxCbor) + testProtocolParams := &shelley.ShelleyProtocolParameters{ + MinFeeA: 7, + MinFeeB: 53, + } + testLedgerState := testLedgerState{} + testTipState := testTipState{} + // 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 := shelley.UtxoValidateFeeTooSmallUtxo( + tmpTestTx, + testLedgerState, + testTipState, + 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 := &shelley.ShelleyTransaction{ + Body: shelley.ShelleyTransactionBody{}, + } + testLedgerState := testLedgerState{ + utxos: []common.Utxo{ + { + Id: testGoodInput, + }, + }, + } + testTipState := testTipState{} + testProtocolParams := &shelley.ShelleyProtocolParameters{} + // Good input + t.Run( + "good input", + func(t *testing.T) { + testTx.Body.TxInputs = shelley.NewShelleyTransactionInputSet( + []shelley.ShelleyTransactionInput{testGoodInput}, + ) + err := shelley.UtxoValidateBadInputsUtxo( + testTx, + testLedgerState, + testTipState, + 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 := shelley.UtxoValidateBadInputsUtxo( + testTx, + testLedgerState, + testTipState, + 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 := &shelley.ShelleyTransaction{ + Body: shelley.ShelleyTransactionBody{ + TxOutputs: []shelley.ShelleyTransactionOutput{ + { + OutputAmount: 123456, + }, + }, + }, + } + testLedgerState := testLedgerState{ + networkId: common.AddressNetworkMainnet, + } + testTipState := testTipState{} + testProtocolParams := &shelley.ShelleyProtocolParameters{} + // Correct network + t.Run( + "correct network", + func(t *testing.T) { + testTx.Body.TxOutputs[0].OutputAddress = testCorrectNetworkAddr + err := shelley.UtxoValidateBadInputsUtxo( + testTx, + testLedgerState, + testTipState, + 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 := shelley.UtxoValidateWrongNetwork( + testTx, + testLedgerState, + testTipState, + 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 := &shelley.ShelleyTransaction{ + Body: shelley.ShelleyTransactionBody{ + TxWithdrawals: map[*common.Address]uint64{}, + }, + } + testLedgerState := testLedgerState{ + networkId: common.AddressNetworkMainnet, + } + testTipState := testTipState{} + testProtocolParams := &shelley.ShelleyProtocolParameters{} + // Correct network + t.Run( + "correct network", + func(t *testing.T) { + testTx.Body.TxWithdrawals[&testCorrectNetworkAddr] = 123456 + err := shelley.UtxoValidateWrongNetworkWithdrawal( + testTx, + testLedgerState, + testTipState, + 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 := shelley.UtxoValidateWrongNetworkWithdrawal( + testTx, + testLedgerState, + testTipState, + 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 := &shelley.ShelleyTransaction{ + Body: 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, + }, + }, + }, + } + testTipState := testTipState{} + testProtocolParams := &shelley.ShelleyProtocolParameters{} + // Exact amount + t.Run( + "exact amount", + func(t *testing.T) { + testTx.Body.TxOutputs[0].OutputAmount = testOutputExactAmount + err := shelley.UtxoValidateValueNotConservedUtxo( + testTx, + testLedgerState, + testTipState, + 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 := shelley.UtxoValidateValueNotConservedUtxo( + testTx, + testLedgerState, + testTipState, + 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 := shelley.UtxoValidateValueNotConservedUtxo( + testTx, + testLedgerState, + testTipState, + 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 := &shelley.ShelleyTransaction{ + Body: shelley.ShelleyTransactionBody{ + TxOutputs: []shelley.ShelleyTransactionOutput{ + // Empty placeholder output + {}, + }, + }, + } + testLedgerState := testLedgerState{} + testTipState := testTipState{} + testProtocolParams := &shelley.ShelleyProtocolParameters{ + MinUtxoValue: 100000, + } + // Good + t.Run( + "sufficient coin", + func(t *testing.T) { + testTx.Body.TxOutputs[0].OutputAmount = testOutputAmountGood + err := shelley.UtxoValidateOutputTooSmallUtxo( + testTx, + testLedgerState, + testTipState, + 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 := shelley.UtxoValidateOutputTooSmallUtxo( + testTx, + testLedgerState, + testTipState, + 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 := &shelley.ShelleyTransaction{ + Body: shelley.ShelleyTransactionBody{ + TxOutputs: []shelley.ShelleyTransactionOutput{ + // Empty placeholder + {}, + }, + }, + } + testLedgerState := testLedgerState{} + testTipState := testTipState{} + testProtocolParams := &shelley.ShelleyProtocolParameters{} + // Good + t.Run( + "Shelley address", + func(t *testing.T) { + testTx.Body.TxOutputs[0].OutputAddress = testGoodAddr + err := shelley.UtxoValidateOutputBootAddrAttrsTooBig( + testTx, + testLedgerState, + testTipState, + 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 := shelley.UtxoValidateOutputBootAddrAttrsTooBig( + testTx, + testLedgerState, + testTipState, + 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 := &shelley.ShelleyTransaction{} + testLedgerState := testLedgerState{} + testTipState := testTipState{} + testProtocolParams := &shelley.ShelleyProtocolParameters{} + // Transaction under limit + t.Run( + "transaction is under limit", + func(t *testing.T) { + testProtocolParams.MaxTxSize = testMaxTxSizeLarge + err := shelley.UtxoValidateMaxTxSizeUtxo( + testTx, + testLedgerState, + testTipState, + 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 := shelley.UtxoValidateMaxTxSizeUtxo( + testTx, + testLedgerState, + testTipState, + 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/shelley/shelley.go b/ledger/shelley/shelley.go index dd8d3dca..8b251940 100644 --- a/ledger/shelley/shelley.go +++ b/ledger/shelley/shelley.go @@ -361,6 +361,13 @@ type ShelleyTransactionInputSet struct { items []ShelleyTransactionInput } +func NewShelleyTransactionInputSet(items []ShelleyTransactionInput) ShelleyTransactionInputSet { + s := ShelleyTransactionInputSet{ + items: items, + } + return s +} + func (s *ShelleyTransactionInputSet) UnmarshalCBOR(data []byte) error { // Make sure this isn't a tag-wrapped set // This is needed to prevent Conway+ TXs from being decoded as an earlier type @@ -380,6 +387,11 @@ func (s *ShelleyTransactionInputSet) Items() []ShelleyTransactionInput { return s.items } +func (s *ShelleyTransactionInputSet) SetItems(items []ShelleyTransactionInput) { + s.items = make([]ShelleyTransactionInput, len(items)) + copy(s.items, items) +} + type ShelleyTransactionInput struct { cbor.StructAsArray TxId common.Blake2b256