Merge pull request #1341 from nspcc-dev/feature/highprio

Support high-priority transactions from committee
This commit is contained in:
Roman Khimov 2020-08-23 13:25:42 +03:00 committed by GitHub
commit 860e7f12ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 335 additions and 24 deletions

View file

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

View file

@ -2,7 +2,6 @@ package core
import (
"errors"
"fmt"
"math/big"
"math/rand"
"testing"
@ -18,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"
@ -252,8 +252,7 @@ func TestVerifyTx(t *testing.T) {
checkErr := func(t *testing.T, expectedErr error, tx *transaction.Transaction) {
err := bc.VerifyTx(tx)
fmt.Println(err)
require.True(t, errors.Is(err, expectedErr))
require.True(t, errors.Is(err, expectedErr), "expected: %v, got: %v", expectedErr, err)
}
testScript := []byte{byte(opcode.PUSH1)}
@ -347,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) {

View file

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

View file

@ -254,3 +254,42 @@ func TestMemPoolFees(t *testing.T) {
}, &FeerStub{})
require.Equal(t, 0, len(mp.fees))
}
func TestMempoolItemsOrder(t *testing.T) {
sender0 := util.Uint160{1, 2, 3}
tx1 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx1.NetworkFee = new(big.Int).Div(balance, big.NewInt(8)).Int64()
tx1.Signers = []transaction.Signer{{Account: sender0}}
tx1.Attributes = []transaction.Attribute{{Type: transaction.HighPriority}}
item1 := &item{txn: tx1}
tx2 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx2.NetworkFee = new(big.Int).Div(balance, big.NewInt(16)).Int64()
tx2.Signers = []transaction.Signer{{Account: sender0}}
tx2.Attributes = []transaction.Attribute{{Type: transaction.HighPriority}}
item2 := &item{txn: tx2}
tx3 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx3.NetworkFee = new(big.Int).Div(balance, big.NewInt(2)).Int64()
tx3.Signers = []transaction.Signer{{Account: sender0}}
item3 := &item{txn: tx3}
tx4 := transaction.New(netmode.UnitTestNet, []byte{byte(opcode.PUSH1)}, 0)
tx4.NetworkFee = new(big.Int).Div(balance, big.NewInt(4)).Int64()
tx4.Signers = []transaction.Signer{{Account: sender0}}
item4 := &item{txn: tx4}
require.True(t, item1.CompareTo(item2) > 0)
require.True(t, item2.CompareTo(item1) < 0)
require.True(t, item1.CompareTo(item3) > 0)
require.True(t, item3.CompareTo(item1) < 0)
require.True(t, item1.CompareTo(item4) > 0)
require.True(t, item4.CompareTo(item1) < 0)
require.True(t, item2.CompareTo(item3) > 0)
require.True(t, item3.CompareTo(item2) < 0)
require.True(t, item2.CompareTo(item4) > 0)
require.True(t, item4.CompareTo(item2) < 0)
require.True(t, item3.CompareTo(item4) > 0)
require.True(t, item4.CompareTo(item3) < 0)
}

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 {
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 {

View file

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

View file

@ -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
)

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

@ -3,6 +3,7 @@ package transaction
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
@ -119,6 +120,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) {
@ -322,35 +333,58 @@ func (t *Transaction) UnmarshalJSON(data []byte) error {
return t.isValid()
}
// Various errors for transaction validation.
var (
ErrInvalidVersion = errors.New("only version 0 is supported")
ErrNegativeSystemFee = errors.New("negative system fee")
ErrNegativeNetworkFee = errors.New("negative network fee")
ErrTooBigFees = errors.New("too big fees: int64 overflow")
ErrEmptySigners = errors.New("signers array should contain sender")
ErrInvalidScope = errors.New("FeeOnly scope can be used only for sender")
ErrNonUniqueSigners = errors.New("transaction signers should be unique")
ErrInvalidAttribute = errors.New("invalid attribute")
ErrEmptyScript = errors.New("no script")
)
// isValid checks whether decoded/unmarshalled transaction has all fields valid.
func (t *Transaction) isValid() error {
if t.Version > 0 {
return errors.New("only version 0 is supported")
return ErrInvalidVersion
}
if t.SystemFee < 0 {
return errors.New("negative system fee")
return ErrNegativeSystemFee
}
if t.NetworkFee < 0 {
return errors.New("negative network fee")
return ErrNegativeNetworkFee
}
if t.NetworkFee+t.SystemFee < t.SystemFee {
return errors.New("too big fees: int64 overflow")
return ErrTooBigFees
}
if len(t.Signers) == 0 {
return errors.New("signers array should contain sender")
return ErrEmptySigners
}
for i := 0; i < len(t.Signers); i++ {
if i > 0 && t.Signers[i].Scopes == FeeOnly {
return errors.New("FeeOnly scope can be used only for sender")
return ErrInvalidScope
}
for j := i + 1; j < len(t.Signers); j++ {
if t.Signers[i].Account.Equals(t.Signers[j].Account) {
return errors.New("transaction signers should be unique")
return ErrNonUniqueSigners
}
}
}
hasHighPrio := false
for i := range t.Attributes {
switch t.Attributes[i].Type {
case HighPriority:
if hasHighPrio {
return fmt.Errorf("%w: multiple high priority attributes", ErrInvalidAttribute)
}
hasHighPrio = true
}
}
if len(t.Script) == 0 {
return errors.New("no script")
return ErrEmptyScript
}
return nil
}

View file

@ -3,6 +3,8 @@ package transaction
import (
"encoding/base64"
"encoding/hex"
"errors"
"math"
"testing"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
@ -112,10 +114,100 @@ 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))
}
func TestTransaction_isValid(t *testing.T) {
newTx := func() *Transaction {
return &Transaction{
Version: 0,
SystemFee: 100,
NetworkFee: 100,
Signers: []Signer{
{Account: util.Uint160{1, 2, 3}},
{
Account: util.Uint160{4, 5, 6},
Scopes: Global,
},
},
Script: []byte{1, 2, 3, 4},
Attributes: []Attribute{},
Scripts: []Witness{},
Trimmed: false,
}
}
t.Run("Valid", func(t *testing.T) {
t.Run("NoAttributes", func(t *testing.T) {
tx := newTx()
require.NoError(t, tx.isValid())
})
t.Run("HighPriority", func(t *testing.T) {
tx := newTx()
tx.Attributes = []Attribute{{Type: HighPriority}}
require.NoError(t, tx.isValid())
})
})
t.Run("InvalidVersion", func(t *testing.T) {
tx := newTx()
tx.Version = 1
require.True(t, errors.Is(tx.isValid(), ErrInvalidVersion))
})
t.Run("NegativeSystemFee", func(t *testing.T) {
tx := newTx()
tx.SystemFee = -1
require.True(t, errors.Is(tx.isValid(), ErrNegativeSystemFee))
})
t.Run("NegativeNetworkFee", func(t *testing.T) {
tx := newTx()
tx.NetworkFee = -1
require.True(t, errors.Is(tx.isValid(), ErrNegativeNetworkFee))
})
t.Run("TooBigFees", func(t *testing.T) {
tx := newTx()
tx.SystemFee = math.MaxInt64 - tx.NetworkFee + 1
require.True(t, errors.Is(tx.isValid(), ErrTooBigFees))
})
t.Run("EmptySigners", func(t *testing.T) {
tx := newTx()
tx.Signers = tx.Signers[:0]
require.True(t, errors.Is(tx.isValid(), ErrEmptySigners))
})
t.Run("InvalidScope", func(t *testing.T) {
tx := newTx()
tx.Signers[1].Scopes = FeeOnly
require.True(t, errors.Is(tx.isValid(), ErrInvalidScope))
})
t.Run("NonUniqueSigners", func(t *testing.T) {
tx := newTx()
tx.Signers[1].Account = tx.Signers[0].Account
require.True(t, errors.Is(tx.isValid(), ErrNonUniqueSigners))
})
t.Run("MultipleHighPriority", func(t *testing.T) {
tx := newTx()
tx.Attributes = []Attribute{
{Type: HighPriority},
{Type: HighPriority},
}
require.True(t, errors.Is(tx.isValid(), ErrInvalidAttribute))
})
t.Run("NoScript", func(t *testing.T) {
tx := newTx()
tx.Script = []byte{}
require.True(t, errors.Is(tx.isValid(), ErrEmptyScript))
})
}

