Merge pull request #2093 from nspcc-dev/states-exchange/drop-nep17-balance-state

core: implement dynamic NEP17 balances tracking
This commit is contained in:
Roman Khimov 2021-07-29 19:08:42 +03:00 committed by GitHub
commit 06c3dda5d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 303 additions and 144 deletions

View file

@ -97,6 +97,25 @@ it only works for native contracts.
This method doesn't work for the Ledger contract, you can get data via regular This method doesn't work for the Ledger contract, you can get data via regular
`getblock` and `getrawtransaction` calls. `getblock` and `getrawtransaction` calls.
#### `getnep17balances`
neo-go's implementation of `getnep17balances` does not perform tracking of NEP17
balances for each account as it is done in the C# node. Instead, neo-go node
maintains the list of NEP17-compliant contracts, i.e. those contracts that have
`NEP-17` declared in the supported standards section of the manifest. Each time
`getnep17balances` is queried, neo-go node asks every NEP17 contract for the
account balance by invoking `balanceOf` method with the corresponding args.
Invocation GAS limit is set to be 3 GAS. All non-zero NEP17 balances are included
in the RPC call result.
Thus, if NEP17 token contract doesn't have `NEP-17` standard declared in the list
of supported standards but emits proper NEP17 `Transfer` notifications, the token
balance won't be shown in the list of NEP17 balances returned by the neo-go node
(unlike the C# node behavior). However, transfer logs of such token are still
available via `getnep17transfers` RPC call.
The behaviour of the `LastUpdatedBlock` tracking matches the C# node's one.
### Unsupported methods ### Unsupported methods
Methods listed down below are not going to be supported for various reasons Methods listed down below are not going to be supported for various reasons

View file

@ -254,13 +254,18 @@ func (chain *FakeChain) GetNextBlockValidators() ([]*keys.PublicKey, error) {
panic("TODO") panic("TODO")
} }
// ForEachNEP17Transfer implements Blockchainer interface. // GetNEP17Contracts implements Blockchainer interface.
func (chain *FakeChain) ForEachNEP17Transfer(util.Uint160, func(*state.NEP17Transfer) (bool, error)) error { func (chain *FakeChain) GetNEP17Contracts() []util.Uint160 {
panic("TODO") panic("TODO")
} }
// GetNEP17Balances implements Blockchainer interface. // GetNEP17LastUpdated implements Blockchainer interface.
func (chain *FakeChain) GetNEP17Balances(util.Uint160) *state.NEP17Balances { func (chain *FakeChain) GetNEP17LastUpdated(acc util.Uint160) (map[int32]uint32, error) {
panic("TODO")
}
// ForEachNEP17Transfer implements Blockchainer interface.
func (chain *FakeChain) ForEachNEP17Transfer(util.Uint160, func(*state.NEP17Transfer) (bool, error)) error {
panic("TODO") panic("TODO")
} }

View file

