diff --git a/docs/rpc.md b/docs/rpc.md index ce5700114..f12a7abca 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -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 `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 Methods listed down below are not going to be supported for various reasons diff --git a/internal/fakechain/fakechain.go b/internal/fakechain/fakechain.go index 5268c4525..ef8145858 100644 --- a/internal/fakechain/fakechain.go +++ b/internal/fakechain/fakechain.go @@ -254,13 +254,18 @@ func (chain *FakeChain) GetNextBlockValidators() ([]*keys.PublicKey, error) { panic("TODO") } -// ForEachNEP17Transfer implements Blockchainer interface. -func (chain *FakeChain) ForEachNEP17Transfer(util.Uint160, func(*state.NEP17Transfer) (bool, error)) error { +// GetNEP17Contracts implements Blockchainer interface. +func (chain *FakeChain) GetNEP17Contracts() []util.Uint160 { panic("TODO") } -// GetNEP17Balances implements Blockchainer interface. -func (chain *FakeChain) GetNEP17Balances(util.Uint160) *state.NEP17Balances { +// GetNEP17LastUpdated implements Blockchainer interface. +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") } diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 689ea3be6..0dd9f174c 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -43,7 +43,7 @@ import ( // Tuning parameters. const ( headerBatchCount = 2000 - version = "0.1.0" + version = "0.1.1" defaultInitialGAS = 52000000_00000000 defaultMemPoolSize = 50000 @@ -52,7 +52,8 @@ const ( defaultMaxBlockSystemFee = 900000000000 defaultMaxTraceableBlocks = 2102400 // 1 year of 15s blocks defaultMaxTransactionsPerBlock = 512 - headerVerificationGasLimit = 3_00000000 // 3 GAS + // HeaderVerificationGasLimit is the maximum amount of GAS for block header verification. + HeaderVerificationGasLimit = 3_00000000 // 3 GAS ) var ( @@ -989,14 +990,11 @@ func (bc *Blockchain) processNEP17Transfer(cache *dao.Cached, h util.Uint256, b Tx: h, } if !fromAddr.Equals(util.Uint160{}) { - balances, err := cache.GetNEP17Balances(fromAddr) + balances, err := cache.GetNEP17TransferInfo(fromAddr) if err != nil { return } - bs := balances.Trackers[id] - bs.Balance = *new(big.Int).Sub(&bs.Balance, amount) - bs.LastUpdatedBlock = b.Index - balances.Trackers[id] = bs + balances.LastUpdated[id] = b.Index transfer.Amount = *new(big.Int).Sub(&transfer.Amount, amount) balances.NewBatch, err = cache.AppendNEP17Transfer(fromAddr, balances.NextTransferBatch, balances.NewBatch, transfer) @@ -1006,19 +1004,16 @@ func (bc *Blockchain) processNEP17Transfer(cache *dao.Cached, h util.Uint256, b if balances.NewBatch { balances.NextTransferBatch++ } - if err := cache.PutNEP17Balances(fromAddr, balances); err != nil { + if err := cache.PutNEP17TransferInfo(fromAddr, balances); err != nil { return } } if !toAddr.Equals(util.Uint160{}) { - balances, err := cache.GetNEP17Balances(toAddr) + balances, err := cache.GetNEP17TransferInfo(toAddr) if err != nil { return } - bs := balances.Trackers[id] - bs.Balance = *new(big.Int).Add(&bs.Balance, amount) - bs.LastUpdatedBlock = b.Index - balances.Trackers[id] = bs + balances.LastUpdated[id] = b.Index transfer.Amount = *amount balances.NewBatch, err = cache.AppendNEP17Transfer(toAddr, @@ -1029,7 +1024,7 @@ func (bc *Blockchain) processNEP17Transfer(cache *dao.Cached, h util.Uint256, b if balances.NewBatch { balances.NextTransferBatch++ } - if err := cache.PutNEP17Balances(toAddr, balances); err != nil { + if err := cache.PutNEP17TransferInfo(toAddr, balances); err != nil { 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. 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 { return nil } @@ -1057,34 +1052,34 @@ func (bc *Blockchain) ForEachNEP17Transfer(acc util.Uint160, f func(*state.NEP17 return nil } -// GetNEP17Balances returns NEP17 balances for the acc. -func (bc *Blockchain) GetNEP17Balances(acc util.Uint160) *state.NEP17Balances { - bs, err := bc.dao.GetNEP17Balances(acc) +// GetNEP17Contracts returns the list of deployed NEP17 contracts. +func (bc *Blockchain) GetNEP17Contracts() []util.Uint160 { + 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 { - return nil + return nil, err } - return bs + return info.LastUpdated, nil } // GetUtilityTokenBalance returns utility token (GAS) balance for the acc. func (bc *Blockchain) GetUtilityTokenBalance(acc util.Uint160) *big.Int { - bs, err := bc.dao.GetNEP17Balances(acc) - if err != nil { + bs := bc.contracts.GAS.BalanceOf(bc.dao, acc) + if bs == nil { return big.NewInt(0) } - balance := bs.Trackers[bc.contracts.GAS.ID].Balance - return &balance + return bs } // GetGoverningTokenBalance returns governing token (NEO) balance and the height // of the last balance change for the account. func (bc *Blockchain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint32) { - bs, err := bc.dao.GetNEP17Balances(acc) - if err != nil { - return big.NewInt(0), 0 - } - neo := bs.Trackers[bc.contracts.NEO.ID] - return &neo.Balance, neo.LastUpdatedBlock + return bc.contracts.NEO.BalanceOf(bc.dao, acc) } // GetNotaryBalance returns Notary deposit amount for the specified account. @@ -1873,7 +1868,7 @@ func (bc *Blockchain) verifyHeaderWitnesses(currHeader, prevHeader *block.Header } else { 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. diff --git a/pkg/core/blockchainer/blockchainer.go b/pkg/core/blockchainer/blockchainer.go index 9ea0ad873..73a5d36d5 100644 --- a/pkg/core/blockchainer/blockchainer.go +++ b/pkg/core/blockchainer/blockchainer.go @@ -47,7 +47,8 @@ type Blockchainer interface { GetNativeContractScriptHash(string) (util.Uint160, error) GetNatives() []state.NativeContract GetNextBlockValidators() ([]*keys.PublicKey, error) - GetNEP17Balances(util.Uint160) *state.NEP17Balances + GetNEP17Contracts() []util.Uint160 + GetNEP17LastUpdated(acc util.Uint160) (map[int32]uint32, error) GetNotaryContractScriptHash() util.Uint160 GetNotaryBalance(acc util.Uint160) *big.Int GetPolicer() Policer diff --git a/pkg/core/dao/cacheddao.go b/pkg/core/dao/cacheddao.go index f23bb61df..342b9d17b 100644 --- a/pkg/core/dao/cacheddao.go +++ b/pkg/core/dao/cacheddao.go @@ -13,7 +13,7 @@ import ( // objects in the storeBlock(). type Cached struct { DAO - balances map[util.Uint160]*state.NEP17Balances + balances map[util.Uint160]*state.NEP17TransferInfo transfers map[util.Uint160]map[uint32]*state.NEP17TransferLog dropNEP17Cache bool @@ -21,21 +21,21 @@ type Cached struct { // NewCached returns new Cached wrapping around given backing store. 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) return &Cached{d.GetWrapped(), balances, transfers, false} } -// GetNEP17Balances retrieves NEP17Balances for the acc. -func (cd *Cached) GetNEP17Balances(acc util.Uint160) (*state.NEP17Balances, error) { +// GetNEP17TransferInfo retrieves NEP17TransferInfo for the acc. +func (cd *Cached) GetNEP17TransferInfo(acc util.Uint160) (*state.NEP17TransferInfo, error) { if bs := cd.balances[acc]; bs != nil { return bs, nil } - return cd.DAO.GetNEP17Balances(acc) + return cd.DAO.GetNEP17TransferInfo(acc) } -// PutNEP17Balances saves NEP17Balances for the acc. -func (cd *Cached) PutNEP17Balances(acc util.Uint160, bs *state.NEP17Balances) error { +// PutNEP17TransferInfo saves NEP17TransferInfo for the acc. +func (cd *Cached) PutNEP17TransferInfo(acc util.Uint160, bs *state.NEP17TransferInfo) error { cd.balances[acc] = bs return nil } @@ -88,7 +88,7 @@ func (cd *Cached) Persist() (int, error) { // caches (accounts/transfer data) in any way. if ok { if cd.dropNEP17Cache { - lowerCache.balances = make(map[util.Uint160]*state.NEP17Balances) + lowerCache.balances = make(map[util.Uint160]*state.NEP17TransferInfo) } var simpleCache *Simple for simpleCache == nil { @@ -105,7 +105,7 @@ func (cd *Cached) Persist() (int, error) { buf := io.NewBufBinWriter() for acc, bs := range cd.balances { - err := cd.DAO.putNEP17Balances(acc, bs, buf) + err := cd.DAO.putNEP17TransferInfo(acc, bs, buf) if err != nil { return 0, err } diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index 96df67a27..de57e7d29 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -42,7 +42,7 @@ type DAO interface { GetCurrentBlockHeight() (uint32, error) GetCurrentHeaderHeight() (i uint32, h util.Uint256, err 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) GetStorageItem(id int32, key []byte) state.StorageItem GetStorageItems(id int32) (map[string]state.StorageItem, error) @@ -55,7 +55,7 @@ type DAO interface { PutAppExecResult(aer *state.AppExecResult, buf *io.BufBinWriter) error PutContractID(id int32, hash util.Uint160) 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 PutStorageItem(id int32, key []byte, si state.StorageItem) error PutVersion(v string) error @@ -63,7 +63,7 @@ type DAO interface { StoreAsBlock(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 - 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. @@ -142,12 +142,12 @@ func (dao *Simple) GetContractScriptHash(id int32) (util.Uint160, error) { return *data, nil } -// -- start nep17 balances. +// -- start nep17 transfer info. -// GetNEP17Balances retrieves nep17 balances from the cache. -func (dao *Simple) GetNEP17Balances(acc util.Uint160) (*state.NEP17Balances, error) { - key := storage.AppendPrefix(storage.STNEP17Balances, acc.BytesBE()) - bs := state.NewNEP17Balances() +// GetNEP17TransferInfo retrieves nep17 transfer info from the cache. +func (dao *Simple) GetNEP17TransferInfo(acc util.Uint160) (*state.NEP17TransferInfo, error) { + key := storage.AppendPrefix(storage.STNEP17TransferInfo, acc.BytesBE()) + bs := state.NewNEP17TransferInfo() err := dao.GetAndDecode(bs, key) if err != nil && err != storage.ErrKeyNotFound { return nil, err @@ -155,17 +155,17 @@ func (dao *Simple) GetNEP17Balances(acc util.Uint160) (*state.NEP17Balances, err return bs, nil } -// PutNEP17Balances saves nep17 balances from the cache. -func (dao *Simple) PutNEP17Balances(acc util.Uint160, bs *state.NEP17Balances) error { - return dao.putNEP17Balances(acc, bs, io.NewBufBinWriter()) +// PutNEP17TransferInfo saves nep17 transfer info in the cache. +func (dao *Simple) PutNEP17TransferInfo(acc util.Uint160, bs *state.NEP17TransferInfo) error { + return dao.putNEP17TransferInfo(acc, bs, io.NewBufBinWriter()) } -func (dao *Simple) putNEP17Balances(acc util.Uint160, bs *state.NEP17Balances, buf *io.BufBinWriter) error { - key := storage.AppendPrefix(storage.STNEP17Balances, acc.BytesBE()) +func (dao *Simple) putNEP17TransferInfo(acc util.Uint160, bs *state.NEP17TransferInfo, buf *io.BufBinWriter) error { + key := storage.AppendPrefix(storage.STNEP17TransferInfo, acc.BytesBE()) return dao.putWithBuffer(bs, key, buf) } -// -- end nep17 balances. +// -- end nep17 transfer info. // -- start transfer log. diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go index 8847bb92a..4f0f2f566 100644 --- a/pkg/core/helper_test.go +++ b/pkg/core/helper_test.go @@ -713,8 +713,8 @@ func checkFAULTState(t *testing.T, result *state.AppExecResult) { } func checkBalanceOf(t *testing.T, chain *Blockchain, addr util.Uint160, expected int) { - balance := chain.GetNEP17Balances(addr).Trackers[chain.contracts.GAS.ID] - require.Equal(t, int64(expected), balance.Balance.Int64()) + balance := chain.GetUtilityTokenBalance(addr) + require.Equal(t, int64(expected), balance.Int64()) } type NotaryFeerStub struct { diff --git a/pkg/core/native/management.go b/pkg/core/native/management.go index a5eb71c52..edb9236a8 100644 --- a/pkg/core/native/management.go +++ b/pkg/core/native/management.go @@ -33,6 +33,8 @@ type Management struct { mtx sync.RWMutex 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 ( @@ -63,6 +65,7 @@ func newManagement() *Management { var m = &Management{ ContractMD: *interop.NewContractMD(nativenames.Management, managementContractID), contracts: make(map[util.Uint160]*state.Contract), + nep17: make(map[util.Uint160]struct{}), } defer m.UpdateHash() @@ -471,6 +474,9 @@ func (m *Management) OnPersist(ic *interop.Context) error { } m.mtx.Lock() m.contracts[md.Hash] = cs + if md.Manifest.IsStandardSupported(manifest.NEP17StandardName) { + m.nep17[md.Hash] = struct{}{} + } m.mtx.Unlock() } @@ -492,6 +498,9 @@ func (m *Management) InitializeCache(d dao.DAO) error { return } m.contracts[cs.Hash] = cs + if cs.Manifest.IsStandardSupported(manifest.NEP17StandardName) { + m.nep17[cs.Hash] = struct{}{} + } }) return initErr } @@ -507,14 +516,33 @@ func (m *Management) PostPersist(ic *interop.Context) error { if err != nil { // Contract was destroyed. delete(m.contracts, h) + delete(m.nep17, h) continue } m.contracts[h] = newCs + if newCs.Manifest.IsStandardSupported(manifest.NEP17StandardName) { + m.nep17[h] = struct{}{} + } else { + delete(m.nep17, h) + } } m.mtx.Unlock() 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. func (m *Management) Initialize(ic *interop.Context) error { if err := setIntWithKey(m.ID, ic.DAO, keyMinimumDeploymentFee, defaultMinimumDeploymentFee); err != nil { diff --git a/pkg/core/native/management_test.go b/pkg/core/native/management_test.go index fedb9ed0e..ca71e4488 100644 --- a/pkg/core/native/management_test.go +++ b/pkg/core/native/management_test.go @@ -83,3 +83,50 @@ func TestManagement_Initialize(t *testing.T) { 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()) +} diff --git a/pkg/core/native/native_gas.go b/pkg/core/native/native_gas.go index 016f30b44..496a22c60 100644 --- a/pkg/core/native/native_gas.go +++ b/pkg/core/native/native_gas.go @@ -5,6 +5,7 @@ import ( "fmt" "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/runtime" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" @@ -138,6 +139,11 @@ func (g *GAS) PostPersist(ic *interop.Context) error { 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) { s, err := smartcontract.CreateDefaultMultiSigRedeemScript(ic.Chain.GetStandByValidators()) if err != nil { diff --git a/pkg/core/native/native_neo.go b/pkg/core/native/native_neo.go index 6f5fb1e9e..e81b658c1 100644 --- a/pkg/core/native/native_neo.go +++ b/pkg/core/native/native_neo.go @@ -1020,6 +1020,20 @@ func (n *NEO) GetNextBlockValidatorsInternal() keys.PublicKeys { 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 { arr := make([]stackitem.Item, len(pubs)) for i := range pubs { diff --git a/pkg/core/native/native_nep17.go b/pkg/core/native/native_nep17.go index 3a6612121..d32d3b925 100644 --- a/pkg/core/native/native_nep17.go +++ b/pkg/core/native/native_nep17.go @@ -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 { 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) - si := ic.DAO.GetStorageItem(c.ID, key) + si := d.GetStorageItem(c.ID, key) if si == nil { - return stackitem.NewBigInteger(big.NewInt(0)) + return big.NewInt(0) } balance, err := c.balFromBytes(&si) if err != nil { 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) { diff --git a/pkg/core/native_gas_test.go b/pkg/core/native_gas_test.go index dbc358f95..7eea4c132 100644 --- a/pkg/core/native_gas_test.go +++ b/pkg/core/native_gas_test.go @@ -76,12 +76,9 @@ func TestGAS_Roundtrip(t *testing.T) { bc := newTestChain(t) getUtilityTokenBalance := func(bc *Blockchain, acc util.Uint160) (*big.Int, uint32) { - bs, err := bc.dao.GetNEP17Balances(acc) - if err != nil { - return big.NewInt(0), 0 - } - balance := bs.Trackers[bc.contracts.GAS.ID] - return &balance.Balance, balance.LastUpdatedBlock + lub, err := bc.GetNEP17LastUpdated(acc) + require.NoError(t, err) + return bc.GetUtilityTokenBalance(acc), lub[bc.contracts.GAS.ID] } initialBalance, _ := getUtilityTokenBalance(bc, neoOwner) diff --git a/pkg/core/native_management_test.go b/pkg/core/native_management_test.go index 57ec4c54e..82f105581 100644 --- a/pkg/core/native_management_test.go +++ b/pkg/core/native_management_test.go @@ -607,3 +607,18 @@ func TestMinimumDeploymentFee(t *testing.T) { 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()) + }) +} diff --git a/pkg/core/state/nep17.go b/pkg/core/state/nep17.go index dd692993a..0da72b58e 100644 --- a/pkg/core/state/nep17.go +++ b/pkg/core/state/nep17.go @@ -11,15 +11,6 @@ import ( // NEP17TransferBatchSize is the maximum number of entries for NEP17TransferLog. 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. type NEP17TransferLog struct { Raw []byte @@ -44,46 +35,44 @@ type NEP17Transfer struct { Tx util.Uint256 } -// NEP17Balances is a map of the NEP17 contract IDs -// to the corresponding structures. -type NEP17Balances struct { - Trackers map[int32]NEP17Tracker +// NEP17TransferInfo stores map of the NEP17 contract IDs to the balance's last updated +// block trackers along with information about NEP17 transfer batch. +type NEP17TransferInfo struct { + LastUpdated map[int32]uint32 // NextTransferBatch stores an index of the next transfer batch. NextTransferBatch uint32 // NewBatch is true if batch with the `NextTransferBatch` index should be created. NewBatch bool } -// NewNEP17Balances returns new NEP17Balances. -func NewNEP17Balances() *NEP17Balances { - return &NEP17Balances{ - Trackers: make(map[int32]NEP17Tracker), +// NewNEP17TransferInfo returns new NEP17TransferInfo. +func NewNEP17TransferInfo() *NEP17TransferInfo { + return &NEP17TransferInfo{ + LastUpdated: make(map[int32]uint32), } } // DecodeBinary implements io.Serializable interface. -func (bs *NEP17Balances) DecodeBinary(r *io.BinReader) { +func (bs *NEP17TransferInfo) DecodeBinary(r *io.BinReader) { bs.NextTransferBatch = r.ReadU32LE() bs.NewBatch = r.ReadBool() lenBalances := r.ReadVarUint() - m := make(map[int32]NEP17Tracker, lenBalances) + m := make(map[int32]uint32, lenBalances) for i := 0; i < int(lenBalances); i++ { key := int32(r.ReadU32LE()) - var tr NEP17Tracker - tr.DecodeBinary(r) - m[key] = tr + m[key] = r.ReadU32LE() } - bs.Trackers = m + bs.LastUpdated = m } // EncodeBinary implements io.Serializable interface. -func (bs *NEP17Balances) EncodeBinary(w *io.BinWriter) { +func (bs *NEP17TransferInfo) EncodeBinary(w *io.BinWriter) { w.WriteU32LE(bs.NextTransferBatch) w.WriteBool(bs.NewBatch) - w.WriteVarUint(uint64(len(bs.Trackers))) - for k, v := range bs.Trackers { + w.WriteVarUint(uint64(len(bs.LastUpdated))) + for k, v := range bs.LastUpdated { w.WriteU32LE(uint32(k)) - v.EncodeBinary(w) + w.WriteU32LE(v) } } @@ -138,18 +127,6 @@ func (lg *NEP17TransferLog) Size() int { 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. func (t *NEP17Transfer) EncodeBinary(w *io.BinWriter) { w.WriteU32LE(uint32(t.Asset)) diff --git a/pkg/core/state/nep17_test.go b/pkg/core/state/nep17_test.go index 5a8f2db46..b21d1cdf7 100644 --- a/pkg/core/state/nep17_test.go +++ b/pkg/core/state/nep17_test.go @@ -38,15 +38,6 @@ func TestNEP17TransferLog_Append(t *testing.T) { 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) { expected := &NEP17Transfer{ Asset: 123, diff --git a/pkg/core/storage/store.go b/pkg/core/storage/store.go index fe2d15703..540fdcc39 100644 --- a/pkg/core/storage/store.go +++ b/pkg/core/storage/store.go @@ -8,19 +8,19 @@ import ( // KeyPrefix constants. const ( - DataBlock KeyPrefix = 0x01 - DataTransaction KeyPrefix = 0x02 - DataMPT KeyPrefix = 0x03 - STAccount KeyPrefix = 0x40 - STNotification KeyPrefix = 0x4d - STContractID KeyPrefix = 0x51 - STStorage KeyPrefix = 0x70 - STNEP17Transfers KeyPrefix = 0x72 - STNEP17Balances KeyPrefix = 0x73 - IXHeaderHashList KeyPrefix = 0x80 - SYSCurrentBlock KeyPrefix = 0xc0 - SYSCurrentHeader KeyPrefix = 0xc1 - SYSVersion KeyPrefix = 0xf0 + DataBlock KeyPrefix = 0x01 + DataTransaction KeyPrefix = 0x02 + DataMPT KeyPrefix = 0x03 + STAccount KeyPrefix = 0x40 + STNotification KeyPrefix = 0x4d + STContractID KeyPrefix = 0x51 + STStorage KeyPrefix = 0x70 + STNEP17Transfers KeyPrefix = 0x72 + STNEP17TransferInfo KeyPrefix = 0x73 + IXHeaderHashList KeyPrefix = 0x80 + SYSCurrentBlock KeyPrefix = 0xc0 + SYSCurrentHeader KeyPrefix = 0xc1 + SYSVersion KeyPrefix = 0xf0 ) const ( diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 2dd03bc8b..76f967d8b 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -40,6 +40,7 @@ import ( "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/util" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "go.uber.org/zap" ) @@ -669,28 +670,65 @@ func (s *Server) getNEP17Balances(ps request.Params) (interface{}, *response.Err return nil, response.ErrInvalidParams } - as := s.chain.GetNEP17Balances(u) bs := &result.NEP17Balances{ Address: address.Uint160ToString(u), Balances: []result.NEP17Balance{}, } - if as != nil { - cache := make(map[int32]util.Uint160) - for id, bal := range as.Trackers { - h, err := s.getHash(id, cache) - if err != nil { - continue - } - bs.Balances = append(bs.Balances, result.NEP17Balance{ - Asset: h, - Amount: bal.Balance.String(), - LastUpdated: bal.LastUpdatedBlock, - }) + lastUpdated, err := s.chain.GetNEP17LastUpdated(u) + if err != nil { + return nil, response.NewRPCError("Failed to get NEP17 last updated block", err.Error(), err) + } + bw := io.NewBufBinWriter() + for _, h := range s.chain.GetNEP17Contracts() { + balance, err := s.getNEP17Balance(h, u, bw) + if err != nil { + continue } + 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 } +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) { var start, end uint64 var limit, page int diff --git a/pkg/smartcontract/manifest/manifest.go b/pkg/smartcontract/manifest/manifest.go index 8fde178dc..65582fa58 100644 --- a/pkg/smartcontract/manifest/manifest.go +++ b/pkg/smartcontract/manifest/manifest.go @@ -119,6 +119,16 @@ func (m *Manifest) IsValid(hash util.Uint160) error { 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. func (m *Manifest) ToStackItem() (stackitem.Item, error) { groups := make([]stackitem.Item, len(m.Groups)) diff --git a/pkg/smartcontract/manifest/manifest_test.go b/pkg/smartcontract/manifest/manifest_test.go index 4ac0589b7..82fcc13b4 100644 --- a/pkg/smartcontract/manifest/manifest_test.go +++ b/pkg/smartcontract/manifest/manifest_test.go @@ -429,3 +429,15 @@ func TestExtraToStackItem(t *testing.T) { 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")) +}