diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 6ed1417b4..3e8c1e855 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1226,6 +1226,7 @@ var ( ErrTxTooBig = errors.New("too big transaction") ErrMemPoolConflict = errors.New("invalid transaction due to conflicts with the memory pool") ErrTxInvalidWitnessNum = errors.New("number of signers doesn't match witnesses") + ErrInvalidAttribute = errors.New("invalid attribute") ) // verifyAndPoolTx verifies whether a transaction is bonafide or not and tries @@ -1271,10 +1272,37 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool. return err } } + if err := bc.verifyTxAttributes(t); err != nil { + return err + } return nil } +func (bc *Blockchain) verifyTxAttributes(tx *transaction.Transaction) error { + for i := range tx.Attributes { + switch tx.Attributes[i].Type { + case transaction.HighPriority: + pubs, err := bc.contracts.NEO.GetCommitteeMembers(bc, bc.dao) + if err != nil { + return err + } + s, err := smartcontract.CreateMajorityMultiSigRedeemScript(pubs) + if err != nil { + return err + } + h := hash.Hash160(s) + for i := range tx.Signers { + if tx.Signers[i].Account.Equals(h) { + return nil + } + } + return fmt.Errorf("%w: high priority tx is not signed by committee", ErrInvalidAttribute) + } + } + return nil +} + // isTxStillRelevant is a callback for mempool transaction filtering after the // new block addition. It returns false for transactions added by the new block // (passed via txHashes) and does witness reverification for non-standard @@ -1290,6 +1318,9 @@ func (bc *Blockchain) isTxStillRelevant(t *transaction.Transaction, txHashes []u if index < len(txHashes) && txHashes[index].Equals(t.Hash()) { return false } + if err := bc.verifyTxAttributes(t); err != nil { + return false + } for i := range t.Scripts { if !vm.IsStandardContract(t.Scripts[i].VerificationScript) { recheckWitness = true diff --git a/pkg/core/blockchain_test.go b/pkg/core/blockchain_test.go index f6c59f4f4..b56ba8349 100644 --- a/pkg/core/blockchain_test.go +++ b/pkg/core/blockchain_test.go @@ -17,6 +17,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" @@ -345,6 +346,35 @@ func TestVerifyTx(t *testing.T) { err := bc.PoolTx(tx2) require.True(t, errors.Is(err, ErrOOM)) }) + t.Run("Attribute", func(t *testing.T) { + t.Run("InvalidHighPriority", func(t *testing.T) { + tx := bc.newTestTx(h, testScript) + tx.Attributes = append(tx.Attributes, transaction.Attribute{Type: transaction.HighPriority}) + require.NoError(t, accs[0].SignTx(tx)) + checkErr(t, ErrInvalidAttribute, tx) + }) + t.Run("ValidHighPriority", func(t *testing.T) { + tx := bc.newTestTx(h, testScript) + tx.NetworkFee += 4_000_000 // multisig check + tx.Signers = []transaction.Signer{{ + Account: testchain.MultisigScriptHash(), + Scopes: transaction.FeeOnly, + }} + validators := bc.GetStandByValidators() + rawScript, err := smartcontract.CreateMajorityMultiSigRedeemScript(validators) + require.NoError(t, err) + size := io.GetVarSize(tx) + netFee, sizeDelta := CalculateNetworkFee(rawScript) + tx.NetworkFee += netFee + tx.NetworkFee += int64(size+sizeDelta) * bc.FeePerByte() + data := tx.GetSignedPart() + tx.Scripts = []transaction.Witness{{ + InvocationScript: testchain.Sign(data), + VerificationScript: rawScript, + }} + require.NoError(t, bc.VerifyTx(tx)) + }) + }) } func TestVerifyHashAgainstScript(t *testing.T) { diff --git a/pkg/core/mempool/mem_pool.go b/pkg/core/mempool/mem_pool.go index 2ccf4001d..a643123ee 100644 --- a/pkg/core/mempool/mem_pool.go +++ b/pkg/core/mempool/mem_pool.go @@ -68,6 +68,14 @@ func (p *item) CompareTo(otherP *item) int { return 1 } + pHigh := p.txn.HasAttribute(transaction.HighPriority) + otherHigh := otherP.txn.HasAttribute(transaction.HighPriority) + if pHigh && !otherHigh { + return 1 + } else if !pHigh && otherHigh { + return -1 + } + // Fees sorted ascending. if ret := int(p.txn.FeePerByte() - otherP.txn.FeePerByte()); ret != 0 { return ret diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index a4e9a2898..e7b306ef9 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -407,7 +407,7 @@ func (n *NEO) GetValidatorsInternal(bc blockchainer.Blockchainer, d dao.DAO) (ke if vals := n.validators.Load().(keys.PublicKeys); vals != nil { return vals.Copy(), nil } - result, err := n.getCommitteeMembers(bc, d) + result, err := n.GetCommitteeMembers(bc, d) if err != nil { return nil, err } @@ -430,7 +430,7 @@ func (n *NEO) getValidators(ic *interop.Context, _ []stackitem.Item) stackitem.I } func (n *NEO) getCommittee(ic *interop.Context, _ []stackitem.Item) stackitem.Item { - pubs, err := n.getCommitteeMembers(ic.Chain, ic.DAO) + pubs, err := n.GetCommitteeMembers(ic.Chain, ic.DAO) if err != nil { panic(err) } @@ -450,7 +450,8 @@ func (n *NEO) modifyVoterTurnout(d dao.DAO, amount *big.Int) error { return d.PutStorageItem(n.ContractID, key, si) } -func (n *NEO) getCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) (keys.PublicKeys, error) { +// GetCommitteeMembers returns public keys of nodes in committee. +func (n *NEO) GetCommitteeMembers(bc blockchainer.Blockchainer, d dao.DAO) (keys.PublicKeys, error) { key := []byte{prefixVotersCount} si := d.GetStorageItem(n.ContractID, key) if si == nil { diff --git a/pkg/core/transaction/attribute.go b/pkg/core/transaction/attribute.go index e61f24085..28f6d0c42 100644 --- a/pkg/core/transaction/attribute.go +++ b/pkg/core/transaction/attribute.go @@ -3,6 +3,7 @@ package transaction import ( "encoding/base64" "encoding/json" + "errors" "fmt" "github.com/nspcc-dev/neo-go/pkg/io" @@ -25,14 +26,12 @@ func (attr *Attribute) DecodeBinary(br *io.BinReader) { attr.Type = AttrType(br.ReadB()) var datasize uint64 - /** - switch attr.Type { + case HighPriority: default: br.Err = fmt.Errorf("failed decoding TX attribute usage: 0x%2x", int(attr.Type)) return } - */ attr.Data = make([]byte, datasize) br.ReadBytes(attr.Data) } @@ -41,6 +40,7 @@ func (attr *Attribute) DecodeBinary(br *io.BinReader) { func (attr *Attribute) EncodeBinary(bw *io.BinWriter) { bw.WriteB(byte(attr.Type)) switch attr.Type { + case HighPriority: default: bw.Err = fmt.Errorf("failed encoding TX attribute usage: 0x%2x", attr.Type) } @@ -49,7 +49,7 @@ func (attr *Attribute) EncodeBinary(bw *io.BinWriter) { // MarshalJSON implements the json Marshaller interface. func (attr *Attribute) MarshalJSON() ([]byte, error) { return json.Marshal(attrJSON{ - Type: "", // attr.Type.String() when we're to have some real attributes + Type: attr.Type.String(), Data: base64.StdEncoding.EncodeToString(attr.Data), }) } @@ -65,13 +65,13 @@ func (attr *Attribute) UnmarshalJSON(data []byte) error { if err != nil { return err } - /** switch aj.Type { + case "HighPriority": + attr.Type = HighPriority default: return errors.New("wrong Type") } - */ attr.Data = binData return nil } diff --git a/pkg/core/transaction/attrtype.go b/pkg/core/transaction/attrtype.go index db2d7a35e..6c20ae438 100644 --- a/pkg/core/transaction/attrtype.go +++ b/pkg/core/transaction/attrtype.go @@ -5,6 +5,7 @@ package transaction // AttrType represents the purpose of the attribute. type AttrType uint8 -// List of valid attribute types (none for preview3). -//const ( -//) +// List of valid attribute types. +const ( + HighPriority AttrType = 1 +) diff --git a/pkg/core/transaction/attrtype_string.go b/pkg/core/transaction/attrtype_string.go new file mode 100644 index 000000000..79cde46a4 --- /dev/null +++ b/pkg/core/transaction/attrtype_string.go @@ -0,0 +1,24 @@ +// Code generated by "stringer -type AttrType"; DO NOT EDIT. + +package transaction + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[HighPriority-1] +} + +const _AttrType_name = "HighPriority" + +var _AttrType_index = [...]uint8{0, 12} + +func (i AttrType) String() string { + i -= 1 + if i >= AttrType(len(_AttrType_index)-1) { + return "AttrType(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _AttrType_name[_AttrType_index[i]:_AttrType_index[i+1]] +} diff --git a/pkg/core/transaction/transaction.go b/pkg/core/transaction/transaction.go index 3b966386e..cb9dde82e 100644 --- a/pkg/core/transaction/transaction.go +++ b/pkg/core/transaction/transaction.go @@ -119,6 +119,16 @@ func (t *Transaction) VerificationHash() util.Uint256 { return t.verificationHash } +// HasAttribute returns true iff t has an attribute of type typ. +func (t *Transaction) HasAttribute(typ AttrType) bool { + for i := range t.Attributes { + if t.Attributes[i].Type == typ { + return true + } + } + return false +} + // 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) { @@ -349,6 +359,16 @@ func (t *Transaction) isValid() error { } } } + hasHighPrio := false + for i := range t.Attributes { + switch t.Attributes[i].Type { + case HighPriority: + if hasHighPrio { + return errors.New("multiple high priority attributes") + } + hasHighPrio = true + } + } if len(t.Script) == 0 { return errors.New("no script") } diff --git a/pkg/core/transaction/transaction_test.go b/pkg/core/transaction/transaction_test.go index 761a07b80..d3ea4c772 100644 --- a/pkg/core/transaction/transaction_test.go +++ b/pkg/core/transaction/transaction_test.go @@ -112,10 +112,19 @@ func TestMarshalUnmarshalJSONInvocationTX(t *testing.T) { Version: 0, Signers: []Signer{{Account: util.Uint160{1, 2, 3}}}, Script: []byte{1, 2, 3, 4}, - Attributes: []Attribute{}, + Attributes: []Attribute{{Type: HighPriority, Data: []byte{}}}, Scripts: []Witness{}, Trimmed: false, } testserdes.MarshalUnmarshalJSON(t, tx, new(Transaction)) } + +func TestTransaction_HasAttribute(t *testing.T) { + tx := New(netmode.UnitTestNet, []byte{1}, 0) + require.False(t, tx.HasAttribute(HighPriority)) + tx.Attributes = append(tx.Attributes, Attribute{Type: HighPriority}) + require.True(t, tx.HasAttribute(HighPriority)) + tx.Attributes = append(tx.Attributes, Attribute{Type: HighPriority}) + require.True(t, tx.HasAttribute(HighPriority)) +}