native: introduce attribute pricing

Port the https://github.com/neo-project/neo/pull/2916.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
This commit is contained in:
Anna Shaleva 2023-09-20 16:33:04 +03:00
parent 80fcf81102
commit 82cb2e718d
17 changed files with 356 additions and 22 deletions

View file

@ -25,6 +25,7 @@ import (
// Ledger is an interface that abstracts the implementation of the blockchain. // Ledger is an interface that abstracts the implementation of the blockchain.
type Ledger interface { type Ledger interface {
BlockHeight() uint32 BlockHeight() uint32
CalculateAttributesFee(tx *transaction.Transaction) int64
FeePerByte() int64 FeePerByte() int64
GetBaseExecFee() int64 GetBaseExecFee() int64
GetHeader(hash util.Uint256) (*block.Header, error) GetHeader(hash util.Uint256) (*block.Header, error)
@ -135,7 +136,7 @@ func signTxGeneric(bc Ledger, sign func(hash.Hashable) []byte, verif []byte, txs
netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), verif) netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), verif)
tx.NetworkFee += netFee tx.NetworkFee += netFee
size += sizeDelta size += sizeDelta
tx.NetworkFee += int64(size) * bc.FeePerByte() tx.NetworkFee += int64(size)*bc.FeePerByte() + bc.CalculateAttributesFee(tx)
tx.Scripts = []transaction.Witness{{ tx.Scripts = []transaction.Witness{{
InvocationScript: sign(tx), InvocationScript: sign(tx),
VerificationScript: verif, VerificationScript: verif,

View file

@ -123,6 +123,14 @@ func TestLedgerVMStates(t *testing.T) {
require.EqualValues(t, ledger.BreakState, vmstate.Break) require.EqualValues(t, ledger.BreakState, vmstate.Break)
} }
func TestPolicyAttributeType(t *testing.T) {
require.EqualValues(t, policy.HighPriorityT, transaction.HighPriority)
require.EqualValues(t, policy.OracleResponseT, transaction.OracleResponseT)
require.EqualValues(t, policy.NotValidBeforeT, transaction.NotValidBeforeT)
require.EqualValues(t, policy.ConflictsT, transaction.ConflictsT)
require.EqualValues(t, policy.NotaryAssistedT, transaction.NotaryAssistedT)
}
type nativeTestCase struct { type nativeTestCase struct {
method string method string
params []string params []string
@ -179,6 +187,8 @@ func TestNativeHelpersCompile(t *testing.T) {
{"setFeePerByte", []string{"42"}}, {"setFeePerByte", []string{"42"}},
{"setStoragePrice", []string{"42"}}, {"setStoragePrice", []string{"42"}},
{"unblockAccount", []string{u160}}, {"unblockAccount", []string{u160}},
{"getAttributeFee", []string{"1"}},
{"setAttributeFee", []string{"1", "123"}},
}) })
runNativeTestCases(t, cs.Ledger.ContractMD, "ledger", []nativeTestCase{ runNativeTestCases(t, cs.Ledger.ContractMD, "ledger", []nativeTestCase{
{"currentHash", nil}, {"currentHash", nil},

View file

@ -53,6 +53,7 @@ type Ledger interface {
SubscribeForBlocks(ch chan *coreb.Block) SubscribeForBlocks(ch chan *coreb.Block)
UnsubscribeFromBlocks(ch chan *coreb.Block) UnsubscribeFromBlocks(ch chan *coreb.Block)
GetBaseExecFee() int64 GetBaseExecFee() int64
CalculateAttributesFee(tx *transaction.Transaction) int64
interop.Ledger interop.Ledger
mempool.Feer mempool.Feer
} }

View file

@ -592,7 +592,7 @@ func signTx(t *testing.T, bc Ledger, txs ...*transaction.Transaction) {
netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript) netFee, sizeDelta := fee.Calculate(bc.GetBaseExecFee(), rawScript)
tx.NetworkFee += +netFee tx.NetworkFee += +netFee
size += sizeDelta size += sizeDelta
tx.NetworkFee += int64(size) * bc.FeePerByte() tx.NetworkFee += int64(size)*bc.FeePerByte() + bc.CalculateAttributesFee(tx)
buf := io.NewBufBinWriter() buf := io.NewBufBinWriter()
for _, key := range privNetKeys { for _, key := range privNetKeys {

View file

@ -2479,7 +2479,7 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
if size > transaction.MaxTransactionSize { if size > transaction.MaxTransactionSize {
return fmt.Errorf("%w: (%d > MaxTransactionSize %d)", ErrTxTooBig, size, transaction.MaxTransactionSize) return fmt.Errorf("%w: (%d > MaxTransactionSize %d)", ErrTxTooBig, size, transaction.MaxTransactionSize)
} }
needNetworkFee := int64(size) * bc.FeePerByte() needNetworkFee := int64(size)*bc.FeePerByte() + bc.CalculateAttributesFee(t)
if bc.P2PSigExtensionsEnabled() { if bc.P2PSigExtensionsEnabled() {
attrs := t.GetAttributes(transaction.NotaryAssistedT) attrs := t.GetAttributes(transaction.NotaryAssistedT)
if len(attrs) != 0 { if len(attrs) != 0 {
@ -2502,7 +2502,7 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
return err return err
} }
} }
err = bc.verifyTxWitnesses(t, nil, isPartialTx) err = bc.verifyTxWitnesses(t, nil, isPartialTx, netFee)
if err != nil { if err != nil {
return err return err
} }
@ -2530,6 +2530,22 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
return nil return nil
} }
// CalculateAttributesFee returns network fee for all transaction attributes that should be
// paid according to native Policy.
func (bc *Blockchain) CalculateAttributesFee(tx *transaction.Transaction) int64 {
var feeSum int64
for _, attr := range tx.Attributes {
base := bc.contracts.Policy.GetAttributeFeeInternal(bc.dao, attr.Type)
switch attr.Type {
case transaction.ConflictsT:
feeSum += base * int64(len(tx.Signers))
default:
feeSum += base
}
}
return feeSum
}
func (bc *Blockchain) verifyTxAttributes(d *dao.Simple, tx *transaction.Transaction, isPartialTx bool) error { func (bc *Blockchain) verifyTxAttributes(d *dao.Simple, tx *transaction.Transaction, isPartialTx bool) error {
for i := range tx.Attributes { for i := range tx.Attributes {
switch attrType := tx.Attributes[i].Type; attrType { switch attrType := tx.Attributes[i].Type; attrType {
@ -2889,17 +2905,24 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa
// transaction. It can reorder them by ScriptHash, because that's required to // transaction. It can reorder them by ScriptHash, because that's required to
// match a slice of script hashes from the Blockchain. Block parameter // match a slice of script hashes from the Blockchain. Block parameter
// is used for easy interop access and can be omitted for transactions that are // is used for easy interop access and can be omitted for transactions that are
// not yet added into any block. // not yet added into any block. verificationFee argument can be provided to
// restrict the maximum amount of GAS allowed to spend on transaction
// verification.
// Golang implementation of VerifyWitnesses method in C# (https://github.com/neo-project/neo/blob/master/neo/SmartContract/Helper.cs#L87). // Golang implementation of VerifyWitnesses method in C# (https://github.com/neo-project/neo/blob/master/neo/SmartContract/Helper.cs#L87).
func (bc *Blockchain) verifyTxWitnesses(t *transaction.Transaction, block *block.Block, isPartialTx bool) error { func (bc *Blockchain) verifyTxWitnesses(t *transaction.Transaction, block *block.Block, isPartialTx bool, verificationFee ...int64) error {
interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, block, t) interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, block, t)
gasLimit := t.NetworkFee - int64(t.Size())*bc.FeePerByte() var gasLimit int64
if bc.P2PSigExtensionsEnabled() { if len(verificationFee) == 0 {
attrs := t.GetAttributes(transaction.NotaryAssistedT) gasLimit = t.NetworkFee - int64(t.Size())*bc.FeePerByte() - bc.CalculateAttributesFee(t)
if len(attrs) != 0 { if bc.P2PSigExtensionsEnabled() {
na := attrs[0].Value.(*transaction.NotaryAssisted) attrs := t.GetAttributes(transaction.NotaryAssistedT)
gasLimit -= (int64(na.NKeys) + 1) * bc.contracts.Notary.GetNotaryServiceFeePerKey(bc.dao) if len(attrs) != 0 {
na := attrs[0].Value.(*transaction.NotaryAssisted)
gasLimit -= (int64(na.NKeys) + 1) * bc.contracts.Notary.GetNotaryServiceFeePerKey(bc.dao)
}
} }
} else {
gasLimit = verificationFee[0]
} }
for i := range t.Signers { for i := range t.Signers {
gasConsumed, err := bc.verifyHashAgainstScript(t.Signers[i].Account, &t.Scripts[i], interopCtx, gasLimit) gasConsumed, err := bc.verifyHashAgainstScript(t.Signers[i].Account, &t.Scripts[i], interopCtx, gasLimit)

View file

@ -347,18 +347,30 @@ func toUint160(s stackitem.Item) util.Uint160 {
return u return u
} }
func toUint32(s stackitem.Item) uint32 { func toUint64(s stackitem.Item) uint64 {
bigInt := toBigInt(s) bigInt := toBigInt(s)
if !bigInt.IsUint64() { if !bigInt.IsUint64() {
panic("bigint is not an uint64") panic("bigint is not a uint64")
} }
uint64Value := bigInt.Uint64() return bigInt.Uint64()
}
func toUint32(s stackitem.Item) uint32 {
uint64Value := toUint64(s)
if uint64Value > math.MaxUint32 { if uint64Value > math.MaxUint32 {
panic("bigint does not fit into uint32") panic("bigint does not fit into uint32")
} }
return uint32(uint64Value) return uint32(uint64Value)
} }
func toUint8(s stackitem.Item) uint8 {
uint64Value := toUint64(s)
if uint64Value > math.MaxUint8 {
panic("bigint does not fit into uint8")
}
return uint8(uint64Value)
}
func toInt64(s stackitem.Item) int64 { func toInt64(s stackitem.Item) int64 {
bigInt := toBigInt(s) bigInt := toBigInt(s)
if !bigInt.IsInt64() { if !bigInt.IsInt64() {

View file

@ -7,8 +7,14 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/interop" "github.com/nspcc-dev/neo-go/pkg/core/interop"
"github.com/nspcc-dev/neo-go/pkg/core/native" "github.com/nspcc-dev/neo-go/pkg/core/native"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/neotest"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
) )
func newPolicyClient(t *testing.T) *neotest.ContractInvoker { func newPolicyClient(t *testing.T) *neotest.ContractInvoker {
@ -39,6 +45,67 @@ func TestPolicy_StoragePriceCache(t *testing.T) {
testGetSetCache(t, newPolicyClient(t), "StoragePrice", native.DefaultStoragePrice) testGetSetCache(t, newPolicyClient(t), "StoragePrice", native.DefaultStoragePrice)
} }
func TestPolicy_AttributeFee(t *testing.T) {
c := newPolicyClient(t)
getName := "getAttributeFee"
setName := "setAttributeFee"
randomInvoker := c.WithSigners(c.NewAccount(t))
committeeInvoker := c.WithSigners(c.Committee)
t.Run("set, not signed by committee", func(t *testing.T) {
randomInvoker.InvokeFail(t, "invalid committee signature", setName, byte(transaction.ConflictsT), 123)
})
t.Run("get, unknown attribute", func(t *testing.T) {
randomInvoker.InvokeFail(t, "invalid attribute type: 84", getName, byte(0x54))
})
t.Run("get, default value", func(t *testing.T) {
randomInvoker.Invoke(t, 0, getName, byte(transaction.ConflictsT))
})
t.Run("set, too large value", func(t *testing.T) {
committeeInvoker.InvokeFail(t, "out of range", setName, byte(transaction.ConflictsT), 10_0000_0001)
})
t.Run("set, unknown attribute", func(t *testing.T) {
committeeInvoker.InvokeFail(t, "invalid attribute type: 84", setName, 0x54, 5)
})
t.Run("set, success", func(t *testing.T) {
// Set and get in the same block.
txSet := committeeInvoker.PrepareInvoke(t, setName, byte(transaction.ConflictsT), 1)
txGet := randomInvoker.PrepareInvoke(t, getName, byte(transaction.ConflictsT))
c.AddNewBlock(t, txSet, txGet)
c.CheckHalt(t, txSet.Hash(), stackitem.Null{})
c.CheckHalt(t, txGet.Hash(), stackitem.Make(1))
// Get in the next block.
randomInvoker.Invoke(t, 1, getName, byte(transaction.ConflictsT))
})
}
func TestPolicy_AttributeFeeCache(t *testing.T) {
c := newPolicyClient(t)
getName := "getAttributeFee"
setName := "setAttributeFee"
committeeInvoker := c.WithSigners(c.Committee)
// Change fee, abort the transaction and check that contract cache wasn't persisted
// for FAULTed tx at the same block.
w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, committeeInvoker.Hash, setName, callflag.All, byte(transaction.ConflictsT), 5)
emit.Opcodes(w.BinWriter, opcode.ABORT)
tx1 := committeeInvoker.PrepareInvocation(t, w.Bytes(), committeeInvoker.Signers)
tx2 := committeeInvoker.PrepareInvoke(t, getName, byte(transaction.ConflictsT))
committeeInvoker.AddNewBlock(t, tx1, tx2)
committeeInvoker.CheckFault(t, tx1.Hash(), "ABORT")
committeeInvoker.CheckHalt(t, tx2.Hash(), stackitem.Make(0))
// Change fee and check that change is available for the next tx.
tx1 = committeeInvoker.PrepareInvoke(t, setName, byte(transaction.ConflictsT), 5)
tx2 = committeeInvoker.PrepareInvoke(t, getName, byte(transaction.ConflictsT))
committeeInvoker.AddNewBlock(t, tx1, tx2)
committeeInvoker.CheckHalt(t, tx1.Hash())
committeeInvoker.CheckHalt(t, tx2.Hash(), stackitem.Make(5))
}
func TestPolicy_BlockedAccounts(t *testing.T) { func TestPolicy_BlockedAccounts(t *testing.T) {
c := newPolicyClient(t) c := newPolicyClient(t)
e := c.Executor e := c.Executor

View file

@ -1,6 +1,7 @@
package native package native
import ( import (
"encoding/hex"
"fmt" "fmt"
"math/big" "math/big"
"sort" "sort"
@ -11,6 +12,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
"github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
@ -24,6 +26,8 @@ const (
defaultExecFeeFactor = interop.DefaultBaseExecFee defaultExecFeeFactor = interop.DefaultBaseExecFee
defaultFeePerByte = 1000 defaultFeePerByte = 1000
defaultMaxVerificationGas = 1_50000000 defaultMaxVerificationGas = 1_50000000
// defaultAttributeFee is a default fee for a transaction attribute those price wasn't set yet.
defaultAttributeFee = 0
// DefaultStoragePrice is the price to pay for 1 byte of storage. // DefaultStoragePrice is the price to pay for 1 byte of storage.
DefaultStoragePrice = 100000 DefaultStoragePrice = 100000
@ -33,9 +37,13 @@ const (
maxFeePerByte = 100_000_000 maxFeePerByte = 100_000_000
// maxStoragePrice is the maximum allowed price for a byte of storage. // maxStoragePrice is the maximum allowed price for a byte of storage.
maxStoragePrice = 10000000 maxStoragePrice = 10000000
// maxAttributeFee is the maximum allowed value for a transaction attribute fee.
maxAttributeFee = 10_00000000
// blockedAccountPrefix is a prefix used to store blocked account. // blockedAccountPrefix is a prefix used to store blocked account.
blockedAccountPrefix = 15 blockedAccountPrefix = 15
// attributeFeePrefix is a prefix used to store attribute fee.
attributeFeePrefix = 20
) )
var ( var (
@ -59,6 +67,7 @@ type PolicyCache struct {
feePerByte int64 feePerByte int64
maxVerificationGas int64 maxVerificationGas int64
storagePrice uint32 storagePrice uint32
attributeFee map[transaction.AttrType]uint32
blockedAccounts []util.Uint160 blockedAccounts []util.Uint160
} }
@ -76,6 +85,10 @@ func (c *PolicyCache) Copy() dao.NativeContractCache {
func copyPolicyCache(src, dst *PolicyCache) { func copyPolicyCache(src, dst *PolicyCache) {
*dst = *src *dst = *src
dst.attributeFee = make(map[transaction.AttrType]uint32, len(src.attributeFee))
for t, v := range src.attributeFee {
dst.attributeFee[t] = v
}
dst.blockedAccounts = make([]util.Uint160, len(src.blockedAccounts)) dst.blockedAccounts = make([]util.Uint160, len(src.blockedAccounts))
copy(dst.blockedAccounts, src.blockedAccounts) copy(dst.blockedAccounts, src.blockedAccounts)
} }
@ -112,6 +125,17 @@ func newPolicy() *Policy {
md = newMethodAndPrice(p.setStoragePrice, 1<<15, callflag.States) md = newMethodAndPrice(p.setStoragePrice, 1<<15, callflag.States)
p.AddMethod(md, desc) p.AddMethod(md, desc)
desc = newDescriptor("getAttributeFee", smartcontract.IntegerType,
manifest.NewParameter("attributeType", smartcontract.IntegerType))
md = newMethodAndPrice(p.getAttributeFee, 1<<15, callflag.ReadStates)
p.AddMethod(md, desc)
desc = newDescriptor("setAttributeFee", smartcontract.VoidType,
manifest.NewParameter("attributeType", smartcontract.IntegerType),
manifest.NewParameter("value", smartcontract.IntegerType))
md = newMethodAndPrice(p.setAttributeFee, 1<<15, callflag.States)
p.AddMethod(md, desc)
desc = newDescriptor("setFeePerByte", smartcontract.VoidType, desc = newDescriptor("setFeePerByte", smartcontract.VoidType,
manifest.NewParameter("value", smartcontract.IntegerType)) manifest.NewParameter("value", smartcontract.IntegerType))
md = newMethodAndPrice(p.setFeePerByte, 1<<15, callflag.States) md = newMethodAndPrice(p.setFeePerByte, 1<<15, callflag.States)
@ -146,6 +170,7 @@ func (p *Policy) Initialize(ic *interop.Context) error {
feePerByte: defaultFeePerByte, feePerByte: defaultFeePerByte,
maxVerificationGas: defaultMaxVerificationGas, maxVerificationGas: defaultMaxVerificationGas,
storagePrice: DefaultStoragePrice, storagePrice: DefaultStoragePrice,
attributeFee: map[transaction.AttrType]uint32{},
blockedAccounts: make([]util.Uint160, 0), blockedAccounts: make([]util.Uint160, 0),
} }
ic.DAO.SetCache(p.ID, cache) ic.DAO.SetCache(p.ID, cache)
@ -183,6 +208,25 @@ func (p *Policy) fillCacheFromDAO(cache *PolicyCache, d *dao.Simple) error {
if fErr != nil { if fErr != nil {
return fmt.Errorf("failed to initialize blocked accounts: %w", fErr) return fmt.Errorf("failed to initialize blocked accounts: %w", fErr)
} }
cache.attributeFee = make(map[transaction.AttrType]uint32)
d.Seek(p.ID, storage.SeekRange{Prefix: []byte{attributeFeePrefix}}, func(k, v []byte) bool {
if len(k) != 1 {
fErr = fmt.Errorf("unexpected attribute type len %d (%s)", len(k), hex.EncodeToString(k))
return false
}
t := transaction.AttrType(k[0])
value := bigint.FromBytes(v)
if value == nil {
fErr = fmt.Errorf("unexpected attribute value format: key=%s, value=%s", hex.EncodeToString(k), hex.EncodeToString(v))
return false
}
cache.attributeFee[t] = uint32(value.Int64())
return true
})
if fErr != nil {
return fmt.Errorf("failed to initialize attribute fees: %w", fErr)
}
return nil return nil
} }
@ -297,6 +341,43 @@ func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) sta
return stackitem.Null{} return stackitem.Null{}
} }
func (p *Policy) getAttributeFee(ic *interop.Context, args []stackitem.Item) stackitem.Item {
t := transaction.AttrType(toUint8(args[0]))
if !transaction.IsValidAttrType(ic.Chain.GetConfig().ReservedAttributes, t) {
panic(fmt.Errorf("invalid attribute type: %d", t))
}
return stackitem.NewBigInteger(big.NewInt(p.GetAttributeFeeInternal(ic.DAO, t)))
}
// GetAttributeFeeInternal returns required transaction's attribute fee.
func (p *Policy) GetAttributeFeeInternal(d *dao.Simple, t transaction.AttrType) int64 {
cache := d.GetROCache(p.ID).(*PolicyCache)
v, ok := cache.attributeFee[t]
if !ok {
// We may safely omit this part, but let it be here in case if defaultAttributeFee value is changed.
v = defaultAttributeFee
}
return int64(v)
}
func (p *Policy) setAttributeFee(ic *interop.Context, args []stackitem.Item) stackitem.Item {
t := transaction.AttrType(toUint8(args[0]))
value := toUint32(args[1])
if !transaction.IsValidAttrType(ic.Chain.GetConfig().ReservedAttributes, t) {
panic(fmt.Errorf("invalid attribute type: %d", t))
}
if value > maxAttributeFee {
panic(fmt.Errorf("attribute value is out of range: %d", value))
}
if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
setIntWithKey(p.ID, ic.DAO, []byte{attributeFeePrefix, byte(t)}, int64(value))
cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
cache.attributeFee[t] = value
return stackitem.Null{}
}
// setFeePerByte is a Policy contract method that sets transaction's fee per byte. // setFeePerByte is a Policy contract method that sets transaction's fee per byte.
func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stackitem.Item { func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stackitem.Item {
value := toBigInt(args[0]).Int64() value := toBigInt(args[0]).Int64()

View file

@ -21,6 +21,15 @@ const (
NotaryAssistedT AttrType = 0x22 // NotaryAssisted NotaryAssistedT AttrType = 0x22 // NotaryAssisted
) )
// attrTypes contains a set of valid attribute types (does not include reserved attributes).
var attrTypes = map[AttrType]struct{}{
HighPriority: {},
OracleResponseT: {},
NotValidBeforeT: {},
ConflictsT: {},
NotaryAssistedT: {},
}
func (a AttrType) allowMultiple() bool { func (a AttrType) allowMultiple() bool {
switch a { switch a {
case ConflictsT: case ConflictsT:
@ -29,3 +38,11 @@ func (a AttrType) allowMultiple() bool {
return false return false
} }
} }
// IsValidAttrType returns whether the provided attribute type is valid.
func IsValidAttrType(reservedAttributesEnabled bool, attrType AttrType) bool {
if _, ok := attrTypes[attrType]; ok {
return true
}
return reservedAttributesEnabled && ReservedLowerBound <= attrType && attrType <= ReservedUpperBound
}

View file

@ -0,0 +1,14 @@
package policy
// AttributeType represents a transaction attribute type.
type AttributeType byte
// List of valid transaction attribute types.
const (
HighPriorityT AttributeType = 1
OracleResponseT AttributeType = 0x11
NotValidBeforeT AttributeType = 0x20
ConflictsT AttributeType = 0x21
// NotaryAssistedT is an extension of Neo protocol available on specifically configured NeoGo networks.
NotaryAssistedT AttributeType = 0x22
)

View file

@ -43,6 +43,16 @@ func SetStoragePrice(value int) {
neogointernal.CallWithTokenNoRet(Hash, "setStoragePrice", int(contract.States), value) neogointernal.CallWithTokenNoRet(Hash, "setStoragePrice", int(contract.States), value)
} }
// GetAttributeFee represents `getAttributeFee` method of Policy native contract.
func GetAttributeFee(t AttributeType) int {
return neogointernal.CallWithToken(Hash, "getAttributeFee", int(contract.ReadStates), t).(int)
}
// SetAttributeFee represents `setAttributeFee` method of Policy native contract.
func SetAttributeFee(t AttributeType, value int) {
neogointernal.CallWithTokenNoRet(Hash, "setAttributeFee", int(contract.States), t, value)
}
// IsBlocked represents `isBlocked` method of Policy native contract. // IsBlocked represents `isBlocked` method of Policy native contract.
func IsBlocked(addr interop.Hash160) bool { func IsBlocked(addr interop.Hash160) bool {
return neogointernal.CallWithToken(Hash, "isBlocked", int(contract.ReadStates), addr).(bool) return neogointernal.CallWithToken(Hash, "isBlocked", int(contract.ReadStates), addr).(bool)

View file

@ -305,7 +305,7 @@ func AddNetworkFee(bc *core.Blockchain, tx *transaction.Transaction, signers ...
tx.NetworkFee += netFee tx.NetworkFee += netFee
size += sizeDelta size += sizeDelta
} }
tx.NetworkFee += int64(size) * bc.FeePerByte() tx.NetworkFee += int64(size)*bc.FeePerByte() + bc.CalculateAttributesFee(tx)
} }
// NewUnsignedBlock creates a new unsigned block from txs. // NewUnsignedBlock creates a new unsigned block from txs.

View file

@ -40,6 +40,7 @@ const (
execFeeSetter = "setExecFeeFactor" execFeeSetter = "setExecFeeFactor"
feePerByteSetter = "setFeePerByte" feePerByteSetter = "setFeePerByte"
storagePriceSetter = "setStoragePrice" storagePriceSetter = "setStoragePrice"
attributeFeeSetter = "setAttributeFee"
) )
// ContractReader provides an interface to call read-only PolicyContract // ContractReader provides an interface to call read-only PolicyContract
@ -88,6 +89,12 @@ func (c *ContractReader) GetStoragePrice() (int64, error) {
return unwrap.Int64(c.invoker.Call(Hash, "getStoragePrice")) return unwrap.Int64(c.invoker.Call(Hash, "getStoragePrice"))
} }
// GetAttributeFee returns current fee for the specified attribute usage. Any
// contract saving data to the storage pays for it according to this value.
func (c *ContractReader) GetAttributeFee(t transaction.AttrType) (int64, error) {
return unwrap.Int64(c.invoker.Call(Hash, "getAttributeFee", byte(t)))
}
// IsBlocked checks if the given account is blocked in the PolicyContract. // IsBlocked checks if the given account is blocked in the PolicyContract.
func (c *ContractReader) IsBlocked(account util.Uint160) (bool, error) { func (c *ContractReader) IsBlocked(account util.Uint160) (bool, error) {
return unwrap.Bool(c.invoker.Call(Hash, "isBlocked", account)) return unwrap.Bool(c.invoker.Call(Hash, "isBlocked", account))
@ -158,6 +165,28 @@ func (c *Contract) SetStoragePriceUnsigned(value int64) (*transaction.Transactio
return c.actor.MakeUnsignedCall(Hash, storagePriceSetter, nil, value) return c.actor.MakeUnsignedCall(Hash, storagePriceSetter, nil, value)
} }
// SetAttributeFee creates and sends a transaction that sets the new attribute
// fee value for the specified attribute. The action is successful when
// transaction ends in HALT state. The returned values are transaction hash, its
// ValidUntilBlock value and an error if any.
func (c *Contract) SetAttributeFee(t transaction.AttrType, value int64) (util.Uint256, uint32, error) {
return c.actor.SendCall(Hash, attributeFeeSetter, byte(t), value)
}
// SetAttributeFeeTransaction creates a transaction that sets the new attribute
// fee value for the specified attribute. This transaction is signed, but not
// sent to the network, instead it's returned to the caller.
func (c *Contract) SetAttributeFeeTransaction(t transaction.AttrType, value int64) (*transaction.Transaction, error) {
return c.actor.MakeCall(Hash, attributeFeeSetter, byte(t), value)
}
// SetAttributeFeeUnsigned creates a transaction that sets the new attribute fee
// value for the specified attribute. This transaction is not signed and just
// returned to the caller.
func (c *Contract) SetAttributeFeeUnsigned(t transaction.AttrType, value int64) (*transaction.Transaction, error) {
return c.actor.MakeUnsignedCall(Hash, attributeFeeSetter, nil, byte(t), value)
}
// BlockAccount creates and sends a transaction that blocks an account on the // BlockAccount creates and sends a transaction that blocks an account on the
// network (via `blockAccount` method), it fails (with FAULT state) if it's not // network (via `blockAccount` method), it fails (with FAULT state) if it's not
// successful. The returned values are transaction hash, its // successful. The returned values are transaction hash, its

View file

@ -58,6 +58,8 @@ func TestReader(t *testing.T) {
} }
_, err := pc.IsBlocked(util.Uint160{1, 2, 3}) _, err := pc.IsBlocked(util.Uint160{1, 2, 3})
require.Error(t, err) require.Error(t, err)
_, err = pc.GetAttributeFee(transaction.ConflictsT)
require.Error(t, err)
ta.err = nil ta.err = nil
ta.res = &result.Invoke{ ta.res = &result.Invoke{
@ -71,6 +73,9 @@ func TestReader(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(42), val) require.Equal(t, int64(42), val)
} }
v, err := pc.GetAttributeFee(transaction.ConflictsT)
require.NoError(t, err)
require.Equal(t, int64(42), v)
ta.res = &result.Invoke{ ta.res = &result.Invoke{
State: "HALT", State: "HALT",
Stack: []stackitem.Item{ Stack: []stackitem.Item{
@ -97,6 +102,8 @@ func TestIntSetters(t *testing.T) {
_, _, err := m(42) _, _, err := m(42)
require.Error(t, err) require.Error(t, err)
} }
_, _, err := pc.SetAttributeFee(transaction.OracleResponseT, 123)
require.Error(t, err)
ta.err = nil ta.err = nil
ta.txh = util.Uint256{1, 2, 3} ta.txh = util.Uint256{1, 2, 3}
@ -107,6 +114,10 @@ func TestIntSetters(t *testing.T) {
require.Equal(t, ta.txh, h) require.Equal(t, ta.txh, h)
require.Equal(t, ta.vub, vub) require.Equal(t, ta.vub, vub)
} }
h, vub, err := pc.SetAttributeFee(transaction.OracleResponseT, 123)
require.NoError(t, err)
require.Equal(t, ta.txh, h)
require.Equal(t, ta.vub, vub)
} }
func TestUint160Setters(t *testing.T) { func TestUint160Setters(t *testing.T) {

View file

@ -743,12 +743,13 @@ func TestCalculateNetworkFee(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, c.Init()) require.NoError(t, c.Init())
h, err := util.Uint160DecodeStringLE(verifyWithArgsContractHash)
require.NoError(t, err)
priv := testchain.PrivateKeyByID(0)
acc0 := wallet.NewAccountFromPrivateKey(priv)
t.Run("ContractWithArgs", func(t *testing.T) { t.Run("ContractWithArgs", func(t *testing.T) {
check := func(t *testing.T, extraFee int64) { check := func(t *testing.T, extraFee int64) {
h, err := util.Uint160DecodeStringLE(verifyWithArgsContractHash)
require.NoError(t, err)
priv := testchain.PrivateKeyByID(0)
acc0 := wallet.NewAccountFromPrivateKey(priv)
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0) tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
require.NoError(t, err) require.NoError(t, err)
tx.ValidUntilBlock = chain.BlockHeight() + 10 tx.ValidUntilBlock = chain.BlockHeight() + 10
@ -802,6 +803,62 @@ func TestCalculateNetworkFee(t *testing.T) {
check(t, -1) check(t, -1)
}) })
}) })
t.Run("extra attribute fee", func(t *testing.T) {
const conflictsFee = 100
tx := transaction.New([]byte{byte(opcode.PUSH1)}, 0)
tx.ValidUntilBlock = chain.BlockHeight() + 10
signer0 := transaction.Signer{
Account: acc0.ScriptHash(),
Scopes: transaction.CalledByEntry,
}
priv1 := testchain.PrivateKeyByID(1)
acc1 := wallet.NewAccountFromPrivateKey(priv1)
signer1 := transaction.Signer{
Account: acc1.ScriptHash(),
Scopes: transaction.CalledByEntry,
}
tx.Signers = []transaction.Signer{signer0, signer1}
tx.Attributes = []transaction.Attribute{
{
Type: transaction.ConflictsT,
Value: &transaction.Conflicts{Hash: util.Uint256{1, 2, 3}},
},
}
tx.Scripts = []transaction.Witness{
{VerificationScript: acc0.Contract.Script},
{VerificationScript: acc1.Contract.Script},
}
oldFee, err := c.CalculateNetworkFee(tx)
require.NoError(t, err)
// Set fee per Conflicts attribute.
script, err := smartcontract.CreateCallScript(state.CreateNativeContractHash(nativenames.Policy), "setAttributeFee", byte(transaction.ConflictsT), conflictsFee)
require.NoError(t, err)
txSetFee := transaction.New(script, 1_0000_0000)
txSetFee.ValidUntilBlock = chain.BlockHeight() + 1
txSetFee.Signers = []transaction.Signer{
signer0,
{
Account: testchain.CommitteeScriptHash(),
Scopes: transaction.CalledByEntry,
},
}
txSetFee.NetworkFee = 10_0000_0000
require.NoError(t, acc0.SignTx(testchain.Network(), txSetFee))
txSetFee.Scripts = append(txSetFee.Scripts, transaction.Witness{
InvocationScript: testchain.SignCommittee(txSetFee),
VerificationScript: testchain.CommitteeVerificationScript(),
})
require.NoError(t, chain.AddBlock(testchain.NewBlock(t, chain, 1, 0, txSetFee)))
// Calculate network fee one more time with updated Conflicts price.
newFee, err := c.CalculateNetworkFee(tx)
require.NoError(t, err)
expectedDiff := len(tx.Signers) * len(tx.GetAttributes(transaction.ConflictsT)) * conflictsFee
require.Equal(t, int64(expectedDiff), newFee-oldFee)
})
} }
func TestNotaryActor(t *testing.T) { func TestNotaryActor(t *testing.T) {

View file

@ -65,6 +65,7 @@ type (
Ledger interface { Ledger interface {
AddBlock(block *block.Block) error AddBlock(block *block.Block) error
BlockHeight() uint32 BlockHeight() uint32
CalculateAttributesFee(tx *transaction.Transaction) int64
CalculateClaimable(h util.Uint160, endHeight uint32) (*big.Int, error) CalculateClaimable(h util.Uint160, endHeight uint32) (*big.Int, error)
CurrentBlockHash() util.Uint256 CurrentBlockHash() util.Uint256
FeePerByte() int64 FeePerByte() int64
@ -983,7 +984,7 @@ func (s *Server) calculateNetworkFee(reqParams params.Params) (any, *neorpc.Erro
} }
} }
fee := s.chain.FeePerByte() fee := s.chain.FeePerByte()
netFee += int64(size) * fee netFee += int64(size)*fee + s.chain.CalculateAttributesFee(tx)
return result.NetworkFee{Value: netFee}, nil return result.NetworkFee{Value: netFee}, nil
} }

View file

@ -87,7 +87,7 @@ const (
faultedTxHashLE = "82279bfe9bada282ca0f8cb8e0bb124b921af36f00c69a518320322c6f4fef60" faultedTxHashLE = "82279bfe9bada282ca0f8cb8e0bb124b921af36f00c69a518320322c6f4fef60"
faultedTxBlock uint32 = 23 faultedTxBlock uint32 = 23
invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA"
block20StateRootLE = "cc8315751c5b6edf39c14f1637bfa0595d6ca86b8ed4d41cb495d572b680504f" block20StateRootLE = "66900c59a718d76cc754635aa49b714cc79dc820b4a34290a19c6dfecd56b03a"
) )
var ( var (