From 368ff820b3f0c14b562f6c41ab8e7894391850f8 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 14 Oct 2020 19:07:16 +0300 Subject: [PATCH 1/5] core: add NotValidBefore transaction attribute Close #1490 --- pkg/config/protocol_config.go | 2 ++ pkg/core/blockchain.go | 9 ++++++ pkg/core/blockchain_test.go | 38 ++++++++++++++++++++++++ pkg/core/transaction/attribute.go | 9 +++++- pkg/core/transaction/attribute_test.go | 18 +++++++++++ pkg/core/transaction/attrtype.go | 1 + pkg/core/transaction/attrtype_string.go | 4 +++ pkg/core/transaction/not_valid_before.go | 29 ++++++++++++++++++ 8 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 pkg/core/transaction/not_valid_before.go diff --git a/pkg/config/protocol_config.go b/pkg/config/protocol_config.go index 64969135d..041c68695 100644 --- a/pkg/config/protocol_config.go +++ b/pkg/config/protocol_config.go @@ -9,6 +9,8 @@ type ( ProtocolConfiguration struct { Magic netmode.Magic `yaml:"Magic"` MemPoolSize int `yaml:"MemPoolSize"` + // P2PSigExtensions enables additional signature-related transaction attributes + P2PSigExtensions bool `yaml:"P2PSigExtensions"` // SaveStorageBatch enables storage batch saving before every persist. SaveStorageBatch bool `yaml:"SaveStorageBatch"` SecondsPerBlock int `yaml:"SecondsPerBlock"` diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 34f9fb453..af03cbdc6 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1196,6 +1196,7 @@ func (bc *Blockchain) verifyHeader(currHeader, prevHeader *block.Header) error { // Various errors that could be returned upon verification. var ( + ErrTxNotYetValid = errors.New("transaction is not yet valid") ErrTxExpired = errors.New("transaction has expired") ErrInsufficientFunds = errors.New("insufficient funds") ErrTxSmallNetworkFee = errors.New("too small network fee") @@ -1294,6 +1295,14 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { if uint64(tx.NetworkFee+tx.SystemFee) < req.GasForResponse { return fmt.Errorf("%w: oracle tx has insufficient gas", ErrInvalidAttribute) } + case transaction.NotValidBeforeT: + if !bc.config.P2PSigExtensions { + return errors.New("NotValidBefore attribute was found, but P2PSigExtensions are disabled") + } + nvb := tx.Attributes[i].Value.(*transaction.NotValidBefore) + if height := bc.BlockHeight(); height < nvb.Height { + return fmt.Errorf("%w: NotValidBefore = %d, current height = %d", ErrTxNotYetValid, nvb.Height, height) + } } } return nil diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index ba7c7797e..de070334d 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -559,6 +559,44 @@ func TestVerifyTx(t *testing.T) { checkErr(t, ErrInvalidAttribute, tx) }) }) + t.Run("NotValidBefore", func(t *testing.T) { + getNVBTx := func(height uint32) *transaction.Transaction { + tx := bc.newTestTx(h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.NotValidBeforeT, Value: &transaction.NotValidBefore{Height: height}}) + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.None, + }} + rawScript := testchain.CommitteeVerificationScript() + require.NoError(t, err) + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{{ + InvocationScript: testchain.SignCommittee(data), + VerificationScript: rawScript, + }} + return tx + } + t.Run("Disabled", func(t *testing.T) { + tx := getNVBTx(bc.blockHeight + 1) + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("Enabled", func(t *testing.T) { + bc.config.P2PSigExtensions = true + t.Run("NotYetValid", func(t *testing.T) { + tx := getNVBTx(bc.blockHeight + 1) + require.True(t, errors.Is(bc.VerifyTx(tx), ErrTxNotYetValid)) + }) + t.Run("positive", func(t *testing.T) { + tx := getNVBTx(bc.blockHeight) + require.NoError(t, bc.VerifyTx(tx)) + }) + }) + }) }) } diff --git a/pkg/core/transaction/attribute.go b/pkg/core/transaction/attribute.go index 38fcf843e..26320da0c 100644 --- a/pkg/core/transaction/attribute.go +++ b/pkg/core/transaction/attribute.go @@ -35,6 +35,9 @@ func (attr *Attribute) DecodeBinary(br *io.BinReader) { case OracleResponseT: attr.Value = new(OracleResponse) attr.Value.DecodeBinary(br) + case NotValidBeforeT: + attr.Value = new(NotValidBefore) + attr.Value.DecodeBinary(br) default: br.Err = fmt.Errorf("failed decoding TX attribute usage: 0x%2x", int(attr.Type)) return @@ -46,7 +49,7 @@ func (attr *Attribute) EncodeBinary(bw *io.BinWriter) { bw.WriteB(byte(attr.Type)) switch attr.Type { case HighPriority: - case OracleResponseT: + case OracleResponseT, NotValidBeforeT: attr.Value.EncodeBinary(bw) default: bw.Err = fmt.Errorf("failed encoding TX attribute usage: 0x%2x", attr.Type) @@ -78,6 +81,10 @@ func (attr *Attribute) UnmarshalJSON(data []byte) error { // value, we can unmarshal the same data. The overhead is minimal. attr.Value = new(OracleResponse) return json.Unmarshal(data, attr.Value) + case "NotValidBefore": + attr.Type = NotValidBeforeT + attr.Value = new(NotValidBefore) + return json.Unmarshal(data, attr.Value) default: return errors.New("wrong Type") diff --git a/pkg/core/transaction/attribute_test.go b/pkg/core/transaction/attribute_test.go index 9072373ce..4de8c3667 100644 --- a/pkg/core/transaction/attribute_test.go +++ b/pkg/core/transaction/attribute_test.go @@ -27,6 +27,15 @@ func TestAttribute_EncodeBinary(t *testing.T) { } testserdes.EncodeDecodeBinary(t, attr, new(Attribute)) }) + t.Run("NotValidBefore", func(t *testing.T) { + attr := &Attribute{ + Type: NotValidBeforeT, + Value: &NotValidBefore{ + Height: 123, + }, + } + testserdes.EncodeDecodeBinary(t, attr, new(Attribute)) + }) } func TestAttribute_MarshalJSON(t *testing.T) { @@ -63,4 +72,13 @@ func TestAttribute_MarshalJSON(t *testing.T) { require.Equal(t, attr, actual) testserdes.EncodeDecodeBinary(t, attr, new(Attribute)) }) + t.Run("NotValidBefore", func(t *testing.T) { + attr := &Attribute{ + Type: NotValidBeforeT, + Value: &NotValidBefore{ + Height: 123, + }, + } + testserdes.MarshalUnmarshalJSON(t, attr, new(Attribute)) + }) } diff --git a/pkg/core/transaction/attrtype.go b/pkg/core/transaction/attrtype.go index 4f687e088..6b1367c70 100644 --- a/pkg/core/transaction/attrtype.go +++ b/pkg/core/transaction/attrtype.go @@ -9,6 +9,7 @@ type AttrType uint8 const ( HighPriority AttrType = 1 OracleResponseT AttrType = 0x11 // OracleResponse + NotValidBeforeT AttrType = 0xe0 // NotValidBefore ) func (a AttrType) allowMultiple() bool { diff --git a/pkg/core/transaction/attrtype_string.go b/pkg/core/transaction/attrtype_string.go index f520f46a1..e45529acb 100644 --- a/pkg/core/transaction/attrtype_string.go +++ b/pkg/core/transaction/attrtype_string.go @@ -10,11 +10,13 @@ func _() { var x [1]struct{} _ = x[HighPriority-1] _ = x[OracleResponseT-17] + _ = x[NotValidBeforeT-224] } const ( _AttrType_name_0 = "HighPriority" _AttrType_name_1 = "OracleResponse" + _AttrType_name_2 = "NotValidBefore" ) func (i AttrType) String() string { @@ -23,6 +25,8 @@ func (i AttrType) String() string { return _AttrType_name_0 case i == 17: return _AttrType_name_1 + case i == 224: + return _AttrType_name_2 default: return "AttrType(" + strconv.FormatInt(int64(i), 10) + ")" } diff --git a/pkg/core/transaction/not_valid_before.go b/pkg/core/transaction/not_valid_before.go new file mode 100644 index 000000000..c1596a277 --- /dev/null +++ b/pkg/core/transaction/not_valid_before.go @@ -0,0 +1,29 @@ +package transaction + +import ( + "encoding/binary" + + "github.com/nspcc-dev/neo-go/pkg/io" +) + +// NotValidBefore represents attribute with the height transaction is not valid before. +type NotValidBefore struct { + Height uint32 `json:"height"` +} + +// DecodeBinary implements io.Serializable interface. +func (n *NotValidBefore) DecodeBinary(br *io.BinReader) { + bytes := br.ReadVarBytes(4) + n.Height = binary.LittleEndian.Uint32(bytes) +} + +// EncodeBinary implements io.Serializable interface. +func (n *NotValidBefore) EncodeBinary(w *io.BinWriter) { + bytes := make([]byte, 4) + binary.LittleEndian.PutUint32(bytes, n.Height) + w.WriteVarBytes(bytes) +} + +func (n *NotValidBefore) toJSONMap(m map[string]interface{}) { + m["height"] = n.Height +} From 09b8b8de7317dfbceee58d8f59b1d55bfc7e7f11 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 15 Oct 2020 13:06:22 +0300 Subject: [PATCH 2/5] core: reserve attributes range for experimantal purposes --- pkg/config/protocol_config.go | 2 ++ pkg/core/blockchain.go | 6 ++++- pkg/core/blockchain_test.go | 32 ++++++++++++++++++++++++++ pkg/core/transaction/attribute.go | 16 +++++++++---- pkg/core/transaction/attribute_test.go | 23 ++++++++++++++++++ pkg/core/transaction/attrtype.go | 11 +++++++-- pkg/core/transaction/reserved.go | 24 +++++++++++++++++++ 7 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 pkg/core/transaction/reserved.go diff --git a/pkg/config/protocol_config.go b/pkg/config/protocol_config.go index 041c68695..cddf69bd5 100644 --- a/pkg/config/protocol_config.go +++ b/pkg/config/protocol_config.go @@ -11,6 +11,8 @@ type ( MemPoolSize int `yaml:"MemPoolSize"` // P2PSigExtensions enables additional signature-related transaction attributes P2PSigExtensions bool `yaml:"P2PSigExtensions"` + // ReservedAttributes allows to have reserved attributes range for experimental or private purposes. + ReservedAttributes bool `yaml:"ReservedAttributes"` // SaveStorageBatch enables storage batch saving before every persist. SaveStorageBatch bool `yaml:"SaveStorageBatch"` SecondsPerBlock int `yaml:"SecondsPerBlock"` diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index af03cbdc6..83f36bd13 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1258,7 +1258,7 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool. func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { for i := range tx.Attributes { - switch tx.Attributes[i].Type { + switch attrType := tx.Attributes[i].Type; attrType { case transaction.HighPriority: h := bc.contracts.NEO.GetCommitteeAddress() for i := range tx.Signers { @@ -1303,6 +1303,10 @@ func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { if height := bc.BlockHeight(); height < nvb.Height { return fmt.Errorf("%w: NotValidBefore = %d, current height = %d", ErrTxNotYetValid, nvb.Height, height) } + default: + if !bc.config.ReservedAttributes && attrType >= transaction.ReservedLowerBound && attrType <= transaction.ReservedUpperBound { + return errors.New("attribute of reserved type was found, but ReservedAttributes are disabled") + } } } return nil diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index de070334d..873957da0 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -597,6 +597,38 @@ func TestVerifyTx(t *testing.T) { }) }) }) + t.Run("Reserved", func(t *testing.T) { + getReservedTx := func(attrType transaction.AttrType) *transaction.Transaction { + tx := bc.newTestTx(h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: attrType, Value: &transaction.Reserved{Value: []byte{1, 2, 3}}}) + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.None, + }} + rawScript := testchain.CommitteeVerificationScript() + require.NoError(t, err) + size := io.GetVarSize(tx) + netFee, sizeDelta := fee.Calculate(rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{{ + InvocationScript: testchain.SignCommittee(data), + VerificationScript: rawScript, + }} + return tx + } + t.Run("Disabled", func(t *testing.T) { + tx := getReservedTx(transaction.ReservedLowerBound + 2) + require.Error(t, bc.VerifyTx(tx)) + }) + t.Run("Enabled", func(t *testing.T) { + bc.config.ReservedAttributes = true + tx := getReservedTx(transaction.ReservedLowerBound + 2) + require.NoError(t, bc.VerifyTx(tx)) + }) + }) }) } diff --git a/pkg/core/transaction/attribute.go b/pkg/core/transaction/attribute.go index 26320da0c..140c0bf34 100644 --- a/pkg/core/transaction/attribute.go +++ b/pkg/core/transaction/attribute.go @@ -30,28 +30,36 @@ type attrJSON struct { func (attr *Attribute) DecodeBinary(br *io.BinReader) { attr.Type = AttrType(br.ReadB()) - switch attr.Type { + switch t := attr.Type; t { case HighPriority: + return case OracleResponseT: attr.Value = new(OracleResponse) - attr.Value.DecodeBinary(br) case NotValidBeforeT: attr.Value = new(NotValidBefore) - attr.Value.DecodeBinary(br) default: + if t >= ReservedLowerBound && t <= ReservedUpperBound { + attr.Value = new(Reserved) + break + } br.Err = fmt.Errorf("failed decoding TX attribute usage: 0x%2x", int(attr.Type)) return } + attr.Value.DecodeBinary(br) } // EncodeBinary implements Serializable interface. func (attr *Attribute) EncodeBinary(bw *io.BinWriter) { bw.WriteB(byte(attr.Type)) - switch attr.Type { + switch t := attr.Type; t { case HighPriority: case OracleResponseT, NotValidBeforeT: attr.Value.EncodeBinary(bw) default: + if t >= ReservedLowerBound && t <= ReservedUpperBound { + attr.Value.EncodeBinary(bw) + break + } bw.Err = fmt.Errorf("failed encoding TX attribute usage: 0x%2x", attr.Type) } } diff --git a/pkg/core/transaction/attribute_test.go b/pkg/core/transaction/attribute_test.go index 4de8c3667..c72b1d278 100644 --- a/pkg/core/transaction/attribute_test.go +++ b/pkg/core/transaction/attribute_test.go @@ -36,6 +36,29 @@ func TestAttribute_EncodeBinary(t *testing.T) { } testserdes.EncodeDecodeBinary(t, attr, new(Attribute)) }) + t.Run("Reserved", func(t *testing.T) { + getReservedAttribute := func(t AttrType) *Attribute { + return &Attribute{ + Type: t, + Value: &Reserved{ + Value: []byte{1, 2, 3, 4, 5}, + }, + } + } + t.Run("lower bound", func(t *testing.T) { + testserdes.EncodeDecodeBinary(t, getReservedAttribute(ReservedLowerBound+2), new(Attribute)) + }) + t.Run("upper bound", func(t *testing.T) { + testserdes.EncodeDecodeBinary(t, getReservedAttribute(ReservedUpperBound), new(Attribute)) + }) + t.Run("inside bounds", func(t *testing.T) { + testserdes.EncodeDecodeBinary(t, getReservedAttribute(ReservedLowerBound+5), new(Attribute)) + }) + t.Run("not reserved", func(t *testing.T) { + _, err := testserdes.EncodeBinary(getReservedAttribute(ReservedLowerBound - 1)) + require.Error(t, err) + }) + }) } func TestAttribute_MarshalJSON(t *testing.T) { diff --git a/pkg/core/transaction/attrtype.go b/pkg/core/transaction/attrtype.go index 6b1367c70..090e28d28 100644 --- a/pkg/core/transaction/attrtype.go +++ b/pkg/core/transaction/attrtype.go @@ -5,11 +5,18 @@ package transaction // AttrType represents the purpose of the attribute. type AttrType uint8 +const ( + // ReservedLowerBound is the lower bound of reserved attribute types + ReservedLowerBound = 0xe0 + // ReservedUpperBound is the upper bound of reserved attribute types + ReservedUpperBound = 0xff +) + // List of valid attribute types. const ( HighPriority AttrType = 1 - OracleResponseT AttrType = 0x11 // OracleResponse - NotValidBeforeT AttrType = 0xe0 // NotValidBefore + OracleResponseT AttrType = 0x11 // OracleResponse + NotValidBeforeT AttrType = ReservedLowerBound // NotValidBefore ) func (a AttrType) allowMultiple() bool { diff --git a/pkg/core/transaction/reserved.go b/pkg/core/transaction/reserved.go new file mode 100644 index 000000000..27acc36f2 --- /dev/null +++ b/pkg/core/transaction/reserved.go @@ -0,0 +1,24 @@ +package transaction + +import ( + "github.com/nspcc-dev/neo-go/pkg/io" +) + +// Reserved represents an attribute for experimental or private usage. +type Reserved struct { + Value []byte +} + +// DecodeBinary implements io.Serializable interface. +func (e *Reserved) DecodeBinary(br *io.BinReader) { + e.Value = br.ReadVarBytes() +} + +// EncodeBinary implements io.Serializable interface. +func (e *Reserved) EncodeBinary(w *io.BinWriter) { + w.WriteVarBytes(e.Value) +} + +func (e *Reserved) toJSONMap(m map[string]interface{}) { + m["value"] = e.Value +} From a6579a05acf0d2cbcad7e020c6ce77e909d441a4 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 15 Oct 2020 14:50:51 +0300 Subject: [PATCH 3/5] core: refactor transaction.Attribute unmarshalling --- pkg/core/transaction/attribute.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pkg/core/transaction/attribute.go b/pkg/core/transaction/attribute.go index 140c0bf34..ca3e1011e 100644 --- a/pkg/core/transaction/attribute.go +++ b/pkg/core/transaction/attribute.go @@ -81,21 +81,19 @@ func (attr *Attribute) UnmarshalJSON(data []byte) error { return err } switch aj.Type { - case "HighPriority": + case HighPriority.String(): attr.Type = HighPriority - case "OracleResponse": + return nil + case OracleResponseT.String(): attr.Type = OracleResponseT // Note: because `type` field will not be present in any attribute // value, we can unmarshal the same data. The overhead is minimal. attr.Value = new(OracleResponse) - return json.Unmarshal(data, attr.Value) - case "NotValidBefore": + case NotValidBeforeT.String(): attr.Type = NotValidBeforeT attr.Value = new(NotValidBefore) - return json.Unmarshal(data, attr.Value) default: return errors.New("wrong Type") - } - return nil + return json.Unmarshal(data, attr.Value) } From 95630b72e8dcf7be5f7574b72e9ac944de93de50 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Thu, 15 Oct 2020 16:52:45 +0300 Subject: [PATCH 4/5] core: verify transaction attributes before adding it into mempool Attributes check should be done before adding transaction to the pool, otherwise there might be a case when transaction with invalid attributes is in the pool. --- pkg/core/blockchain.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 83f36bd13..4994642eb 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1234,6 +1234,9 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool. if err != nil { return err } + if err := bc.verifyTxAttributes(t); err != nil { + return err + } err = pool.Add(t, bc) if err != nil { switch { @@ -1249,9 +1252,6 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool. return err } } - if err := bc.verifyTxAttributes(t); err != nil { - return err - } return nil } From 7947e99a1e7dba7414e0f6a99fb4caaef6d52d31 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 20 Oct 2020 18:39:01 +0300 Subject: [PATCH 5/5] core: add transaction.GetAttributes --- pkg/core/transaction/transaction.go | 12 ++++++++ pkg/core/transaction/transaction_test.go | 37 ++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/pkg/core/transaction/transaction.go b/pkg/core/transaction/transaction.go index ff9f6973c..2b26be5c1 100644 --- a/pkg/core/transaction/transaction.go +++ b/pkg/core/transaction/transaction.go @@ -133,6 +133,18 @@ func (t *Transaction) HasAttribute(typ AttrType) bool { return false } +// GetAttributes returns the list of transaction's attributes of the given type. +// Returns nil in case if attributes not found. +func (t *Transaction) GetAttributes(typ AttrType) []Attribute { + var result []Attribute + for _, attr := range t.Attributes { + if attr.Type == typ { + result = append(result, attr) + } + } + return result +} + // decodeHashableFields decodes the fields that are used for signing the // transaction, which are all fields except the scripts. func (t *Transaction) decodeHashableFields(br *io.BinReader) { diff --git a/pkg/core/transaction/transaction_test.go b/pkg/core/transaction/transaction_test.go index 3741dc987..fd213accd 100644 --- a/pkg/core/transaction/transaction_test.go +++ b/pkg/core/transaction/transaction_test.go @@ -7,12 +7,13 @@ import ( "math" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/internal/testserdes" "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestWitnessEncodeDecode(t *testing.T) { @@ -215,3 +216,35 @@ func TestTransaction_isValid(t *testing.T) { require.True(t, errors.Is(tx.isValid(), ErrEmptyScript)) }) } + +func TestTransaction_GetAttributes(t *testing.T) { + attributesTypes := []AttrType{ + HighPriority, + OracleResponseT, + NotValidBeforeT, + } + t.Run("no attributes", func(t *testing.T) { + tx := new(Transaction) + for _, typ := range attributesTypes { + require.Nil(t, tx.GetAttributes(typ)) + } + }) + t.Run("single attributes", func(t *testing.T) { + attrs := make([]Attribute, len(attributesTypes)) + for i, typ := range attributesTypes { + attrs[i] = Attribute{Type: typ} + } + tx := &Transaction{Attributes: attrs} + for _, typ := range attributesTypes { + require.Equal(t, []Attribute{{Type: typ}}, tx.GetAttributes(typ)) + } + }) + t.Run("multiple attributes", func(t *testing.T) { + typ := AttrType(ReservedLowerBound + 1) + conflictsAttrs := []Attribute{{Type: typ}, {Type: typ}} + tx := Transaction{ + Attributes: append([]Attribute{{Type: HighPriority}}, conflictsAttrs...), + } + require.Equal(t, conflictsAttrs, tx.GetAttributes(typ)) + }) +}