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 +}