transaction: add HighPriority attribute

HighPriority attributes specifies that transaction was
signed by a committee.
This commit is contained in:
Evgenii Stratonikov 2020-08-19 16:20:48 +03:00
parent 51cebe9e47
commit 2661ebd295
9 changed files with 137 additions and 13 deletions

View file

@ -1226,6 +1226,7 @@ var (
ErrTxTooBig = errors.New("too big transaction") ErrTxTooBig = errors.New("too big transaction")
ErrMemPoolConflict = errors.New("invalid transaction due to conflicts with the memory pool") ErrMemPoolConflict = errors.New("invalid transaction due to conflicts with the memory pool")
ErrTxInvalidWitnessNum = errors.New("number of signers doesn't match witnesses") 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 // 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 return err
} }
} }
if err := bc.verifyTxAttributes(t); err != nil {
return err
}
return nil 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 // isTxStillRelevant is a callback for mempool transaction filtering after the
// new block addition. It returns false for transactions added by the new block // new block addition. It returns false for transactions added by the new block
// (passed via txHashes) and does witness reverification for non-standard // (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()) { if index < len(txHashes) && txHashes[index].Equals(t.Hash()) {
return false return false
} }
if err := bc.verifyTxAttributes(t); err != nil {
return false
}
for i := range t.Scripts { for i := range t.Scripts {
if !vm.IsStandardContract(t.Scripts[i].VerificationScript) { if !vm.IsStandardContract(t.Scripts[i].VerificationScript) {
recheckWitness = true recheckWitness = true

View file

@ -17,6 +17,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/crypto/hash" "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/internal/testchain"
"github.com/nspcc-dev/neo-go/pkg/io" "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/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm"
@ -345,6 +346,35 @@ func TestVerifyTx(t *testing.T) {
err := bc.PoolTx(tx2) err := bc.PoolTx(tx2)
require.True(t, errors.Is(err, ErrOOM)) 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) { func TestVerifyHashAgainstScript(t *testing.T) {

View file

@ -68,6 +68,14 @@ func (p *item) CompareTo(otherP *item) int {
return 1 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. // Fees sorted ascending.
if ret := int(p.txn.FeePerByte() - otherP.txn.FeePerByte()); ret != 0 { if ret := int(p.txn.FeePerByte() - otherP.txn.FeePerByte()); ret != 0 {
return ret return ret

View file

@ -407,7 +407,7 @@ func (n *NEO) GetValidatorsInternal(bc blockchainer.Blockchainer, d dao.DAO) (ke
if vals := n.validators.Load().(keys.PublicKeys); vals != nil { if vals := n.validators.Load().(keys.PublicKeys); vals != nil {
return vals.Copy(), nil return vals.Copy(), nil
} }
result, err := n.getCommitteeMembers(bc, d) result, err := n.GetCommitteeMembers(bc, d)
if err != nil { if err != nil {
return nil, err 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 { 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 { if err != nil {
panic(err) panic(err)
} }
@ -450,7 +450,8 @@ func (n *NEO) modifyVoterTurnout(d dao.DAO, amount *big.Int) error {
return d.PutStorageItem(n.ContractID, key, si) 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} key := []byte{prefixVotersCount}
si := d.GetStorageItem(n.ContractID, key) si := d.GetStorageItem(n.ContractID, key)
if si == nil { if si == nil {

View file

@ -3,6 +3,7 @@ package transaction
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/io"
@ -25,14 +26,12 @@ func (attr *Attribute) DecodeBinary(br *io.BinReader) {
attr.Type = AttrType(br.ReadB()) attr.Type = AttrType(br.ReadB())
var datasize uint64 var datasize uint64
/**
switch attr.Type { switch attr.Type {
case HighPriority:
default: default:
br.Err = fmt.Errorf("failed decoding TX attribute usage: 0x%2x", int(attr.Type)) br.Err = fmt.Errorf("failed decoding TX attribute usage: 0x%2x", int(attr.Type))
return return
} }
*/
attr.Data = make([]byte, datasize) attr.Data = make([]byte, datasize)
br.ReadBytes(attr.Data) br.ReadBytes(attr.Data)
} }
@ -41,6 +40,7 @@ func (attr *Attribute) DecodeBinary(br *io.BinReader) {
func (attr *Attribute) EncodeBinary(bw *io.BinWriter) { func (attr *Attribute) EncodeBinary(bw *io.BinWriter) {
bw.WriteB(byte(attr.Type)) bw.WriteB(byte(attr.Type))
switch attr.Type { switch attr.Type {
case HighPriority:
default: default:
bw.Err = fmt.Errorf("failed encoding TX attribute usage: 0x%2x", attr.Type) 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. // MarshalJSON implements the json Marshaller interface.
func (attr *Attribute) MarshalJSON() ([]byte, error) { func (attr *Attribute) MarshalJSON() ([]byte, error) {
return json.Marshal(attrJSON{ 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), Data: base64.StdEncoding.EncodeToString(attr.Data),
}) })
} }
@ -65,13 +65,13 @@ func (attr *Attribute) UnmarshalJSON(data []byte) error {
if err != nil { if err != nil {
return err return err
} }
/**
switch aj.Type { switch aj.Type {
case "HighPriority":
attr.Type = HighPriority
default: default:
return errors.New("wrong Type") return errors.New("wrong Type")
} }
*/
attr.Data = binData attr.Data = binData
return nil return nil
} }

View file

@ -5,6 +5,7 @@ package transaction
// AttrType represents the purpose of the attribute. // AttrType represents the purpose of the attribute.
type AttrType uint8 type AttrType uint8
// List of valid attribute types (none for preview3). // List of valid attribute types.
//const ( const (
//) HighPriority AttrType = 1
)

View file

@ -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]]
}

View file

@ -119,6 +119,16 @@ func (t *Transaction) VerificationHash() util.Uint256 {
return t.verificationHash 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 // decodeHashableFields decodes the fields that are used for signing the
// transaction, which are all fields except the scripts. // transaction, which are all fields except the scripts.
func (t *Transaction) decodeHashableFields(br *io.BinReader) { 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 { if len(t.Script) == 0 {
return errors.New("no script") return errors.New("no script")
} }

View file

@ -112,10 +112,19 @@ func TestMarshalUnmarshalJSONInvocationTX(t *testing.T) {
Version: 0, Version: 0,
Signers: []Signer{{Account: util.Uint160{1, 2, 3}}}, Signers: []Signer{{Account: util.Uint160{1, 2, 3}}},
Script: []byte{1, 2, 3, 4}, Script: []byte{1, 2, 3, 4},
Attributes: []Attribute{}, Attributes: []Attribute{{Type: HighPriority, Data: []byte{}}},
Scripts: []Witness{}, Scripts: []Witness{},
Trimmed: false, Trimmed: false,
} }
testserdes.MarshalUnmarshalJSON(t, tx, new(Transaction)) 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))
}