View file

@ -44,3 +44,11 @@ func CreateDefaultMultiSigRedeemScript(publicKeys keys.PublicKeys) ([]byte, erro
m := n - (n-1)/3
return CreateMultiSigRedeemScript(m, publicKeys)
}
// CreateMajorityMultiSigRedeemScript creates an "m out of n" type verification script
// using publicKeys length with m set to majority.
func CreateMajorityMultiSigRedeemScript(publicKeys keys.PublicKeys) ([]byte, error) {
n := len(publicKeys)
m := n - (n-1)/2
return CreateMultiSigRedeemScript(m, publicKeys)
}

View file

@ -83,3 +83,48 @@ func TestCreateDefaultMultiSigRedeemScript(t *testing.T) {
}
checkM(7)
}
func TestCreateMajorityMultiSigRedeemScript(t *testing.T) {
var validators = make([]*keys.PublicKey, 0)
var addKey = func() {
key, err := keys.NewPrivateKey()
require.NoError(t, err)
validators = append(validators, key.PublicKey())
}
var checkM = func(m int) {
validScript, err := CreateMultiSigRedeemScript(m, validators)
require.NoError(t, err)
defaultScript, err := CreateMajorityMultiSigRedeemScript(validators)
require.NoError(t, err)
require.Equal(t, validScript, defaultScript)
}
// 1 out of 1
addKey()
checkM(1)
// 2 out of 2
addKey()
checkM(2)
// 3 out of 4
addKey()
addKey()
checkM(3)
// 4 out of 6
addKey()
addKey()
checkM(4)
// 5 out of 8
addKey()
addKey()
checkM(5)
// 6 out of 10
addKey()
addKey()
checkM(6)
}