Skip to content

Commit

Permalink
native: introduce attribute pricing
Browse files Browse the repository at this point in the history
Port the neo-project/neo#2916.

Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
  • Loading branch information
AnnaShaleva committed Sep 21, 2023
1 parent ee09eb8 commit b92c851
Show file tree
Hide file tree
Showing 13 changed files with 346 additions and 16 deletions.
10 changes: 10 additions & 0 deletions pkg/compiler/native_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ func TestLedgerVMStates(t *testing.T) {
require.EqualValues(t, ledger.BreakState, vmstate.Break)
}

func TestPolicyAttributeType(t *testing.T) {
require.EqualValues(t, policy.HighPriority, 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 {
method string
params []string
Expand Down Expand Up @@ -179,6 +187,8 @@ func TestNativeHelpersCompile(t *testing.T) {
{"setFeePerByte", []string{"42"}},
{"setStoragePrice", []string{"42"}},
{"unblockAccount", []string{u160}},
{"getAttributeFee", []string{"1"}},
{"setAttributeFee", []string{"1", "123"}},
})
runNativeTestCases(t, cs.Ledger.ContractMD, "ledger", []nativeTestCase{
{"currentHash", nil},
Expand Down
53 changes: 43 additions & 10 deletions pkg/core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2485,7 +2485,7 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
if 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() {
attrs := t.GetAttributes(transaction.NotaryAssistedT)
if len(attrs) != 0 {
Expand All @@ -2508,7 +2508,7 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
return err
}
}
err = bc.verifyTxWitnesses(t, nil, isPartialTx)
err = bc.verifyTxWitnesses(t, nil, isPartialTx, netFee)
if err != nil {
return err
}
Expand Down Expand Up @@ -2536,6 +2536,32 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
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 (
feeCache map[transaction.AttrType]int64
feeSum int64
)
for _, attr := range tx.Attributes {
if feeCache == nil {
feeCache = make(map[transaction.AttrType]int64)
}
base, ok := feeCache[attr.Type]
if !ok {
base = bc.contracts.Policy.GetAttributeFeeInternal(bc.dao, attr.Type)
feeCache[attr.Type] = base
}
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 {
for i := range tx.Attributes {
switch attrType := tx.Attributes[i].Type; attrType {
Expand Down Expand Up @@ -2885,17 +2911,24 @@ func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transa
// transaction. It can reorder them by ScriptHash, because that's required to
// 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
// 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).
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)
gasLimit := t.NetworkFee - int64(t.Size())*bc.FeePerByte()
if bc.P2PSigExtensionsEnabled() {
attrs := t.GetAttributes(transaction.NotaryAssistedT)
if len(attrs) != 0 {
na := attrs[0].Value.(*transaction.NotaryAssisted)
gasLimit -= (int64(na.NKeys) + 1) * bc.contracts.Notary.GetNotaryServiceFeePerKey(bc.dao)
var gasLimit int64
if len(verificationFee) == 0 {
gasLimit = t.NetworkFee - int64(t.Size())*bc.FeePerByte() - bc.CalculateAttributesFee(t)
if bc.P2PSigExtensionsEnabled() {
attrs := t.GetAttributes(transaction.NotaryAssistedT)
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 {
gasConsumed, err := bc.verifyHashAgainstScript(t.Signers[i].Account, &t.Scripts[i], interopCtx, gasLimit)
Expand Down
12 changes: 12 additions & 0 deletions pkg/core/native/native_nep17.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,18 @@ func toUint32(s stackitem.Item) uint32 {
return uint32(uint64Value)
}

func toUint8(s stackitem.Item) uint8 {
bigInt := toBigInt(s)
if !bigInt.IsUint64() {
panic("bigint is not an uint64")
}
uint64Value := bigInt.Uint64()
if uint64Value > math.MaxUint8 {
panic("bigint does not fit into uint8")
}
return uint8(uint64Value)
}

func toInt64(s stackitem.Item) int64 {
bigInt := toBigInt(s)
if !bigInt.IsInt64() {
Expand Down
67 changes: 67 additions & 0 deletions pkg/core/native/native_test/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ import (
"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/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/smartcontract/callflag"
"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 {
Expand Down Expand Up @@ -39,6 +45,67 @@ func TestPolicy_StoragePriceCache(t *testing.T) {
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) {
c := newPolicyClient(t)
e := c.Executor
Expand Down
81 changes: 81 additions & 0 deletions pkg/core/native/policy.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package native

import (
"encoding/hex"
"fmt"
"math/big"
"sort"
Expand All @@ -11,6 +12,7 @@ import (
"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/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/callflag"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
Expand All @@ -24,6 +26,8 @@ const (
defaultExecFeeFactor = interop.DefaultBaseExecFee
defaultFeePerByte = 1000
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 = 100000

Expand All @@ -33,9 +37,13 @@ const (
maxFeePerByte = 100_000_000
// maxStoragePrice is the maximum allowed price for a byte of storage.
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 = 15
// attributeFeePrefix is a prefix used to store attribute fee.
attributeFeePrefix = 20
)

var (
Expand All @@ -59,6 +67,7 @@ type PolicyCache struct {
feePerByte int64
maxVerificationGas int64
storagePrice uint32
attributeFee map[transaction.AttrType]uint32
blockedAccounts []util.Uint160
}

Expand All @@ -76,6 +85,10 @@ func (c *PolicyCache) Copy() dao.NativeContractCache {

func copyPolicyCache(src, dst *PolicyCache) {
*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))
copy(dst.blockedAccounts, src.blockedAccounts)
}
Expand Down Expand Up @@ -112,6 +125,17 @@ func newPolicy() *Policy {
md = newMethodAndPrice(p.setStoragePrice, 1<<15, callflag.States)
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,
manifest.NewParameter("value", smartcontract.IntegerType))
md = newMethodAndPrice(p.setFeePerByte, 1<<15, callflag.States)
Expand Down Expand Up @@ -146,6 +170,7 @@ func (p *Policy) Initialize(ic *interop.Context) error {
feePerByte: defaultFeePerByte,
maxVerificationGas: defaultMaxVerificationGas,
storagePrice: DefaultStoragePrice,
attributeFee: map[transaction.AttrType]uint32{},
blockedAccounts: make([]util.Uint160, 0),
}
ic.DAO.SetCache(p.ID, cache)
Expand Down Expand Up @@ -183,6 +208,25 @@ func (p *Policy) fillCacheFromDAO(cache *PolicyCache, d *dao.Simple) error {
if fErr != nil {
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
}

Expand Down Expand Up @@ -290,6 +334,43 @@ func (p *Policy) setStoragePrice(ic *interop.Context, args []stackitem.Item) sta
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.
func (p *Policy) setFeePerByte(ic *interop.Context, args []stackitem.Item) stackitem.Item {
value := toBigInt(args[0]).Int64()
Expand Down
17 changes: 17 additions & 0 deletions pkg/core/transaction/attrtype.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ const (
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 {
switch a {
case ConflictsT:
Expand All @@ -29,3 +38,11 @@ func (a AttrType) allowMultiple() bool {
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
}
Loading

0 comments on commit b92c851

Please sign in to comment.