From 9571ecffac7bbd6a4475f18ad4553a0e4efcf561 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 12 Feb 2021 00:01:00 +0300 Subject: [PATCH 01/17] core: add proper asset issuer for UTXO assets It just wasn't set which is wrong (AFmseVrdL9f9oyCzZefL9tG6UbvhPbdYzM was always the issuer). --- pkg/core/blockchain.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index ec9d14f57..ee728310a 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -688,6 +688,7 @@ func (bc *Blockchain) storeBlock(block *block.Block) error { Precision: t.Precision, Owner: t.Owner, Admin: t.Admin, + Issuer: t.Admin, Expiration: bc.BlockHeight() + registeredAssetLifetime, }) if err != nil { From 90a06ed7a5922ae90d5ea0b1bdce63f3ac3bf3c9 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 12 Feb 2021 00:04:13 +0300 Subject: [PATCH 02/17] rpc: fix IsFrozen JSONization for assets C# node uses `frozen` as field name here. --- pkg/rpc/client/rpc_test.go | 2 +- pkg/rpc/response/result/asset_state.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index 8369f2fb2..a0aba662e 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -178,7 +178,7 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ invoke: func(c *Client) (interface{}, error) { return c.GetAssetState(util.Uint256{}) }, - serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"id":"0xc56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b","type":0,"name":"NEO","amount":"100000000","available":"100000000","precision":0,"owner":"00","admin":"Abf2qMs1pzQb8kYk9RuxtUb9jtRKJVuBJt","issuer":"AFmseVrdL9f9oyCzZefL9tG6UbvhPbdYzM","expiration":4000000,"is_frozen":false}}`, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"id":"0xc56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b","type":0,"name":"NEO","amount":"100000000","available":"100000000","precision":0,"owner":"00","admin":"Abf2qMs1pzQb8kYk9RuxtUb9jtRKJVuBJt","issuer":"AFmseVrdL9f9oyCzZefL9tG6UbvhPbdYzM","expiration":4000000,"frozen":false}}`, result: func(c *Client) interface{} { return &result.AssetState{ ID: core.GoverningTokenID(), diff --git a/pkg/rpc/response/result/asset_state.go b/pkg/rpc/response/result/asset_state.go index 3c3d5f38d..930bdc1dd 100644 --- a/pkg/rpc/response/result/asset_state.go +++ b/pkg/rpc/response/result/asset_state.go @@ -20,7 +20,7 @@ type AssetState struct { Admin string `json:"admin"` Issuer string `json:"issuer"` Expiration uint32 `json:"expiration"` - IsFrozen bool `json:"is_frozen"` + IsFrozen bool `json:"frozen"` } // NewAssetState creates a new Asset wrapper. From c3b6405f0fe84e4ee60bfcb47e1184a8d2e75d05 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 12 Feb 2021 23:02:51 +0300 Subject: [PATCH 03/17] transaction: fix asset type JSONization It's stringy in C#. --- pkg/core/transaction/asset_type.go | 70 ++++++++++++++++++++++++++++++ pkg/rpc/client/rpc_test.go | 2 +- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/pkg/core/transaction/asset_type.go b/pkg/core/transaction/asset_type.go index 9d54eb28f..2a3caba05 100644 --- a/pkg/core/transaction/asset_type.go +++ b/pkg/core/transaction/asset_type.go @@ -1,5 +1,10 @@ package transaction +import ( + "errors" + "fmt" +) + // AssetType represents a NEO asset type. type AssetType uint8 @@ -14,3 +19,68 @@ const ( Invoice AssetType = DutyFlag | 0x18 Token AssetType = CreditFlag | 0x20 ) + +// String implements Stringer interface. +func (a AssetType) String() string { + switch a { + case CreditFlag: + return "CreditFlag" + case DutyFlag: + return "DutyFlag" + case GoverningToken: + return "GoverningToken" + case UtilityToken: + return "UtilityToken" + case Currency: + return "Currency" + case Share: + return "Share" + case Invoice: + return "Invoice" + case Token: + return "Token" + default: + return fmt.Sprintf("Unknonwn (%d)", a) + + } +} + +// MarshalJSON implements the json marshaller interface. +func (a AssetType) MarshalJSON() ([]byte, error) { + return []byte(`"` + a.String() + `"`), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (a *AssetType) UnmarshalJSON(data []byte) error { + l := len(data) + if l < 2 || data[0] != '"' || data[l-1] != '"' { + return errors.New("wrong format") + } + var err error + *a, err = AssetTypeFromString(string(data[1 : l-1])) + return err +} + +// AssetTypeFromString converts string into AssetType. +func AssetTypeFromString(s string) (AssetType, error) { + switch s { + case "CreditFlag": + return CreditFlag, nil + case "DutyFlag": + return DutyFlag, nil + case "GoverningToken": + return GoverningToken, nil + case "UtilityToken": + return UtilityToken, nil + case "Currency": + return Currency, nil + case "Share": + return Share, nil + case "Invoice": + return Invoice, nil + case "Token": + return Token, nil + default: + return 0, errors.New("bad asset type") + } +} diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index a0aba662e..f87e57296 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -178,7 +178,7 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ invoke: func(c *Client) (interface{}, error) { return c.GetAssetState(util.Uint256{}) }, - serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"id":"0xc56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b","type":0,"name":"NEO","amount":"100000000","available":"100000000","precision":0,"owner":"00","admin":"Abf2qMs1pzQb8kYk9RuxtUb9jtRKJVuBJt","issuer":"AFmseVrdL9f9oyCzZefL9tG6UbvhPbdYzM","expiration":4000000,"frozen":false}}`, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"id":"0xc56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b","type":"GoverningToken","name":"NEO","amount":"100000000","available":"100000000","precision":0,"owner":"00","admin":"Abf2qMs1pzQb8kYk9RuxtUb9jtRKJVuBJt","issuer":"AFmseVrdL9f9oyCzZefL9tG6UbvhPbdYzM","expiration":4000000,"frozen":false}}`, result: func(c *Client) interface{} { return &result.AssetState{ ID: core.GoverningTokenID(), From 9c34dea296ee532928f74096ff22c718c9b08745 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 15 Feb 2021 16:18:03 +0300 Subject: [PATCH 04/17] rpc/server: return more specific error in getblockhash --- pkg/rpc/server/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 68db88eb1..0864f4adb 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -461,7 +461,7 @@ func (s *Server) getBlockHash(reqParams request.Params) (interface{}, *response. } num, err := s.blockHeightFromParam(param) if err != nil { - return nil, response.ErrInvalidParams + return nil, err } return s.chain.GetHeaderHash(num), nil From fda8b784c8a6d48527d562fc1723780f1554db9a Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 15 Feb 2021 17:27:23 +0300 Subject: [PATCH 05/17] rpc/server: allow numbers for getblockheader call --- pkg/rpc/server/server.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 0864f4adb..98739d4b7 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -1371,9 +1371,9 @@ func (s *Server) getBlockSysFee(reqParams request.Params) (interface{}, *respons // getBlockHeader returns the corresponding block header information according to the specified script hash. func (s *Server) getBlockHeader(reqParams request.Params) (interface{}, *response.Error) { - hash, err := reqParams.ValueWithType(0, request.StringT).GetUint256() - if err != nil { - return nil, response.ErrInvalidParams + hash, respErr := s.getBlockHashFromParam(reqParams.Value(0)) + if respErr != nil { + return nil, respErr } verbose := reqParams.Value(1).GetBoolean() From a46c93f6bb01f32cdd0beba2a20f977f687aa93d Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 15 Feb 2021 17:58:39 +0300 Subject: [PATCH 06/17] rpc: fix getblocksystemfee call It should return cumulative fee and it should be wrapped into a string. --- pkg/core/blockchain.go | 10 +++++----- pkg/core/blockchainer.go | 1 + pkg/network/helper_test.go | 3 +++ pkg/rpc/server/server.go | 11 +---------- pkg/rpc/server/server_test.go | 10 +++------- 5 files changed, 13 insertions(+), 22 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index ee728310a..159a5153f 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -551,9 +551,9 @@ func (bc *Blockchain) processHeader(h *block.Header, batch storage.Batch, header return nil } -// bc.GetHeaderHash(int(endHeight)) returns sum of all system fees for blocks up to h. +// GetSystemFeeAmount returns sum of all system fees for blocks up to h. // and 0 if no such block exists. -func (bc *Blockchain) getSystemFeeAmount(h util.Uint256) uint32 { +func (bc *Blockchain) GetSystemFeeAmount(h util.Uint256) uint32 { _, sf, _ := bc.dao.GetBlock(h) return sf } @@ -582,7 +582,7 @@ func (bc *Blockchain) GetStateRoot(height uint32) (*state.MPTRootState, error) { func (bc *Blockchain) storeBlock(block *block.Block) error { cache := dao.NewCached(bc.dao) appExecResults := make([]*state.AppExecResult, 0, len(block.Transactions)) - fee := bc.getSystemFeeAmount(block.PrevHash) + fee := bc.GetSystemFeeAmount(block.PrevHash) for _, tx := range block.Transactions { fee += uint32(bc.SystemFee(tx).IntegralValue()) } @@ -1503,9 +1503,9 @@ func (bc *Blockchain) CalculateClaimable(value util.Fixed8, startHeight, endHeig startHeight++ } h := bc.GetHeaderHash(int(startHeight - 1)) - feeStart := bc.getSystemFeeAmount(h) + feeStart := bc.GetSystemFeeAmount(h) h = bc.GetHeaderHash(int(endHeight - 1)) - feeEnd := bc.getSystemFeeAmount(h) + feeEnd := bc.GetSystemFeeAmount(h) sysFeeTotal := util.Fixed8(feeEnd - feeStart) ratio := value / 100000000 diff --git a/pkg/core/blockchainer.go b/pkg/core/blockchainer.go index 76c032c61..5d72d8543 100644 --- a/pkg/core/blockchainer.go +++ b/pkg/core/blockchainer.go @@ -44,6 +44,7 @@ type Blockchainer interface { GetStateRoot(height uint32) (*state.MPTRootState, error) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem GetStorageItems(hash util.Uint160) (map[string]*state.StorageItem, error) + GetSystemFeeAmount(h util.Uint256) uint32 GetTestVM(tx *transaction.Transaction) *vm.VM GetTransaction(util.Uint256) (*transaction.Transaction, uint32, error) GetUnspentCoinState(util.Uint256) *state.UnspentCoin diff --git a/pkg/network/helper_test.go b/pkg/network/helper_test.go index 5f078fddc..8922733aa 100644 --- a/pkg/network/helper_test.go +++ b/pkg/network/helper_test.go @@ -123,6 +123,9 @@ func (chain testChain) GetStateRoot(height uint32) (*state.MPTRootState, error) func (chain testChain) GetStorageItem(scripthash util.Uint160, key []byte) *state.StorageItem { panic("TODO") } +func (chain testChain) GetSystemFeeAmount(h util.Uint256) uint32 { + panic("TODO") +} func (chain testChain) GetTestVM(tx *transaction.Transaction) *vm.VM { panic("TODO") } diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 98739d4b7..623a49e4d 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -1356,17 +1356,8 @@ func (s *Server) getBlockSysFee(reqParams request.Params) (interface{}, *respons } headerHash := s.chain.GetHeaderHash(num) - block, errBlock := s.chain.GetBlock(headerHash) - if errBlock != nil { - return 0, response.NewRPCError(errBlock.Error(), "", nil) - } - var blockSysFee util.Fixed8 - for _, tx := range block.Transactions { - blockSysFee += s.chain.SystemFee(tx) - } - - return blockSysFee, nil + return util.Fixed8FromInt64(int64(s.chain.GetSystemFeeAmount(headerHash))), nil } // getBlockHeader returns the corresponding block header information according to the specified script hash. diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 4b544610c..35b5407d9 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -548,13 +548,9 @@ var rpcTestCases = map[string][]rpcTestCase{ name: "positive", params: "[1]", result: func(e *executor) interface{} { - block, _ := e.chain.GetBlock(e.chain.GetHeaderHash(1)) - - var expectedBlockSysFee util.Fixed8 - for _, tx := range block.Transactions { - expectedBlockSysFee += e.chain.SystemFee(tx) - } - return &expectedBlockSysFee + sf := e.chain.GetSystemFeeAmount(e.chain.GetHeaderHash(1)) + r := util.Fixed8FromInt64(int64(sf)) + return &r }, }, { From 70a20ce031f0e0ff3e57a59fb66284878020aa50 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 15 Feb 2021 19:07:08 +0300 Subject: [PATCH 07/17] core: fix system fee calculation It was completely wrong starting from the genesis block. --- pkg/core/blockchain.go | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 159a5153f..8bb1b0b9a 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1577,11 +1577,44 @@ func (bc *Blockchain) NetworkFee(t *transaction.Transaction) util.Fixed8 { // SystemFee returns system fee. func (bc *Blockchain) SystemFee(t *transaction.Transaction) util.Fixed8 { - if t.Type == transaction.InvocationType { + switch t.Type { + case transaction.InvocationType: inv := t.Data.(*transaction.InvocationTX) - if inv.Version >= 1 { - return inv.Gas + return inv.Gas + case transaction.IssueType: + if t.Version >= 1 { + return util.Fixed8(0) } + var iszero = true + for i := range t.Outputs { + asset := t.Outputs[i].AssetID + if asset != UtilityTokenID() && asset != GoverningTokenID() { + iszero = false + break + } + } + if iszero { + return util.Fixed8(0) + } + case transaction.RegisterType: + reg := t.Data.(*transaction.RegisterTX) + if reg.AssetType == transaction.GoverningToken || reg.AssetType == transaction.UtilityToken { + return util.Fixed8(0) + } + case transaction.StateType: + res := util.Fixed8(0) + st := t.Data.(*transaction.StateTX) + for _, desc := range st.Descriptors { + if desc.Type == transaction.Validator && desc.Field == "Registered" { + for i := range desc.Value { + if desc.Value[i] != 0 { + res += util.Fixed8FromInt64(1000) + break + } + } + } + } + return res } return bc.GetConfig().SystemFee.TryGetValue(t.Type) } From 850d29060a5efcbb9e6781a40e2e6e679007d776 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 15 Feb 2021 22:09:35 +0300 Subject: [PATCH 08/17] result: assets are LE in JSON LE/BE split is the worst NEO feature ever. --- pkg/rpc/response/result/tx_output.go | 2 +- pkg/rpc/server/server_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/rpc/response/result/tx_output.go b/pkg/rpc/response/result/tx_output.go index 4078de755..61baa6973 100644 --- a/pkg/rpc/response/result/tx_output.go +++ b/pkg/rpc/response/result/tx_output.go @@ -20,7 +20,7 @@ func NewTxOutput(out *transaction.Output) *TransactionOutput { return &TransactionOutput{ N: out.Position, - Asset: "0x" + out.AssetID.String(), + Asset: "0x" + out.AssetID.StringLE(), Value: out.Amount, Address: addr, } diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 35b5407d9..698dbfe23 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -1213,7 +1213,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] err := json.Unmarshal(res, &txOut) require.NoErrorf(t, err, "could not parse response: %s", res) assert.Equal(t, 1, txOut.N) - assert.Equal(t, "0x9b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc5", txOut.Asset) + assert.Equal(t, "0xc56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", txOut.Asset) assert.Equal(t, util.Fixed8FromInt64(1000), txOut.Value) assert.Equal(t, "AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU", txOut.Address) }) From 1ef91fa409e72b3c244c3b0b559762a9eb6e3aec Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 15 Feb 2021 22:19:28 +0300 Subject: [PATCH 09/17] rpc: encode port numbers as proper numbers in getpeers The way C# node does it. --- pkg/rpc/client/rpc_test.go | 8 ++++---- pkg/rpc/response/result/peers.go | 6 ++++-- pkg/rpc/response/result/peers_test.go | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index f87e57296..856c72216 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -517,25 +517,25 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{ invoke: func(c *Client) (interface{}, error) { return c.GetPeers() }, - serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"unconnected":[{"address":"172.200.0.1","port":"20333"}],"connected":[{"address":"127.0.0.1","port":"20335"}],"bad":[{"address":"172.200.0.254","port":"20332"}]}}`, + serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"unconnected":[{"address":"172.200.0.1","port":20333}],"connected":[{"address":"127.0.0.1","port":20335}],"bad":[{"address":"172.200.0.254","port":20332}]}}`, result: func(c *Client) interface{} { return &result.GetPeers{ Unconnected: result.Peers{ { Address: "172.200.0.1", - Port: "20333", + Port: 20333, }, }, Connected: result.Peers{ { Address: "127.0.0.1", - Port: "20335", + Port: 20335, }, }, Bad: result.Peers{ { Address: "172.200.0.254", - Port: "20332", + Port: 20332, }, }, } diff --git a/pkg/rpc/response/result/peers.go b/pkg/rpc/response/result/peers.go index b18acc0e8..67c637c36 100644 --- a/pkg/rpc/response/result/peers.go +++ b/pkg/rpc/response/result/peers.go @@ -1,6 +1,7 @@ package result import ( + "strconv" "strings" ) @@ -18,7 +19,7 @@ type ( // Peer represents the peer. Peer struct { Address string `json:"address"` - Port string `json:"port"` + Port uint16 `json:"port"` } ) @@ -50,9 +51,10 @@ func (g *GetPeers) AddBad(addrs []string) { func (p *Peers) addPeers(addrs []string) { for i := range addrs { addressParts := strings.Split(addrs[i], ":") + port, _ := strconv.Atoi(addressParts[1]) // We know it's a good port number. peer := Peer{ Address: addressParts[0], - Port: addressParts[1], + Port: uint16(port), } *p = append(*p, peer) diff --git a/pkg/rpc/response/result/peers_test.go b/pkg/rpc/response/result/peers_test.go index 6e5a9339e..8187f4154 100644 --- a/pkg/rpc/response/result/peers_test.go +++ b/pkg/rpc/response/result/peers_test.go @@ -20,7 +20,7 @@ func TestGetPeers(t *testing.T) { require.Equal(t, 1, len(gp.Connected)) require.Equal(t, 1, len(gp.Bad)) require.Equal(t, "192.168.0.1", gp.Connected[0].Address) - require.Equal(t, "10333", gp.Connected[0].Port) + require.Equal(t, uint16(10333), gp.Connected[0].Port) require.Equal(t, "127.0.0.1", gp.Bad[0].Address) - require.Equal(t, "20333", gp.Bad[0].Port) + require.Equal(t, uint16(20333), gp.Bad[0].Port) } From 07c91ea22d10042be0348057bd1d52b769161b8e Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 15 Feb 2021 23:16:36 +0300 Subject: [PATCH 10/17] core: fix getenrollments Make getvalidators return the same set of keys with C# for mainnet. --- pkg/core/blockchain.go | 17 ++++------------- pkg/rpc/server/server_test.go | 8 ++++---- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 8bb1b0b9a..1e56785cc 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -2266,23 +2266,14 @@ func (bc *Blockchain) GetEnrollments() ([]*state.Validator, error) { for _, validator := range validators { if validator.Registered { result = append(result, validator) + continue } - } - for _, sBValidator := range uniqueSBValidators { - isAdded := false - for _, v := range result { - if v.PublicKey == sBValidator { - isAdded = true + for _, sbValidator := range uniqueSBValidators { + if validator.PublicKey.Equal(sbValidator) { + result = append(result, validator) break } } - if !isAdded { - result = append(result, &state.Validator{ - PublicKey: sBValidator, - Registered: false, - Votes: 0, - }) - } } return result, nil } diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 698dbfe23..4902396ba 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -723,12 +723,12 @@ var rpcTestCases = map[string][]rpcTestCase{ }, check: func(t *testing.T, e *executor, validators interface{}) { var expected []result.Validator - sBValidators, err := e.chain.GetStandByValidators() + enrolls, err := e.chain.GetEnrollments() require.NoError(t, err) - for _, sbValidator := range sBValidators { + for _, validator := range enrolls { expected = append(expected, result.Validator{ - PublicKey: *sbValidator, - Votes: 0, + PublicKey: *validator.PublicKey, + Votes: validator.Votes, Active: true, }) } From d4f26fe473cc3d0ad9aac48a26c9faa0a32b4328 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 16 Feb 2021 10:22:10 +0300 Subject: [PATCH 11/17] docs: update RPC documentation Add missing methods and notice a bit more implementation differences. --- docs/rpc.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/docs/rpc.md b/docs/rpc.md index 57c5513d7..0aacd9147 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -46,11 +46,15 @@ which would yield the response: | `getclaimable` | | `getconnectioncount` | | `getcontractstate` | +| `getminimumnetworkfee` | | `getnep5balances` | | `getnep5transfers` | | `getpeers` | +| `getproof` | | `getrawmempool` | | `getrawtransaction` | +| `getstateheight` | +| `getstateroot` | | `getstorage` | | `gettransactionheight` | | `gettxout` | @@ -65,9 +69,78 @@ which would yield the response: | `sendrawtransaction` | | `submitblock` | | `validateaddress` | +| `verifyproof` | #### Implementation notices +##### `getaccountstate` + +The order of assets in `balances` section may differ from the one returned by +C# implementation. Assets can still be identified by their hashes so it +shouldn't be an issue. + +##### `getapplicationlog` + +Error handling for incorrect stack items differs with C# implementation. C# +implementation substitutes `stack` and `state` arrays with "error: recursive +reference" string if there are any invalid items. NeoGo never does this, for +bad `state` items it uses byte array susbstitute with message "bad +notification: ..." (may vary depending on the problem), for incorrect `stack` +items it just omits them (still returning valid ones). + +##### `getassetstate` + +It returns "NEO" for NEO and "NEOGas" for GAS in the `name` field instead of +language-aware JSON structures. + +##### `getblock` and `getrawtransaction` + +In their verbose outputs neo-go can omit some fields with default values for +transactions, this includes: + * zero "nonce" for Miner transactions (usually nonce is not zero) + * zero "gas" for Invocation transactions (most of the time it is zero). + +##### `getclaimable` + +`claimable` array ordering differs, neo-go orders entries there by the +`end_height` field, while C# implementation orders by `txid`. + +##### `getcontractstate` + +C# implementation doesn't return `Payable` flag in its output, neo-go has +`is_payable` field in `properties` for that. + +##### `getnep5transfers` + +`received` and `sent` entries are sorted differently, C# node uses +chronological order and neo-go uses reverse chronological order (which is +important for paging support, see Extensions section down below). + +##### `getrawmempool` + +neo-go doesn't support boolean parameter to `getrawmempool` for unverified +transactions request because neo-go actually never stores unverified +transactions in the mempool. + +##### `getunclaimed` + +Numeric results are wrapped into strings in neo-go (the same way fees are +encoded) to prevent floating point rounding errors. + +##### `getunspents` + +neo-go uses standard "0xhash" syntax for `txid` and `asset_hash` fields +whereas C# module doesn't add "0x" prefix. The order of `balance` or `unspent` +entries can differ. neo-go returns all UTXO assets while C# module only tracks +and returns NEO and GAS. + +##### `getutxotransfers` + +`transactions` are sorted differently, C# node uses chronological order and +neo-go uses reverse chronological order (which is important for paging +support, see Extensions section down below). + + ##### `invokefunction` and `invoke` neo-go's implementation of `invokefunction` and `invoke` does not return `tx` From 38842531ca491d00dbe0beec7bdee26967814b1e Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 16 Feb 2021 16:30:11 +0300 Subject: [PATCH 12/17] core: drop NEP5 tracker data if balance is zero Makes no sense storing it and returning to the user (C# plugin doesn't do that). --- pkg/core/blockchain.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 1e56785cc..1644f101b 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -945,8 +945,12 @@ func (bc *Blockchain) processNEP5Transfer(cache *dao.Cached, transfer *state.NEP } bs := balances.Trackers[transfer.Asset] bs.Balance -= transfer.Amount - bs.LastUpdatedBlock = transfer.Block - balances.Trackers[transfer.Asset] = bs + if bs.Balance != 0 { + bs.LastUpdatedBlock = transfer.Block + balances.Trackers[transfer.Asset] = bs + } else { + delete(balances.Trackers, transfer.Asset) + } transfer.Amount = -transfer.Amount isBig, err := cache.AppendNEP5Transfer(transfer.From, balances.NextTransferBatch, transfer) From eb1986d2fc781f5ef7bcca89cc899b5c01e9ea24 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 16 Feb 2021 20:16:02 +0300 Subject: [PATCH 13/17] state: use big.Int for NEP5 balances and transfer amounts In general, NEP5 contracts are not limited to int64. And we have an example of pnWETH Flamingo token now (with 18 decimals) that easily overflows int64, so for correctness we need to store big.Int. And as TransferLog is shared for different purposes I've decided to not make it variable-length on Neo 2. --- pkg/core/blockchain.go | 17 ++++++++++++----- pkg/core/state/nep5.go | 32 ++++++++++++++++++++++++-------- pkg/core/state/nep5_test.go | 7 ++++--- pkg/rpc/server/server.go | 19 +++++++++++-------- 4 files changed, 51 insertions(+), 24 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 1644f101b..511044fc6 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -3,6 +3,7 @@ package core import ( "fmt" "math" + "math/big" "sort" "sync" "sync/atomic" @@ -944,20 +945,23 @@ func (bc *Blockchain) processNEP5Transfer(cache *dao.Cached, transfer *state.NEP return } bs := balances.Trackers[transfer.Asset] - bs.Balance -= transfer.Amount - if bs.Balance != 0 { + if bs.Balance == nil { + return + } + bs.Balance.Sub(bs.Balance, transfer.Amount) + if bs.Balance.Sign() > 0 { bs.LastUpdatedBlock = transfer.Block balances.Trackers[transfer.Asset] = bs } else { delete(balances.Trackers, transfer.Asset) } - transfer.Amount = -transfer.Amount + transfer.Amount.Neg(transfer.Amount) isBig, err := cache.AppendNEP5Transfer(transfer.From, balances.NextTransferBatch, transfer) if err != nil { return } - transfer.Amount = -transfer.Amount + transfer.Amount.Neg(transfer.Amount) if isBig { balances.NextTransferBatch++ } @@ -971,7 +975,10 @@ func (bc *Blockchain) processNEP5Transfer(cache *dao.Cached, transfer *state.NEP return } bs := balances.Trackers[transfer.Asset] - bs.Balance += transfer.Amount + if bs.Balance == nil { + bs.Balance = new(big.Int) + } + bs.Balance.Add(bs.Balance, transfer.Amount) bs.LastUpdatedBlock = transfer.Block balances.Trackers[transfer.Asset] = bs diff --git a/pkg/core/state/nep5.go b/pkg/core/state/nep5.go index d289ec00e..a87685293 100644 --- a/pkg/core/state/nep5.go +++ b/pkg/core/state/nep5.go @@ -13,7 +13,7 @@ import ( // NEP5Tracker contains info about a single account in a NEP5 contract. type NEP5Tracker struct { // Balance is the current balance of the account. - Balance int64 + Balance *big.Int // LastUpdatedBlock is a number of block when last `transfer` to or from the // account occured. LastUpdatedBlock uint32 @@ -25,7 +25,7 @@ type TransferLog struct { } // NEP5TransferSize is a size of a marshaled NEP5Transfer struct in bytes. -const NEP5TransferSize = util.Uint160Size*3 + 8 + 4 + 4 + util.Uint256Size + 4 +const NEP5TransferSize = util.Uint160Size*3 + amountSize + 4 + 4 + util.Uint256Size + 4 // NEP5Transfer represents a single NEP5 Transfer event. type NEP5Transfer struct { @@ -37,7 +37,7 @@ type NEP5Transfer struct { To util.Uint160 // Amount is the amount of tokens transferred. // It is negative when tokens are sent and positive if they are received. - Amount int64 + Amount *big.Int // Block is a number of block when the event occured. Block uint32 // Timestamp is the timestamp of the block where transfer occured. @@ -48,6 +48,8 @@ type NEP5Transfer struct { Index uint32 } +const amountSize = 32 + // NEP5Balances is a map of the NEP5 contract hashes // to the corresponding structures. type NEP5Balances struct { @@ -143,13 +145,13 @@ func (lg *TransferLog) Size() int { // EncodeBinary implements io.Serializable interface. func (t *NEP5Tracker) EncodeBinary(w *io.BinWriter) { - w.WriteU64LE(uint64(t.Balance)) + w.WriteVarBytes(emit.IntToBytes(t.Balance)) w.WriteU32LE(t.LastUpdatedBlock) } // DecodeBinary implements io.Serializable interface. func (t *NEP5Tracker) DecodeBinary(r *io.BinReader) { - t.Balance = int64(r.ReadU64LE()) + t.Balance = emit.BytesToInt(r.ReadVarBytes(amountSize)) t.LastUpdatedBlock = r.ReadU32LE() } @@ -194,7 +196,7 @@ func NEP5TransferFromNotification(ne NotificationEvent, txHash util.Uint256, hei Asset: ne.ScriptHash, From: fromAddr, To: toAddr, - Amount: amount.Int64(), + Amount: amount, Block: height, Timestamp: time, Tx: txHash, @@ -212,7 +214,19 @@ func (t *NEP5Transfer) EncodeBinary(w *io.BinWriter) { w.WriteBytes(t.To[:]) w.WriteU32LE(t.Block) w.WriteU32LE(t.Timestamp) - w.WriteU64LE(uint64(t.Amount)) + am := emit.IntToBytes(t.Amount) + if len(am) > amountSize { + panic("bad integer length") + } + fillerLen := amountSize - len(am) + w.WriteBytes(am) + var filler byte + if t.Amount.Sign() < 0 { + filler = 0xff + } + for i := 0; i < fillerLen; i++ { + w.WriteB(filler) + } w.WriteU32LE(t.Index) } @@ -224,6 +238,8 @@ func (t *NEP5Transfer) DecodeBinary(r *io.BinReader) { r.ReadBytes(t.To[:]) t.Block = r.ReadU32LE() t.Timestamp = r.ReadU32LE() - t.Amount = int64(r.ReadU64LE()) + amount := make([]byte, amountSize) + r.ReadBytes(amount) + t.Amount = emit.BytesToInt(amount) t.Index = r.ReadU32LE() } diff --git a/pkg/core/state/nep5_test.go b/pkg/core/state/nep5_test.go index 19f055d1c..a2c8ea764 100644 --- a/pkg/core/state/nep5_test.go +++ b/pkg/core/state/nep5_test.go @@ -1,6 +1,7 @@ package state import ( + "math/big" "math/rand" "testing" "time" @@ -41,7 +42,7 @@ func TestNEP5TransferLog_Append(t *testing.T) { func TestNEP5Tracker_EncodeBinary(t *testing.T) { expected := &NEP5Tracker{ - Balance: int64(rand.Uint64()), + Balance: big.NewInt(int64(rand.Uint64())), LastUpdatedBlock: rand.Uint32(), } @@ -53,7 +54,7 @@ func TestNEP5Transfer_DecodeBinary(t *testing.T) { Asset: util.Uint160{1, 2, 3}, From: util.Uint160{5, 6, 7}, To: util.Uint160{8, 9, 10}, - Amount: 42, + Amount: big.NewInt(42), Block: 12345, Timestamp: 54321, Tx: util.Uint256{8, 5, 3}, @@ -70,7 +71,7 @@ func TestNEP5TransferSize(t *testing.T) { func randomTransfer(r *rand.Rand) *NEP5Transfer { return &NEP5Transfer{ - Amount: int64(r.Uint64()), + Amount: big.NewInt(int64(r.Uint64())), Block: r.Uint32(), Asset: random.Uint160(), From: random.Uint160(), diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 623a49e4d..b1ba0636c 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -771,7 +771,7 @@ func (s *Server) getNEP5Balances(ps request.Params) (interface{}, *response.Erro } if as != nil { for h, bal := range as.Trackers { - amount := strconv.FormatInt(bal.Balance, 10) + amount := bal.Balance.String() bs.Balances = append(bs.Balances, result.NEP5Balance{ Asset: h, Amount: amount, @@ -824,14 +824,15 @@ func (s *Server) getNEP5Transfers(ps request.Params) (interface{}, *response.Err NotifyIndex: tr.Index, } - if tr.Amount > 0 { // token was received - transfer.Amount = strconv.FormatInt(tr.Amount, 10) + if tr.Amount.Sign() > 0 { // token was received + transfer.Amount = tr.Amount.String() if !tr.From.Equals(util.Uint160{}) { transfer.Address = address.Uint160ToString(tr.From) } bs.Received = append(bs.Received, transfer) } else { - transfer.Amount = strconv.FormatInt(-tr.Amount, 10) + tr.Amount.Neg(tr.Amount) + transfer.Amount = tr.Amount.String() if !tr.To.Equals(util.Uint160{}) { transfer.Address = address.Uint160ToString(tr.To) } @@ -888,12 +889,14 @@ func uint160ToString(u util.Uint160) string { func appendNEP5ToTransferTx(transfer *result.TransferTx, nepTr *state.NEP5Transfer) { var event result.TransferTxEvent event.Asset = nepTr.Asset.StringLE() - if nepTr.Amount > 0 { // token was received - event.Value = strconv.FormatInt(nepTr.Amount, 10) + if nepTr.Amount.Sign() > 0 { // token was received + event.Value = nepTr.Amount.String() event.Type = "receive" event.Address = uint160ToString(nepTr.From) } else { - event.Value = strconv.FormatInt(-nepTr.Amount, 10) + nepTr.Amount.Neg(nepTr.Amount) + event.Value = nepTr.Amount.String() + nepTr.Amount.Neg(nepTr.Amount) event.Type = "send" event.Address = uint160ToString(nepTr.To) } @@ -1327,7 +1330,7 @@ func (s *Server) getBlockTransferTx(ps request.Params) (interface{}, *response.E Asset: nepTr.Asset.StringLE(), From: uint160ToString(nepTr.From), To: uint160ToString(nepTr.To), - Value: strconv.FormatInt(nepTr.Amount, 10), + Value: nepTr.Amount.String(), }) index++ } From 857110774146b1e3fbb95f2ffbb93f665dc6e7f7 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 18 Feb 2021 10:48:01 +0300 Subject: [PATCH 14/17] core: bump DB version for NEP5 tracking data change It's incompatible. --- pkg/core/blockchain.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 511044fc6..46d448668 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -32,7 +32,7 @@ import ( // Tuning parameters. const ( headerBatchCount = 2000 - version = "0.0.9" + version = "0.0.10" // This one comes from C# code and it's different from the constant used // when creating an asset with Neo.Asset.Create interop call. It looks From 9d6a5dc26f8519804058549d782cddbb70944ee4 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 18 Feb 2021 17:16:15 +0300 Subject: [PATCH 15/17] result: fix confirmations count for blocks It's +1, current block actually had `-1` value calcuated that transformed into 4294967295. --- pkg/rpc/response/result/block.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/rpc/response/result/block.go b/pkg/rpc/response/result/block.go index b1cadb605..a074021fb 100644 --- a/pkg/rpc/response/result/block.go +++ b/pkg/rpc/response/result/block.go @@ -48,7 +48,7 @@ func NewBlock(b *block.Block, chain core.Blockchainer) Block { Base: &b.Base, BlockMetadataAndTx: BlockMetadataAndTx{ Size: io.GetVarSize(b), - Confirmations: chain.BlockHeight() - b.Index - 1, + Confirmations: chain.BlockHeight() - b.Index + 1, Tx: make([]Tx, 0, len(b.Transactions)), }, } From 7fc4f3c4ea847f7b592689786f82ea39b9d06f0f Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 18 Feb 2021 17:18:53 +0300 Subject: [PATCH 16/17] transaction: always marshal all token's data for register tx Fix missing precision and token type. --- pkg/core/transaction/register.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/core/transaction/register.go b/pkg/core/transaction/register.go index 3ce5be61f..014864ab7 100644 --- a/pkg/core/transaction/register.go +++ b/pkg/core/transaction/register.go @@ -56,10 +56,10 @@ func (tx *RegisterTX) EncodeBinary(bw *io.BinWriter) { // registeredAsset is a wrapper for RegisterTransaction type registeredAsset struct { - AssetType AssetType `json:"type,omitempty"` - Name json.RawMessage `json:"name,omitempty"` - Amount util.Fixed8 `json:"amount,omitempty"` - Precision uint8 `json:"precision,omitempty"` - Owner keys.PublicKey `json:"owner,omitempty"` - Admin string `json:"admin,omitempty"` + AssetType AssetType `json:"type"` + Name json.RawMessage `json:"name"` + Amount util.Fixed8 `json:"amount"` + Precision uint8 `json:"precision"` + Owner keys.PublicKey `json:"owner"` + Admin string `json:"admin"` } From 98580ae9b44430a821f46ea75bfa28ab1f4aca78 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Thu, 18 Feb 2021 17:26:24 +0300 Subject: [PATCH 17/17] transaction: always JSONize all contract fields for Publish tx --- pkg/core/transaction/publish.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/core/transaction/publish.go b/pkg/core/transaction/publish.go index 1cd89fa96..f9811e759 100644 --- a/pkg/core/transaction/publish.go +++ b/pkg/core/transaction/publish.go @@ -67,18 +67,18 @@ func (tx *PublishTX) EncodeBinary(bw *io.BinWriter) { // publishedContract is a JSON wrapper for PublishTransaction type publishedContract struct { Code publishedCode `json:"code"` - NeedStorage bool `json:"needstorage,omitempty"` - Name string `json:"name,omitempty"` - CodeVersion string `json:"version,omitempty"` - Author string `json:"author,omitempty"` - Email string `json:"email,omitempty"` - Description string `json:"description,omitempty"` + NeedStorage bool `json:"needstorage"` + Name string `json:"name"` + CodeVersion string `json:"version"` + Author string `json:"author"` + Email string `json:"email"` + Description string `json:"description"` } // publishedCode is a JSON wrapper for PublishTransaction Code type publishedCode struct { - Hash util.Uint160 `json:"hash,omitempty"` - Script string `json:"script,omitempty"` - ParamList []smartcontract.ParamType `json:"parameters,omitempty"` - ReturnType smartcontract.ParamType `json:"returntype,omitempty"` + Hash util.Uint160 `json:"hash"` + Script string `json:"script"` + ParamList []smartcontract.ParamType `json:"parameters"` + ReturnType smartcontract.ParamType `json:"returntype"` }