Skip to content

Commit

Permalink
core: fix nspcc-dev#2845 system fee refund attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
ZhangTao1596 committed Feb 9, 2023
1 parent 1d6e48e commit 18b7900
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 28 deletions.
22 changes: 19 additions & 3 deletions pkg/core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
cache = bc.dao.GetPrivate()
aerCache = bc.dao.GetPrivate()
appExecResults = make([]*state.AppExecResult, 0, 2+len(block.Transactions))
txesConsumed = make(map[util.Uint256]int64, len(block.Transactions))
aerchan = make(chan *state.AppExecResult, len(block.Transactions)/8) // Tested 8 and 4 with no practical difference, but feel free to test more and tune.
aerdone = make(chan error)
)
Expand Down Expand Up @@ -1500,7 +1501,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
close(aerdone)
}()
_ = cache.GetItemCtx() // Prime serialization context cache (it'll be reused by upper layer DAOs).
aer, v, err := bc.runPersist(bc.contracts.GetPersistScript(), block, cache, trigger.OnPersist, nil)
aer, v, err := bc.runPersist(bc.contracts.GetPersistScript(), block, cache, trigger.OnPersist, nil, nil)
if err != nil {
// Release goroutines, don't care about errors, we already have one.
close(aerchan)
Expand Down Expand Up @@ -1533,6 +1534,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
zap.Error(err))
faultException = err.Error()
}
txesConsumed[tx.Hash()] = v.GasConsumed()
aer := &state.AppExecResult{
Container: tx.Hash(),
Execution: state.Execution{
Expand All @@ -1548,7 +1550,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
aerchan <- aer
}

aer, _, err = bc.runPersist(bc.contracts.GetPostPersistScript(), block, cache, trigger.PostPersist, v)
aer, _, err = bc.runPersist(bc.contracts.GetPostPersistScript(), block, cache, trigger.PostPersist, v, txesConsumed)
if err != nil {
// Release goroutines, don't care about errors, we already have one.
close(aerchan)
Expand Down Expand Up @@ -1683,13 +1685,14 @@ func (bc *Blockchain) IsExtensibleAllowed(u util.Uint160) bool {
return n < len(us)
}

func (bc *Blockchain) runPersist(script []byte, block *block.Block, cache *dao.Simple, trig trigger.Type, v *vm.VM) (*state.AppExecResult, *vm.VM, error) {
func (bc *Blockchain) runPersist(script []byte, block *block.Block, cache *dao.Simple, trig trigger.Type, v *vm.VM, txesConsumed map[util.Uint256]int64) (*state.AppExecResult, *vm.VM, error) {
systemInterop := bc.newInteropContext(trig, cache, block, nil)
if v == nil {
v = systemInterop.SpawnVM()
} else {
systemInterop.ReuseVM(v)
}
systemInterop.TxesConsumed = txesConsumed
v.LoadScriptWithFlags(script, callflag.All)
if err := systemInterop.Exec(); err != nil {
return nil, v, fmt.Errorf("VM has failed: %w", err)
Expand Down Expand Up @@ -2248,6 +2251,11 @@ func (bc *Blockchain) FeePerByte() int64 {
return bc.contracts.Policy.GetFeePerByteInternal(bc.dao)
}

// GasRefundFee returns extra fee for system fee refundable transaction
func (bc *Blockchain) SystemFeeRefundCost() int64 {
return bc.contracts.Policy.GetSystemFeeRefundCostInternal(bc.dao)
}

// GetMemPool returns the memory pool of the blockchain.
func (bc *Blockchain) GetMemPool() *mempool.Pool {
return bc.memPool
Expand Down Expand Up @@ -2367,6 +2375,9 @@ func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.
needNetworkFee += (int64(na.NKeys) + 1) * bc.contracts.Notary.GetNotaryServiceFeePerKey(bc.dao)
}
}
if len(t.GetAttributes(transaction.RefundableSystemFeeT)) > 0 {
needNetworkFee += bc.SystemFeeRefundCost()
}
netFee := t.NetworkFee - needNetworkFee
if netFee < 0 {
return fmt.Errorf("%w: net fee is %v, need %v", ErrTxSmallNetworkFee, t.NetworkFee, needNetworkFee)
Expand Down Expand Up @@ -2480,6 +2491,11 @@ func (bc *Blockchain) verifyTxAttributes(d *dao.Simple, tx *transaction.Transact
if !tx.HasSigner(bc.contracts.Notary.Hash) {
return fmt.Errorf("%w: NotaryAssisted attribute was found, but transaction is not signed by the Notary native contract", ErrInvalidAttribute)
}
case transaction.RefundableSystemFeeT:
state := bc.GetContractState(tx.Sender())
if state != nil {
return fmt.Errorf("%w: RefundableSystemFee attribute was found, but transaction sender is contract", ErrInvalidAttribute)
}
default:
if !bc.config.ReservedAttributes && attrType >= transaction.ReservedLowerBound && attrType <= transaction.ReservedUpperBound {
return fmt.Errorf("%w: attribute of reserved type was found, but ReservedAttributes are disabled", ErrInvalidAttribute)
Expand Down
32 changes: 17 additions & 15 deletions pkg/core/interop/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,23 @@ type Ledger interface {

// Context represents context in which interops are executed.
type Context struct {
Chain Ledger
Container hash.Hashable
Network uint32
Hardforks map[string]uint32
Natives []Contract
Trigger trigger.Type
Block *block.Block
NonceData [16]byte
Tx *transaction.Transaction
DAO *dao.Simple
Notifications []state.NotificationEvent
Log *zap.Logger
VM *vm.VM
Functions []Function
Invocations map[util.Uint160]int
Chain Ledger
Container hash.Hashable
Network uint32
Hardforks map[string]uint32
Natives []Contract
Trigger trigger.Type
Block *block.Block
NonceData [16]byte
Tx *transaction.Transaction
DAO *dao.Simple
Notifications []state.NotificationEvent
Log *zap.Logger
VM *vm.VM
Functions []Function
Invocations map[util.Uint160]int
TxesConsumed map[util.Uint256]int64

cancelFuncs []context.CancelFunc
getContract func(*dao.Simple, util.Uint160) (*state.Contract, error)
baseExecFee int64
Expand Down
9 changes: 9 additions & 0 deletions pkg/core/native/native_gas.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,15 @@ func (g *GAS) OnPersist(ic *interop.Context) error {

// PostPersist implements the Contract interface.
func (g *GAS) PostPersist(ic *interop.Context) error {
for _, tx := range ic.Block.Transactions {
attrs := tx.GetAttributes(transaction.RefundableSystemFeeT)
if len(attrs) != 0 {
consumed := ic.TxesConsumed[tx.Hash()]
if consumed < tx.SystemFee {
g.mint(ic, tx.Sender(), big.NewInt(tx.SystemFee-consumed), false)
}
}
}
return nil
}

Expand Down
41 changes: 37 additions & 4 deletions pkg/core/native/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ import (
const (
policyContractID = -7

defaultExecFeeFactor = interop.DefaultBaseExecFee
defaultFeePerByte = 1000
defaultMaxVerificationGas = 1_50000000
defaultExecFeeFactor = interop.DefaultBaseExecFee
defaultFeePerByte = 1000
defaultMaxVerificationGas = 1_50000000
defaultSystemFeeRefundCost = 0_10000000
// DefaultStoragePrice is the price to pay for 1 byte of storage.
DefaultStoragePrice = 100000

Expand All @@ -33,7 +34,8 @@ const (
maxFeePerByte = 100_000_000
// maxStoragePrice is the maximum allowed price for a byte of storage.
maxStoragePrice = 10000000

// maxSystemFeeRefundCost is the maximun allowed extra fee for gas refundable transaction
maxSystemFeeRefundCost = 1_00000000
// blockedAccountPrefix is a prefix used to store blocked account.
blockedAccountPrefix = 15
)
Expand All @@ -46,6 +48,8 @@ var (
feePerByteKey = []byte{10}
// storagePriceKey is a key used to store storage price.
storagePriceKey = []byte{19}
// systemFeeRefundCostKey is a key usesd to store gas refund fee
systemFeeRefundCostKey = []byte{20}
)

// Policy represents Policy native contract.
Expand All @@ -59,6 +63,7 @@ type PolicyCache struct {
feePerByte int64
maxVerificationGas int64
storagePrice uint32
systemFeeRefundCost int64
blockedAccounts []util.Uint160
}

Expand Down Expand Up @@ -127,6 +132,11 @@ func newPolicy() *Policy {
md = newMethodAndPrice(p.unblockAccount, 1<<15, callflag.States)
p.AddMethod(md, desc)

desc = newDescriptor("setSystemFeeRefundCost", smartcontract.VoidType,
manifest.NewParameter("value", smartcontract.IntegerType))
md = newMethodAndPrice(p.setSystemFeeRefundCost, 1<<15, callflag.States)
p.AddMethod(md, desc)

return p
}

Expand All @@ -140,12 +150,14 @@ func (p *Policy) Initialize(ic *interop.Context) error {
setIntWithKey(p.ID, ic.DAO, feePerByteKey, defaultFeePerByte)
setIntWithKey(p.ID, ic.DAO, execFeeFactorKey, defaultExecFeeFactor)
setIntWithKey(p.ID, ic.DAO, storagePriceKey, DefaultStoragePrice)
setIntWithKey(p.ID, ic.DAO, systemFeeRefundCostKey, defaultSystemFeeRefundCost)

cache := &PolicyCache{
execFeeFactor: defaultExecFeeFactor,
feePerByte: defaultFeePerByte,
maxVerificationGas: defaultMaxVerificationGas,
storagePrice: DefaultStoragePrice,
systemFeeRefundCost: defaultSystemFeeRefundCost,
blockedAccounts: make([]util.Uint160, 0),
}
ic.DAO.SetCache(p.ID, cache)
Expand All @@ -168,6 +180,7 @@ func (p *Policy) fillCacheFromDAO(cache *PolicyCache, d *dao.Simple) error {
cache.feePerByte = getIntWithKey(p.ID, d, feePerByteKey)
cache.maxVerificationGas = defaultMaxVerificationGas
cache.storagePrice = uint32(getIntWithKey(p.ID, d, storagePriceKey))
cache.systemFeeRefundCost = getIntWithKey(p.ID, d, systemFeeRefundCostKey)

cache.blockedAccounts = make([]util.Uint160, 0)
var fErr error
Expand Down Expand Up @@ -354,6 +367,26 @@ func (p *Policy) unblockAccount(ic *interop.Context, args []stackitem.Item) stac
return stackitem.NewBool(true)
}

func (p *Policy) GetSystemFeeRefundCostInternal(d *dao.Simple) int64 {
cache := d.GetROCache(p.ID).(*PolicyCache)
return cache.systemFeeRefundCost
}

// setSystemFeeRefundCost is a Policy contract method that set extra network fee for gas refundable transaction.
func (p *Policy) setSystemFeeRefundCost(ic *interop.Context, args []stackitem.Item) stackitem.Item {
value := toBigInt(args[0]).Int64()
if value < 0 || value > maxSystemFeeRefundCost {
panic(fmt.Errorf("SystemFeeRefundCost shouldn't be negative or greater than %d", maxSystemFeeRefundCost))
}
if !p.NEO.checkCommittee(ic) {
panic("invalid committee signature")
}
setIntWithKey(p.ID, ic.DAO, systemFeeRefundCostKey, value)
cache := ic.DAO.GetRWCache(p.ID).(*PolicyCache)
cache.systemFeeRefundCost = value
return stackitem.Null{}
}

// CheckPolicy checks whether a transaction conforms to the current policy restrictions,
// like not being signed by a blocked account or not exceeding the block-level system
// fee limit.
Expand Down
7 changes: 6 additions & 1 deletion pkg/core/transaction/attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ func (attr *Attribute) DecodeBinary(br *io.BinReader) {
attr.Value = new(Conflicts)
case NotaryAssistedT:
attr.Value = new(NotaryAssisted)
case RefundableSystemFeeT:
attr.Value = new(RefundableSystemFee)
default:
if t >= ReservedLowerBound && t <= ReservedUpperBound {
attr.Value = new(Reserved)
Expand All @@ -57,7 +59,7 @@ func (attr *Attribute) EncodeBinary(bw *io.BinWriter) {
bw.WriteB(byte(attr.Type))
switch t := attr.Type; t {
case HighPriority:
case OracleResponseT, NotValidBeforeT, ConflictsT, NotaryAssistedT:
case OracleResponseT, NotValidBeforeT, ConflictsT, NotaryAssistedT, RefundableSystemFeeT:
attr.Value.EncodeBinary(bw)
default:
if t >= ReservedLowerBound && t <= ReservedUpperBound {
Expand Down Expand Up @@ -102,6 +104,9 @@ func (attr *Attribute) UnmarshalJSON(data []byte) error {
case NotaryAssistedT.String():
attr.Type = NotaryAssistedT
attr.Value = new(NotaryAssisted)
case RefundableSystemFeeT.String():
attr.Type = RefundableSystemFeeT
attr.Value = new(RefundableSystemFee)
default:
return errors.New("wrong Type")
}
Expand Down
11 changes: 6 additions & 5 deletions pkg/core/transaction/attrtype.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ const (

// List of valid attribute types.
const (
HighPriority AttrType = 1
OracleResponseT AttrType = 0x11 // OracleResponse
NotValidBeforeT AttrType = 0x20 // NotValidBefore
ConflictsT AttrType = 0x21 // Conflicts
NotaryAssistedT AttrType = 0x22 // NotaryAssisted
HighPriority AttrType = 1
OracleResponseT AttrType = 0x11 // OracleResponse
NotValidBeforeT AttrType = 0x20 // NotValidBefore
ConflictsT AttrType = 0x21 // Conflicts
NotaryAssistedT AttrType = 0x22 // NotaryAssisted
RefundableSystemFeeT AttrType = 0x30 // RefundableSystemFee
)

func (a AttrType) allowMultiple() bool {
Expand Down
20 changes: 20 additions & 0 deletions pkg/core/transaction/refundablesystemfee.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package transaction

import (
"github.com/nspcc-dev/neo-go/pkg/io"
)

// Conflicts represents attribute for refund gas transaction.
type RefundableSystemFee struct {
}

// DecodeBinary implements the io.Serializable interface.
func (c *RefundableSystemFee) DecodeBinary(br *io.BinReader) {
}

// EncodeBinary implements the io.Serializable interface.
func (c *RefundableSystemFee) EncodeBinary(w *io.BinWriter) {
}

func (c *RefundableSystemFee) toJSONMap(m map[string]interface{}) {
}
4 changes: 4 additions & 0 deletions pkg/services/rpcsrv/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type (
CalculateClaimable(h util.Uint160, endHeight uint32) (*big.Int, error)
CurrentBlockHash() util.Uint256
FeePerByte() int64
SystemFeeRefundCost() int64
ForEachNEP11Transfer(acc util.Uint160, newestTimestamp uint64, f func(*state.NEP11Transfer) (bool, error)) error
ForEachNEP17Transfer(acc util.Uint160, newestTimestamp uint64, f func(*state.NEP17Transfer) (bool, error)) error
GetAppExecResults(util.Uint256, trigger.Type) ([]state.AppExecResult, error)
Expand Down Expand Up @@ -847,6 +848,9 @@ func (s *Server) calculateNetworkFee(reqParams params.Params) (interface{}, *neo
netFee += (int64(na.NKeys) + 1) * s.chain.GetNotaryServiceFeePerKey()
}
}
if len(tx.GetAttributes(transaction.RefundableSystemFeeT)) > 0 {
netFee += s.chain.SystemFeeRefundCost()
}
fee := s.chain.FeePerByte()
netFee += int64(size) * fee
return result.NetworkFee{Value: netFee}, nil
Expand Down

0 comments on commit 18b7900

Please sign in to comment.