@ -43,7 +43,7 @@ import (
// Tuning parameters. // Tuning parameters.
const ( const (
headerBatchCount = 2000 headerBatchCount = 2000
version = "0.1.0" version = "0.1.1"
defaultInitialGAS = 52000000_00000000 defaultInitialGAS = 52000000_00000000
defaultMemPoolSize = 50000 defaultMemPoolSize = 50000
@ -52,7 +52,8 @@ const (
defaultMaxBlockSystemFee = 900000000000 defaultMaxBlockSystemFee = 900000000000
defaultMaxTraceableBlocks = 2102400 // 1 year of 15s blocks defaultMaxTraceableBlocks = 2102400 // 1 year of 15s blocks
defaultMaxTransactionsPerBlock = 512 defaultMaxTransactionsPerBlock = 512
headerVerificationGasLimit = 3_00000000 // 3 GAS // HeaderVerificationGasLimit is the maximum amount of GAS for block header verification.
HeaderVerificationGasLimit = 3_00000000 // 3 GAS
) )
var ( var (
@ -989,14 +990,11 @@ func (bc *Blockchain) processNEP17Transfer(cache *dao.Cached, h util.Uint256, b
Tx: h, Tx: h,
} }
if !fromAddr.Equals(util.Uint160{}) { if !fromAddr.Equals(util.Uint160{}) {
balances, err := cache.GetNEP17Balances(fromAddr) balances, err := cache.GetNEP17TransferInfo(fromAddr)
if err != nil { if err != nil {
return return
} }
bs := balances.Trackers[id] balances.LastUpdated[id] = b.Index
bs.Balance = *new(big.Int).Sub(&bs.Balance, amount)
bs.LastUpdatedBlock = b.Index
balances.Trackers[id] = bs
transfer.Amount = *new(big.Int).Sub(&transfer.Amount, amount) transfer.Amount = *new(big.Int).Sub(&transfer.Amount, amount)
balances.NewBatch, err = cache.AppendNEP17Transfer(fromAddr, balances.NewBatch, err = cache.AppendNEP17Transfer(fromAddr,
balances.NextTransferBatch, balances.NewBatch, transfer) balances.NextTransferBatch, balances.NewBatch, transfer)
@ -1006,19 +1004,16 @@ func (bc *Blockchain) processNEP17Transfer(cache *dao.Cached, h util.Uint256, b
if balances.NewBatch { if balances.NewBatch {
balances.NextTransferBatch++ balances.NextTransferBatch++
} }
if err := cache.PutNEP17Balances(fromAddr, balances); err != nil { if err := cache.PutNEP17TransferInfo(fromAddr, balances); err != nil {
return return
} }
} }
if !toAddr.Equals(util.Uint160{}) { if !toAddr.Equals(util.Uint160{}) {
balances, err := cache.GetNEP17Balances(toAddr) balances, err := cache.GetNEP17TransferInfo(toAddr)
if err != nil { if err != nil {
return return
} }
bs := balances.Trackers[id] balances.LastUpdated[id] = b.Index
bs.Balance = *new(big.Int).Add(&bs.Balance, amount)
bs.LastUpdatedBlock = b.Index
balances.Trackers[id] = bs
transfer.Amount = *amount transfer.Amount = *amount
balances.NewBatch, err = cache.AppendNEP17Transfer(toAddr, balances.NewBatch, err = cache.AppendNEP17Transfer(toAddr,
@ -1029,7 +1024,7 @@ func (bc *Blockchain) processNEP17Transfer(cache *dao.Cached, h util.Uint256, b
if balances.NewBatch { if balances.NewBatch {
balances.NextTransferBatch++ balances.NextTransferBatch++
} }
if err := cache.PutNEP17Balances(toAddr, balances); err != nil { if err := cache.PutNEP17TransferInfo(toAddr, balances); err != nil {
return return
} }
} }
@ -1037,7 +1032,7 @@ func (bc *Blockchain) processNEP17Transfer(cache *dao.Cached, h util.Uint256, b
// ForEachNEP17Transfer executes f for each nep17 transfer in log. // ForEachNEP17Transfer executes f for each nep17 transfer in log.
func (bc *Blockchain) ForEachNEP17Transfer(acc util.Uint160, f func(*state.NEP17Transfer) (bool, error)) error { func (bc *Blockchain) ForEachNEP17Transfer(acc util.Uint160, f func(*state.NEP17Transfer) (bool, error)) error {
balances, err := bc.dao.GetNEP17Balances(acc) balances, err := bc.dao.GetNEP17TransferInfo(acc)
if err != nil { if err != nil {
return nil return nil
} }
@ -1057,34 +1052,34 @@ func (bc *Blockchain) ForEachNEP17Transfer(acc util.Uint160, f func(*state.NEP17
return nil return nil
} }
// GetNEP17Balances returns NEP17 balances for the acc. // GetNEP17Contracts returns the list of deployed NEP17 contracts.
func (bc *Blockchain) GetNEP17Balances(acc util.Uint160) *state.NEP17Balances { func (bc *Blockchain) GetNEP17Contracts() []util.Uint160 {
bs, err := bc.dao.GetNEP17Balances(acc) return bc.contracts.Management.GetNEP17Contracts()
}
// GetNEP17LastUpdated returns a set of contract ids with the corresponding last updated
// block indexes.
func (bc *Blockchain) GetNEP17LastUpdated(acc util.Uint160) (map[int32]uint32, error) {
info, err := bc.dao.GetNEP17TransferInfo(acc)
if err != nil { if err != nil {
return nil return nil, err
} }
return bs return info.LastUpdated, nil
} }
// GetUtilityTokenBalance returns utility token (GAS) balance for the acc. // GetUtilityTokenBalance returns utility token (GAS) balance for the acc.
func (bc *Blockchain) GetUtilityTokenBalance(acc util.Uint160) *big.Int { func (bc *Blockchain) GetUtilityTokenBalance(acc util.Uint160) *big.Int {
bs, err := bc.dao.GetNEP17Balances(acc) bs := bc.contracts.GAS.BalanceOf(bc.dao, acc)
if err != nil { if bs == nil {
return big.NewInt(0) return big.NewInt(0)
} }
balance := bs.Trackers[bc.contracts.GAS.ID].Balance return bs
return &balance
} }
// GetGoverningTokenBalance returns governing token (NEO) balance and the height // GetGoverningTokenBalance returns governing token (NEO) balance and the height
// of the last balance change for the account. // of the last balance change for the account.
func (bc *Blockchain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint32) { func (bc *Blockchain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint32) {
bs, err := bc.dao.GetNEP17Balances(acc) return bc.contracts.NEO.BalanceOf(bc.dao, acc)
if err != nil {
return big.NewInt(0), 0
}
neo := bs.Trackers[bc.contracts.NEO.ID]
return &neo.Balance, neo.LastUpdatedBlock
} }
// GetNotaryBalance returns Notary deposit amount for the specified account. // GetNotaryBalance returns Notary deposit amount for the specified account.
@ -1873,7 +1868,7 @@ func (bc *Blockchain) verifyHeaderWitnesses(currHeader, prevHeader *block.Header
} else { } else {
hash = prevHeader.NextConsensus hash = prevHeader.NextConsensus
} }
return bc.VerifyWitness(hash, currHeader, &currHeader.Script, headerVerificationGasLimit) return bc.VerifyWitness(hash, currHeader, &currHeader.Script, HeaderVerificationGasLimit)
} }
// GoverningTokenHash returns the governing token (NEO) native contract hash. // GoverningTokenHash returns the governing token (NEO) native contract hash.

View file

@ -47,7 +47,8 @@ type Blockchainer interface {
GetNativeContractScriptHash(string) (util.Uint160, error) GetNativeContractScriptHash(string) (util.Uint160, error)
GetNatives() []state.NativeContract GetNatives() []state.NativeContract
GetNextBlockValidators() ([]*keys.PublicKey, error) GetNextBlockValidators() ([]*keys.PublicKey, error)
GetNEP17Balances(util.Uint160) *state.NEP17Balances GetNEP17Contracts() []util.Uint160
GetNEP17LastUpdated(acc util.Uint160) (map[int32]uint32, error)
GetNotaryContractScriptHash() util.Uint160 GetNotaryContractScriptHash() util.Uint160
GetNotaryBalance(acc util.Uint160) *big.Int GetNotaryBalance(acc util.Uint160) *big.Int
GetPolicer() Policer GetPolicer() Policer

View file

@ -13,7 +13,7 @@ import (
// objects in the storeBlock(). // objects in the storeBlock().
type Cached struct { type Cached struct {
DAO DAO
balances map[util.Uint160]*state.NEP17Balances balances map[util.Uint160]*state.NEP17TransferInfo
transfers map[util.Uint160]map[uint32]*state.NEP17TransferLog transfers map[util.Uint160]map[uint32]*state.NEP17TransferLog
dropNEP17Cache bool dropNEP17Cache bool
@ -21,21 +21,21 @@ type Cached struct {
// NewCached returns new Cached wrapping around given backing store. // NewCached returns new Cached wrapping around given backing store.
func NewCached(d DAO) *Cached { func NewCached(d DAO) *Cached {
balances := make(map[util.Uint160]*state.NEP17Balances) balances := make(map[util.Uint160]*state.NEP17TransferInfo)
transfers := make(map[util.Uint160]map[uint32]*state.NEP17TransferLog) transfers := make(map[util.Uint160]map[uint32]*state.NEP17TransferLog)
return &Cached{d.GetWrapped(), balances, transfers, false} return &Cached{d.GetWrapped(), balances, transfers, false}
} }
// GetNEP17Balances retrieves NEP17Balances for the acc. // GetNEP17TransferInfo retrieves NEP17TransferInfo for the acc.
func (cd *Cached) GetNEP17Balances(acc util.Uint160) (*state.NEP17Balances, error) { func (cd *Cached) GetNEP17TransferInfo(acc util.Uint160) (*state.NEP17TransferInfo, error) {
if bs := cd.balances[acc]; bs != nil { if bs := cd.balances[acc]; bs != nil {
return bs, nil return bs, nil
} }
return cd.DAO.GetNEP17Balances(acc) return cd.DAO.GetNEP17TransferInfo(acc)
} }
// PutNEP17Balances saves NEP17Balances for the acc. // PutNEP17TransferInfo saves NEP17TransferInfo for the acc.
func (cd *Cached) PutNEP17Balances(acc util.Uint160, bs *state.NEP17Balances) error { func (cd *Cached) PutNEP17TransferInfo(acc util.Uint160, bs *state.NEP17TransferInfo) error {
cd.balances[acc] = bs cd.balances[acc] = bs
return nil return nil
} }
@ -88,7 +88,7 @@ func (cd *Cached) Persist() (int, error) {
// caches (accounts/transfer data) in any way. // caches (accounts/transfer data) in any way.
if ok { if ok {
if cd.dropNEP17Cache { if cd.dropNEP17Cache {
lowerCache.balances = make(map[util.Uint160]*state.NEP17Balances) lowerCache.balances = make(map[util.Uint160]*state.NEP17TransferInfo)
} }
var simpleCache *Simple var simpleCache *Simple
for simpleCache == nil { for simpleCache == nil {
@ -105,7 +105,7 @@ func (cd *Cached) Persist() (int, error) {
buf := io.NewBufBinWriter() buf := io.NewBufBinWriter()
for acc, bs := range cd.balances { for acc, bs := range cd.balances {
err := cd.DAO.putNEP17Balances(acc, bs, buf) err := cd.DAO.putNEP17TransferInfo(acc, bs, buf)
if err != nil { if err != nil {
return 0, err return 0, err
} }

View file

@ -42,7 +42,7 @@ type DAO interface {
GetCurrentBlockHeight() (uint32, error) GetCurrentBlockHeight() (uint32, error)
GetCurrentHeaderHeight() (i uint32, h util.Uint256, err error) GetCurrentHeaderHeight() (i uint32, h util.Uint256, err error)
GetHeaderHashes() ([]util.Uint256, error) GetHeaderHashes() ([]util.Uint256, error)
GetNEP17Balances(acc util.Uint160) (*state.NEP17Balances, error) GetNEP17TransferInfo(acc util.Uint160) (*state.NEP17TransferInfo, error)
GetNEP17TransferLog(acc util.Uint160, index uint32) (*state.NEP17TransferLog, error) GetNEP17TransferLog(acc util.Uint160, index uint32) (*state.NEP17TransferLog, error)
GetStorageItem(id int32, key []byte) state.StorageItem GetStorageItem(id int32, key []byte) state.StorageItem
GetStorageItems(id int32) (map[string]state.StorageItem, error) GetStorageItems(id int32) (map[string]state.StorageItem, error)
@ -55,7 +55,7 @@ type DAO interface {
PutAppExecResult(aer *state.AppExecResult, buf *io.BufBinWriter) error PutAppExecResult(aer *state.AppExecResult, buf *io.BufBinWriter) error
PutContractID(id int32, hash util.Uint160) error PutContractID(id int32, hash util.Uint160) error
PutCurrentHeader(hashAndIndex []byte) error PutCurrentHeader(hashAndIndex []byte) error
PutNEP17Balances(acc util.Uint160, bs *state.NEP17Balances) error PutNEP17TransferInfo(acc util.Uint160, bs *state.NEP17TransferInfo) error
PutNEP17TransferLog(acc util.Uint160, index uint32, lg *state.NEP17TransferLog) error PutNEP17TransferLog(acc util.Uint160, index uint32, lg *state.NEP17TransferLog) error
PutStorageItem(id int32, key []byte, si state.StorageItem) error PutStorageItem(id int32, key []byte, si state.StorageItem) error
PutVersion(v string) error PutVersion(v string) error
@ -63,7 +63,7 @@ type DAO interface {
StoreAsBlock(block *block.Block, buf *io.BufBinWriter) error StoreAsBlock(block *block.Block, buf *io.BufBinWriter) error
StoreAsCurrentBlock(block *block.Block, buf *io.BufBinWriter) error StoreAsCurrentBlock(block *block.Block, buf *io.BufBinWriter) error
StoreAsTransaction(tx *transaction.Transaction, index uint32, buf *io.BufBinWriter) error StoreAsTransaction(tx *transaction.Transaction, index uint32, buf *io.BufBinWriter) error
putNEP17Balances(acc util.Uint160, bs *state.NEP17Balances, buf *io.BufBinWriter) error putNEP17TransferInfo(acc util.Uint160, bs *state.NEP17TransferInfo, buf *io.BufBinWriter) error
} }
// Simple is memCached wrapper around DB, simple DAO implementation. // Simple is memCached wrapper around DB, simple DAO implementation.
@ -142,12 +142,12 @@ func (dao *Simple) GetContractScriptHash(id int32) (util.Uint160, error) {
return *data, nil return *data, nil
} }
// -- start nep17 balances. // -- start nep17 transfer info.
// GetNEP17Balances retrieves nep17 balances from the cache. // GetNEP17TransferInfo retrieves nep17 transfer info from the cache.
func (dao *Simple) GetNEP17Balances(acc util.Uint160) (*state.NEP17Balances, error) { func (dao *Simple) GetNEP17TransferInfo(acc util.Uint160) (*state.NEP17TransferInfo, error) {
key := storage.AppendPrefix(storage.STNEP17Balances, acc.BytesBE()) key := storage.AppendPrefix(storage.STNEP17TransferInfo, acc.BytesBE())
bs := state.NewNEP17Balances() bs := state.NewNEP17TransferInfo()
err := dao.GetAndDecode(bs, key) err := dao.GetAndDecode(bs, key)
if err != nil && err != storage.ErrKeyNotFound { if err != nil && err != storage.ErrKeyNotFound {
return nil, err return nil, err
@ -155,17 +155,17 @@ func (dao *Simple) GetNEP17Balances(acc util.Uint160) (*state.NEP17Balances, err
return bs, nil return bs, nil
} }
// PutNEP17Balances saves nep17 balances from the cache. // PutNEP17TransferInfo saves nep17 transfer info in the cache.
func (dao *Simple) PutNEP17Balances(acc util.Uint160, bs *state.NEP17Balances) error { func (dao *Simple) PutNEP17TransferInfo(acc util.Uint160, bs *state.NEP17TransferInfo) error {
return dao.putNEP17Balances(acc, bs, io.NewBufBinWriter()) return dao.putNEP17TransferInfo(acc, bs, io.NewBufBinWriter())
} }
func (dao *Simple) putNEP17Balances(acc util.Uint160, bs *state.NEP17Balances, buf *io.BufBinWriter) error { func (dao *Simple) putNEP17TransferInfo(acc util.Uint160, bs *state.NEP17TransferInfo, buf *io.BufBinWriter) error {
key := storage.AppendPrefix(storage.STNEP17Balances, acc.BytesBE()) key := storage.AppendPrefix(storage.STNEP17TransferInfo, acc.BytesBE())
return dao.putWithBuffer(bs, key, buf) return dao.putWithBuffer(bs, key, buf)
} }
// -- end nep17 balances. // -- end nep17 transfer info.
// -- start transfer log. // -- start transfer log.

View file

@ -713,8 +713,8 @@ func checkFAULTState(t *testing.T, result *state.AppExecResult) {
} }
func checkBalanceOf(t *testing.T, chain *Blockchain, addr util.Uint160, expected int) { func checkBalanceOf(t *testing.T, chain *Blockchain, addr util.Uint160, expected int) {
balance := chain.GetNEP17Balances(addr).Trackers[chain.contracts.GAS.ID] balance := chain.GetUtilityTokenBalance(addr)
require.Equal(t, int64(expected), balance.Balance.Int64()) require.Equal(t, int64(expected), balance.Int64())
} }
type NotaryFeerStub struct { type NotaryFeerStub struct {

View file

@ -33,6 +33,8 @@ type Management struct {
mtx sync.RWMutex mtx sync.RWMutex
contracts map[util.Uint160]*state.Contract contracts map[util.Uint160]*state.Contract
// nep17 is a map of NEP17-compliant contracts which is updated with every PostPersist.
nep17 map[util.Uint160]struct{}
} }
const ( const (
@ -63,6 +65,7 @@ func newManagement() *Management {
var m = &Management{ var m = &Management{
ContractMD: *interop.NewContractMD(nativenames.Management, managementContractID), ContractMD: *interop.NewContractMD(nativenames.Management, managementContractID),
contracts: make(map[util.Uint160]*state.Contract), contracts: make(map[util.Uint160]*state.Contract),
nep17: make(map[util.Uint160]struct{}),
} }
defer m.UpdateHash() defer m.UpdateHash()
@ -471,6 +474,9 @@ func (m *Management) OnPersist(ic *interop.Context) error {
} }
m.mtx.Lock() m.mtx.Lock()
m.contracts[md.Hash] = cs m.contracts[md.Hash] = cs
if md.Manifest.IsStandardSupported(manifest.NEP17StandardName) {
m.nep17[md.Hash] = struct{}{}
}
m.mtx.Unlock() m.mtx.Unlock()
} }
@ -492,6 +498,9 @@ func (m *Management) InitializeCache(d dao.DAO) error {
return return
} }
m.contracts[cs.Hash] = cs m.contracts[cs.Hash] = cs
if cs.Manifest.IsStandardSupported(manifest.NEP17StandardName) {
m.nep17[cs.Hash] = struct{}{}
}
}) })
return initErr return initErr
} }
@ -507,14 +516,33 @@ func (m *Management) PostPersist(ic *interop.Context) error {
if err != nil { if err != nil {
// Contract was destroyed. // Contract was destroyed.
delete(m.contracts, h) delete(m.contracts, h)
delete(m.nep17, h)
continue continue
} }
m.contracts[h] = newCs m.contracts[h] = newCs
if newCs.Manifest.IsStandardSupported(manifest.NEP17StandardName) {
m.nep17[h] = struct{}{}
} else {
delete(m.nep17, h)
}
} }
m.mtx.Unlock() m.mtx.Unlock()
return nil return nil
} }
// GetNEP17Contracts returns hashes of all deployed contracts that support NEP17 standard. The list
// is updated every PostPersist, so until PostPersist is called, the result for the previous block
// is returned.
func (m *Management) GetNEP17Contracts() []util.Uint160 {
m.mtx.RLock()
result := make([]util.Uint160, 0, len(m.nep17))
for h := range m.nep17 {
result = append(result, h)
}
m.mtx.RUnlock()
return result
}
// Initialize implements Contract interface. // Initialize implements Contract interface.
func (m *Management) Initialize(ic *interop.Context) error { func (m *Management) Initialize(ic *interop.Context) error {
if err := setIntWithKey(m.ID, ic.DAO, keyMinimumDeploymentFee, defaultMinimumDeploymentFee); err != nil { if err := setIntWithKey(m.ID, ic.DAO, keyMinimumDeploymentFee, defaultMinimumDeploymentFee); err != nil {

View file

@ -83,3 +83,50 @@ func TestManagement_Initialize(t *testing.T) {
require.Error(t, mgmt.InitializeCache(d)) require.Error(t, mgmt.InitializeCache(d))
}) })
} }
func TestManagement_GetNEP17Contracts(t *testing.T) {
mgmt := newManagement()
d := dao.NewCached(dao.NewSimple(storage.NewMemoryStore(), false))
err := mgmt.Initialize(&interop.Context{DAO: d})
require.NoError(t, err)
require.Empty(t, mgmt.GetNEP17Contracts())
// Deploy NEP17 contract
script := []byte{byte(opcode.RET)}
sender := util.Uint160{1, 2, 3}
ne, err := nef.NewFile(script)
require.NoError(t, err)
manif := manifest.NewManifest("Test")
manif.ABI.Methods = append(manif.ABI.Methods, manifest.Method{
Name: "dummy",
ReturnType: smartcontract.VoidType,
Parameters: []manifest.Parameter{},
})
manif.SupportedStandards = []string{manifest.NEP17StandardName}
c1, err := mgmt.Deploy(d, sender, ne, manif)
require.NoError(t, err)
// PostPersist is not yet called, thus no NEP17 contracts are expected
require.Empty(t, mgmt.GetNEP17Contracts())
// Call PostPersist, check c1 contract hash is returned
require.NoError(t, mgmt.PostPersist(&interop.Context{DAO: d}))
require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts())
// Update contract
manif.ABI.Methods = append(manif.ABI.Methods, manifest.Method{
Name: "dummy2",
ReturnType: smartcontract.VoidType,
Parameters: []manifest.Parameter{},
})
c2, err := mgmt.Update(d, c1.Hash, ne, manif)
require.NoError(t, err)
// No changes expected before PostPersist call.
require.Equal(t, []util.Uint160{c1.Hash}, mgmt.GetNEP17Contracts())
// Call PostPersist, check c2 contract hash is returned
require.NoError(t, mgmt.PostPersist(&interop.Context{DAO: d}))
require.Equal(t, []util.Uint160{c2.Hash}, mgmt.GetNEP17Contracts())
}

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"math/big" "math/big"
"github.com/nspcc-dev/neo-go/pkg/core/dao"
"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/interop/runtime" "github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
@ -138,6 +139,11 @@ func (g *GAS) PostPersist(ic *interop.Context) error {
return nil return nil
} }
// BalanceOf returns native GAS token balance for the acc.
func (g *GAS) BalanceOf(d dao.DAO, acc util.Uint160) *big.Int {
return g.balanceOfInternal(d, acc)
}
func getStandbyValidatorsHash(ic *interop.Context) (util.Uint160, error) { func getStandbyValidatorsHash(ic *interop.Context) (util.Uint160, error) {
s, err := smartcontract.CreateDefaultMultiSigRedeemScript(ic.Chain.GetStandByValidators()) s, err := smartcontract.CreateDefaultMultiSigRedeemScript(ic.Chain.GetStandByValidators())
if err != nil { if err != nil {

View file

@ -1020,6 +1020,20 @@ func (n *NEO) GetNextBlockValidatorsInternal() keys.PublicKeys {
return n.nextValidators.Load().(keys.PublicKeys).Copy() return n.nextValidators.Load().(keys.PublicKeys).Copy()
} }
// BalanceOf returns native NEO token balance for the acc.
func (n *NEO) BalanceOf(d dao.DAO, acc util.Uint160) (*big.Int, uint32) {
key := makeAccountKey(acc)
si := d.GetStorageItem(n.ID, key)
if si == nil {
return big.NewInt(0), 0
}
st, err := state.NEOBalanceFromBytes(si)
if err != nil {
panic(fmt.Errorf("failed to decode NEO balance state: %w", err))
}
return &st.Balance, st.BalanceHeight
}
func pubsToArray(pubs keys.PublicKeys) stackitem.Item { func pubsToArray(pubs keys.PublicKeys) stackitem.Item {
arr := make([]stackitem.Item, len(pubs)) arr := make([]stackitem.Item, len(pubs))
for i := range pubs { for i := range pubs {

View file

@ -246,16 +246,20 @@ func (c *nep17TokenNative) TransferInternal(ic *interop.Context, from, to util.U
func (c *nep17TokenNative) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item { func (c *nep17TokenNative) balanceOf(ic *interop.Context, args []stackitem.Item) stackitem.Item {
h := toUint160(args[0]) h := toUint160(args[0])
return stackitem.NewBigInteger(c.balanceOfInternal(ic.DAO, h))
}
func (c *nep17TokenNative) balanceOfInternal(d dao.DAO, h util.Uint160) *big.Int {
key := makeAccountKey(h) key := makeAccountKey(h)
si := ic.DAO.GetStorageItem(c.ID, key) si := d.GetStorageItem(c.ID, key)
if si == nil { if si == nil {
return stackitem.NewBigInteger(big.NewInt(0)) return big.NewInt(0)
} }
balance, err := c.balFromBytes(&si) balance, err := c.balFromBytes(&si)
if err != nil { if err != nil {
panic(fmt.Errorf("can not deserialize balance state: %w", err)) panic(fmt.Errorf("can not deserialize balance state: %w", err))
} }
return stackitem.NewBigInteger(balance) return balance
} }
func (c *nep17TokenNative) mint(ic *interop.Context, h util.Uint160, amount *big.Int, callOnPayment bool) { func (c *nep17TokenNative) mint(ic *interop.Context, h util.Uint160, amount *big.Int, callOnPayment bool) {

View file

@ -76,12 +76,9 @@ func TestGAS_Roundtrip(t *testing.T) {
bc := newTestChain(t) bc := newTestChain(t)
getUtilityTokenBalance := func(bc *Blockchain, acc util.Uint160) (*big.Int, uint32) { getUtilityTokenBalance := func(bc *Blockchain, acc util.Uint160) (*big.Int, uint32) {
bs, err := bc.dao.GetNEP17Balances(acc) lub, err := bc.GetNEP17LastUpdated(acc)
if err != nil { require.NoError(t, err)
return big.NewInt(0), 0 return bc.GetUtilityTokenBalance(acc), lub[bc.contracts.GAS.ID]
}
balance := bs.Trackers[bc.contracts.GAS.ID]
return &balance.Balance, balance.LastUpdatedBlock
} }
initialBalance, _ := getUtilityTokenBalance(bc, neoOwner) initialBalance, _ := getUtilityTokenBalance(bc, neoOwner)

View file

@ -607,3 +607,18 @@ func TestMinimumDeploymentFee(t *testing.T) {
testGetSet(t, chain, chain.contracts.Management.Hash, "MinimumDeploymentFee", 10_00000000, 0, 0) testGetSet(t, chain, chain.contracts.Management.Hash, "MinimumDeploymentFee", 10_00000000, 0, 0)
} }
func TestManagement_GetNEP17Contracts(t *testing.T) {
t.Run("empty chain", func(t *testing.T) {
chain := newTestChain(t)
require.ElementsMatch(t, []util.Uint160{chain.contracts.NEO.Hash, chain.contracts.GAS.Hash}, chain.contracts.Management.GetNEP17Contracts())
})
t.Run("test chain", func(t *testing.T) {
chain := newTestChain(t)
initBasicChain(t, chain)
rublesHash, err := chain.GetContractScriptHash(1)
require.NoError(t, err)
require.ElementsMatch(t, []util.Uint160{chain.contracts.NEO.Hash, chain.contracts.GAS.Hash, rublesHash}, chain.contracts.Management.GetNEP17Contracts())
})
}

View file

@ -11,15 +11,6 @@ import (
// NEP17TransferBatchSize is the maximum number of entries for NEP17TransferLog. // NEP17TransferBatchSize is the maximum number of entries for NEP17TransferLog.
const NEP17TransferBatchSize = 128 const NEP17TransferBatchSize = 128
// NEP17Tracker contains info about a single account in a NEP17 contract.
type NEP17Tracker struct {
// Balance is the current balance of the account.
Balance big.Int
// LastUpdatedBlock is a number of block when last `transfer` to or from the
// account occurred.
LastUpdatedBlock uint32
}
// NEP17TransferLog is a log of NEP17 token transfers for the specific command. // NEP17TransferLog is a log of NEP17 token transfers for the specific command.
type NEP17TransferLog struct { type NEP17TransferLog struct {
Raw []byte Raw []byte
@ -44,46 +35,44 @@ type NEP17Transfer struct {
Tx util.Uint256 Tx util.Uint256
} }
// NEP17Balances is a map of the NEP17 contract IDs // NEP17TransferInfo stores map of the NEP17 contract IDs to the balance's last updated
// to the corresponding structures. // block trackers along with information about NEP17 transfer batch.
type NEP17Balances struct { type NEP17TransferInfo struct {
Trackers map[int32]NEP17Tracker LastUpdated map[int32]uint32
// NextTransferBatch stores an index of the next transfer batch. // NextTransferBatch stores an index of the next transfer batch.
NextTransferBatch uint32 NextTransferBatch uint32
// NewBatch is true if batch with the `NextTransferBatch` index should be created. // NewBatch is true if batch with the `NextTransferBatch` index should be created.
NewBatch bool NewBatch bool
} }
// NewNEP17Balances returns new NEP17Balances. // NewNEP17TransferInfo returns new NEP17TransferInfo.
func NewNEP17Balances() *NEP17Balances { func NewNEP17TransferInfo() *NEP17TransferInfo {
return &NEP17Balances{ return &NEP17TransferInfo{
Trackers: make(map[int32]NEP17Tracker), LastUpdated: make(map[int32]uint32),
} }
} }
// DecodeBinary implements io.Serializable interface. // DecodeBinary implements io.Serializable interface.
func (bs *NEP17Balances) DecodeBinary(r *io.BinReader) { func (bs *NEP17TransferInfo) DecodeBinary(r *io.BinReader) {
bs.NextTransferBatch = r.ReadU32LE() bs.NextTransferBatch = r.ReadU32LE()
bs.NewBatch = r.ReadBool() bs.NewBatch = r.ReadBool()
lenBalances := r.ReadVarUint() lenBalances := r.ReadVarUint()
m := make(map[int32]NEP17Tracker, lenBalances) m := make(map[int32]uint32, lenBalances)
for i := 0; i < int(lenBalances); i++ { for i := 0; i < int(lenBalances); i++ {
key := int32(r.ReadU32LE()) key := int32(r.ReadU32LE())
var tr NEP17Tracker m[key] = r.ReadU32LE()
tr.DecodeBinary(r)
m[key] = tr
} }
bs.Trackers = m bs.LastUpdated = m
} }
// EncodeBinary implements io.Serializable interface. // EncodeBinary implements io.Serializable interface.
func (bs *NEP17Balances) EncodeBinary(w *io.BinWriter) { func (bs *NEP17TransferInfo) EncodeBinary(w *io.BinWriter) {
w.WriteU32LE(bs.NextTransferBatch) w.WriteU32LE(bs.NextTransferBatch)
w.WriteBool(bs.NewBatch) w.WriteBool(bs.NewBatch)
w.WriteVarUint(uint64(len(bs.Trackers))) w.WriteVarUint(uint64(len(bs.LastUpdated)))
for k, v := range bs.Trackers { for k, v := range bs.LastUpdated {
w.WriteU32LE(uint32(k)) w.WriteU32LE(uint32(k))
v.EncodeBinary(w) w.WriteU32LE(v)
} }
} }
@ -138,18 +127,6 @@ func (lg *NEP17TransferLog) Size() int {
return int(lg.Raw[0]) return int(lg.Raw[0])
} }
// EncodeBinary implements io.Serializable interface.
func (t *NEP17Tracker) EncodeBinary(w *io.BinWriter) {
w.WriteVarBytes(bigint.ToBytes(&t.Balance))
w.WriteU32LE(t.LastUpdatedBlock)
}
// DecodeBinary implements io.Serializable interface.
func (t *NEP17Tracker) DecodeBinary(r *io.BinReader) {
t.Balance = *bigint.FromBytes(r.ReadVarBytes())
t.LastUpdatedBlock = r.ReadU32LE()
}
// EncodeBinary implements io.Serializable interface. // EncodeBinary implements io.Serializable interface.
func (t *NEP17Transfer) EncodeBinary(w *io.BinWriter) { func (t *NEP17Transfer) EncodeBinary(w *io.BinWriter) {
w.WriteU32LE(uint32(t.Asset)) w.WriteU32LE(uint32(t.Asset))

View file

@ -38,15 +38,6 @@ func TestNEP17TransferLog_Append(t *testing.T) {
require.True(t, cont) require.True(t, cont)
} }
func TestNEP17Tracker_EncodeBinary(t *testing.T) {
expected := &NEP17Tracker{
Balance: *big.NewInt(int64(rand.Uint64())),
LastUpdatedBlock: rand.Uint32(),
}
testserdes.EncodeDecodeBinary(t, expected, new(NEP17Tracker))
}
func TestNEP17Transfer_DecodeBinary(t *testing.T) { func TestNEP17Transfer_DecodeBinary(t *testing.T) {
expected := &NEP17Transfer{ expected := &NEP17Transfer{
Asset: 123, Asset: 123,

View file

@ -8,19 +8,19 @@ import (
// KeyPrefix constants. // KeyPrefix constants.
const ( const (
DataBlock KeyPrefix = 0x01 DataBlock KeyPrefix = 0x01
DataTransaction KeyPrefix = 0x02 DataTransaction KeyPrefix = 0x02
DataMPT KeyPrefix = 0x03 DataMPT KeyPrefix = 0x03
STAccount KeyPrefix = 0x40 STAccount KeyPrefix = 0x40
STNotification KeyPrefix = 0x4d STNotification KeyPrefix = 0x4d
STContractID KeyPrefix = 0x51 STContractID KeyPrefix = 0x51
STStorage KeyPrefix = 0x70 STStorage KeyPrefix = 0x70
STNEP17Transfers KeyPrefix = 0x72 STNEP17Transfers KeyPrefix = 0x72
STNEP17Balances KeyPrefix = 0x73 STNEP17TransferInfo KeyPrefix = 0x73
IXHeaderHashList KeyPrefix = 0x80 IXHeaderHashList KeyPrefix = 0x80
SYSCurrentBlock KeyPrefix = 0xc0 SYSCurrentBlock KeyPrefix = 0xc0
SYSCurrentHeader KeyPrefix = 0xc1 SYSCurrentHeader KeyPrefix = 0xc1
SYSVersion KeyPrefix = 0xf0 SYSVersion KeyPrefix = 0xf0
) )
const ( const (

View file

@ -40,6 +40,7 @@ import (
"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/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/emit"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -669,28 +670,65 @@ func (s *Server) getNEP17Balances(ps request.Params) (interface{}, *response.Err
return nil, response.ErrInvalidParams return nil, response.ErrInvalidParams
} }
as := s.chain.GetNEP17Balances(u)
bs := &result.NEP17Balances{ bs := &result.NEP17Balances{
Address: address.Uint160ToString(u), Address: address.Uint160ToString(u),
Balances: []result.NEP17Balance{}, Balances: []result.NEP17Balance{},
} }
if as != nil { lastUpdated, err := s.chain.GetNEP17LastUpdated(u)
cache := make(map[int32]util.Uint160) if err != nil {
for id, bal := range as.Trackers { return nil, response.NewRPCError("Failed to get NEP17 last updated block", err.Error(), err)
h, err := s.getHash(id, cache) }
if err != nil { bw := io.NewBufBinWriter()
continue for _, h := range s.chain.GetNEP17Contracts() {
} balance, err := s.getNEP17Balance(h, u, bw)
bs.Balances = append(bs.Balances, result.NEP17Balance{ if err != nil {
Asset: h, continue
Amount: bal.Balance.String(),
LastUpdated: bal.LastUpdatedBlock,
})
} }
if balance.Sign() == 0 {
continue
}
cs := s.chain.GetContractState(h)
if cs == nil {
continue
}
bs.Balances = append(bs.Balances, result.NEP17Balance{
Asset: h,
Amount: balance.String(),
LastUpdated: lastUpdated[cs.ID],
})
} }
return bs, nil return bs, nil
} }
func (s *Server) getNEP17Balance(h util.Uint160, acc util.Uint160, bw *io.BufBinWriter) (*big.Int, error) {
if bw == nil {
bw = io.NewBufBinWriter()
} else {
bw.Reset()
}
emit.AppCall(bw.BinWriter, h, "balanceOf", callflag.ReadStates, acc)
if bw.Err != nil {
return nil, fmt.Errorf("failed to create `balanceOf` invocation script: %w", bw.Err)
}
script := bw.Bytes()
tx := &transaction.Transaction{Script: script}
v := s.chain.GetTestVM(trigger.Application, tx, nil)
v.GasLimit = core.HeaderVerificationGasLimit
v.LoadScriptWithFlags(script, callflag.All)
err := v.Run()
if err != nil {
return nil, fmt.Errorf("failed to run `balanceOf` for %s: %w", h.StringLE(), err)
}
if v.Estack().Len() != 1 {
return nil, fmt.Errorf("invalid `balanceOf` return values count: expected 1, got %d", v.Estack().Len())
}
res, err := v.Estack().Pop().Item().TryInteger()
if err != nil {
return nil, fmt.Errorf("unexpected `balanceOf` result type: %w", err)
}
return res, nil
}
func getTimestampsAndLimit(ps request.Params, index int) (uint64, uint64, int, int, error) { func getTimestampsAndLimit(ps request.Params, index int) (uint64, uint64, int, int, error) {
var start, end uint64 var start, end uint64
var limit, page int var limit, page int

View file

@ -119,6 +119,16 @@ func (m *Manifest) IsValid(hash util.Uint160) error {
return Permissions(m.Permissions).AreValid() return Permissions(m.Permissions).AreValid()
} }
// IsStandardSupported denotes whether the specified standard supported by the contract.
func (m *Manifest) IsStandardSupported(standard string) bool {
for _, st := range m.SupportedStandards {
if st == standard {
return true
}
}
return false
}
// ToStackItem converts Manifest to stackitem.Item. // ToStackItem converts Manifest to stackitem.Item.
func (m *Manifest) ToStackItem() (stackitem.Item, error) { func (m *Manifest) ToStackItem() (stackitem.Item, error) {
groups := make([]stackitem.Item, len(m.Groups)) groups := make([]stackitem.Item, len(m.Groups))

View file

@ -429,3 +429,15 @@ func TestExtraToStackItem(t *testing.T) {
require.Equal(t, tc.expected, string(actual)) require.Equal(t, tc.expected, string(actual))
} }
} }
func TestManifest_IsStandardSupported(t *testing.T) {
m := &Manifest{
SupportedStandards: []string{NEP17StandardName, NEP17Payable, NEP11Payable},
}
for _, st := range m.SupportedStandards {
require.True(t, m.IsStandardSupported(st))
}
require.False(t, m.IsStandardSupported(NEP11StandardName))
require.False(t, m.IsStandardSupported(""))
require.False(t, m.IsStandardSupported("unknown standard"))
}