From 1a5731e00570753c0191459cad84857cceb13985 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 15 Nov 2019 20:35:09 +0300 Subject: [PATCH 1/4] core: rework balance accounting Store all unspents instead of balance values. Bump store version as this is an incompatible change. --- pkg/core/account_state.go | 62 +++++++++++++++++++++---------- pkg/core/account_state_test.go | 26 ++++++++++++- pkg/core/blockchain.go | 30 +++++++++++---- pkg/core/interop_neo.go | 2 +- pkg/rpc/wrappers/account_state.go | 2 +- 5 files changed, 91 insertions(+), 31 deletions(-) diff --git a/pkg/core/account_state.go b/pkg/core/account_state.go index 9404fd7bf..78646c4fc 100644 --- a/pkg/core/account_state.go +++ b/pkg/core/account_state.go @@ -69,13 +69,21 @@ func (a Accounts) commit(store storage.Store) error { return nil } +// UnspentBalance contains input/output transactons that sum up into the +// account balance for the given asset. +type UnspentBalance struct { + Tx util.Uint256 + Index uint16 + Value util.Fixed8 +} + // AccountState represents the state of a NEO account. type AccountState struct { Version uint8 ScriptHash util.Uint160 IsFrozen bool Votes []*keys.PublicKey - Balances map[util.Uint256]util.Fixed8 + Balances map[util.Uint256][]UnspentBalance } // NewAccountState returns a new AccountState object. @@ -85,7 +93,7 @@ func NewAccountState(scriptHash util.Uint160) *AccountState { ScriptHash: scriptHash, IsFrozen: false, Votes: []*keys.PublicKey{}, - Balances: make(map[util.Uint256]util.Fixed8), + Balances: make(map[util.Uint256][]UnspentBalance), } } @@ -96,14 +104,14 @@ func (s *AccountState) DecodeBinary(br *io.BinReader) { br.ReadLE(&s.IsFrozen) br.ReadArray(&s.Votes) - s.Balances = make(map[util.Uint256]util.Fixed8) + s.Balances = make(map[util.Uint256][]UnspentBalance) lenBalances := br.ReadVarUint() for i := 0; i < int(lenBalances); i++ { key := util.Uint256{} br.ReadLE(&key) - var val util.Fixed8 - br.ReadLE(&val) - s.Balances[key] = val + ubs := make([]UnspentBalance, 0) + br.ReadArray(&ubs) + s.Balances[key] = ubs } } @@ -114,21 +122,37 @@ func (s *AccountState) EncodeBinary(bw *io.BinWriter) { bw.WriteLE(s.IsFrozen) bw.WriteArray(s.Votes) - balances := s.nonZeroBalances() - bw.WriteVarUint(uint64(len(balances))) - for k, v := range balances { + bw.WriteVarUint(uint64(len(s.Balances))) + for k, v := range s.Balances { bw.WriteLE(k) - bw.WriteLE(v) + bw.WriteArray(v) } } -// nonZeroBalances returns only the non-zero balances for the account. -func (s *AccountState) nonZeroBalances() map[util.Uint256]util.Fixed8 { - b := make(map[util.Uint256]util.Fixed8) - for k, v := range s.Balances { - if v > 0 { - b[k] = v - } - } - return b +// DecodeBinary implements io.Serializable interface. +func (u *UnspentBalance) DecodeBinary(r *io.BinReader) { + u.Tx.DecodeBinary(r) + r.ReadLE(&u.Index) + r.ReadLE(&u.Value) +} + +// EncodeBinary implements io.Serializable interface. +func (u UnspentBalance) EncodeBinary(w *io.BinWriter) { + u.Tx.EncodeBinary(w) + w.WriteLE(u.Index) + w.WriteLE(u.Value) +} + +// GetBalanceValues sums all unspent outputs and returns a map of asset IDs to +// overall balances. +func (s *AccountState) GetBalanceValues() map[util.Uint256]util.Fixed8 { + res := make(map[util.Uint256]util.Fixed8) + for k, v := range s.Balances { + balance := util.Fixed8(0) + for _, b := range v { + balance += b.Value + } + res[k] = balance + } + return res } diff --git a/pkg/core/account_state_test.go b/pkg/core/account_state_test.go index b00225984..6517a1679 100644 --- a/pkg/core/account_state_test.go +++ b/pkg/core/account_state_test.go @@ -12,11 +12,18 @@ import ( func TestDecodeEncodeAccountState(t *testing.T) { var ( n = 10 - balances = make(map[util.Uint256]util.Fixed8) + balances = make(map[util.Uint256][]UnspentBalance) votes = make([]*keys.PublicKey, n) ) for i := 0; i < n; i++ { - balances[randomUint256()] = util.Fixed8(int64(randomInt(1, 10000))) + asset := randomUint256() + for j := 0; j < i+1; j++ { + balances[asset] = append(balances[asset], UnspentBalance{ + Tx: randomUint256(), + Index: uint16(randomInt(0, 65535)), + Value: util.Fixed8(int64(randomInt(1, 10000))), + }) + } k, err := keys.NewPrivateKey() assert.Nil(t, err) votes[i] = k.PublicKey() @@ -48,3 +55,18 @@ func TestDecodeEncodeAccountState(t *testing.T) { } assert.Equal(t, a.Balances, aDecode.Balances) } + +func TestAccountStateBalanceValues(t *testing.T) { + asset1 := randomUint256() + asset2 := randomUint256() + as := AccountState{Balances: make(map[util.Uint256][]UnspentBalance)} + ref := 0 + for i := 0; i < 10; i++ { + ref += i + as.Balances[asset1] = append(as.Balances[asset1], UnspentBalance{Value: util.Fixed8(i)}) + as.Balances[asset2] = append(as.Balances[asset2], UnspentBalance{Value: util.Fixed8(i * 10)}) + } + bVals := as.GetBalanceValues() + assert.Equal(t, util.Fixed8(ref), bVals[asset1]) + assert.Equal(t, util.Fixed8(ref*10), bVals[asset2]) +} diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index f8126fe87..dd861c90e 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -23,7 +23,7 @@ import ( // Tuning parameters. const ( headerBatchCount = 2000 - version = "0.0.1" + version = "0.0.2" // 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 @@ -361,16 +361,16 @@ func (bc *Blockchain) storeBlock(block *Block) error { unspentCoins[tx.Hash()] = NewUnspentCoinState(len(tx.Outputs)) // Process TX outputs. - for _, output := range tx.Outputs { + for index, output := range tx.Outputs { account, err := accounts.getAndUpdate(bc.store, output.ScriptHash) if err != nil { return err } - if _, ok := account.Balances[output.AssetID]; ok { - account.Balances[output.AssetID] += output.Amount - } else { - account.Balances[output.AssetID] = output.Amount - } + account.Balances[output.AssetID] = append(account.Balances[output.AssetID], UnspentBalance{ + Tx: tx.Hash(), + Index: uint16(index), + Value: output.Amount, + }) } // Process TX inputs that are grouped by previous hash. @@ -398,7 +398,21 @@ func (bc *Blockchain) storeBlock(block *Block) error { spentCoins[input.PrevHash] = spentCoin } - account.Balances[prevTXOutput.AssetID] -= prevTXOutput.Amount + balancesLen := len(account.Balances[prevTXOutput.AssetID]) + if balancesLen <= 1 { + delete(account.Balances, prevTXOutput.AssetID) + } else { + var gotTx bool + for index, balance := range account.Balances[prevTXOutput.AssetID] { + if !gotTx && balance.Tx.Equals(input.PrevHash) && balance.Index == input.PrevIndex { + gotTx = true + } + if gotTx && index+1 < balancesLen { + account.Balances[prevTXOutput.AssetID][index] = account.Balances[prevTXOutput.AssetID][index+1] + } + } + account.Balances[prevTXOutput.AssetID] = account.Balances[prevTXOutput.AssetID][:balancesLen-1] + } } } diff --git a/pkg/core/interop_neo.go b/pkg/core/interop_neo.go index 39623d6d0..1d41100b1 100644 --- a/pkg/core/interop_neo.go +++ b/pkg/core/interop_neo.go @@ -338,7 +338,7 @@ func (ic *interopContext) accountGetBalance(v *vm.VM) error { if err != nil { return err } - balance, ok := acc.Balances[ashash] + balance, ok := acc.GetBalanceValues()[ashash] if !ok { balance = util.Fixed8(0) } diff --git a/pkg/rpc/wrappers/account_state.go b/pkg/rpc/wrappers/account_state.go index 948c852b0..688a61e46 100644 --- a/pkg/rpc/wrappers/account_state.go +++ b/pkg/rpc/wrappers/account_state.go @@ -35,7 +35,7 @@ type Balance struct { // NewAccountState creates a new AccountState wrapper. func NewAccountState(a *core.AccountState) AccountState { balances := make(Balances, 0, len(a.Balances)) - for k, v := range a.Balances { + for k, v := range a.GetBalanceValues() { balances = append(balances, Balance{ Asset: k, Value: v, From e4868cd4294221f11b3b92af11a00aa3059b6a15 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 15 Nov 2019 21:54:02 +0300 Subject: [PATCH 2/4] core: implement EncodeBinary on value Fixes panic: panic: core.NotificationEvent does not have EncodeBinary(*BinWriter) goroutine 97 [running]: github.com/CityOfZion/neo-go/pkg/io.(*BinWriter).WriteArray(0xc004f7dda0, 0xabf820, 0xc004f7ddc0) /home/rik/dev/neo-go/pkg/io/binaryWriter.go:45 +0x4b7 github.com/CityOfZion/neo-go/pkg/core.(*AppExecResult).EncodeBinary(0xc000635498, 0xc004f7dda0) /home/rik/dev/neo-go/pkg/core/notification_event.go:73 +0x9c github.com/CityOfZion/neo-go/pkg/core.putAppExecResultIntoStore(0xcf0020, 0xc001aafec0, 0xc000635498, 0xc004f81940, 0x40) /home/rik/dev/neo-go/pkg/core/notification_event.go:33 +0xab github.com/CityOfZion/neo-go/pkg/core.(*Blockchain).storeBlock(0xc000118000, 0xc006f89ea0, 0xc006f89ea0, 0x0) /home/rik/dev/neo-go/pkg/core/blockchain.go:534 +0x2539 github.com/CityOfZion/neo-go/pkg/core.(*Blockchain).AddBlock(0xc000118000, 0xc006f89ea0, 0xc004f368b0, 0x1) /home/rik/dev/neo-go/pkg/core/blockchain.go:256 +0x91 github.com/CityOfZion/neo-go/pkg/network.(*blockQueue).run(0xc0048f4b80) /home/rik/dev/neo-go/pkg/network/blockqueue.go:39 +0x14c created by github.com/CityOfZion/neo-go/pkg/network.(*Server).Start /home/rik/dev/neo-go/pkg/network/server.go:133 +0x272 --- pkg/core/notification_event.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/core/notification_event.go b/pkg/core/notification_event.go index 5b15e39cd..850642557 100644 --- a/pkg/core/notification_event.go +++ b/pkg/core/notification_event.go @@ -56,7 +56,7 @@ func getAppExecResultFromStore(s storage.Store, hash util.Uint256) (*AppExecResu } // EncodeBinary implements the Serializable interface. -func (ne *NotificationEvent) EncodeBinary(w *io.BinWriter) { +func (ne NotificationEvent) EncodeBinary(w *io.BinWriter) { w.WriteLE(ne.ScriptHash) vm.EncodeBinaryStackItem(ne.Item, w) } From 5841d3931ed41e4079ba3ed22cbd507778ec6126 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 15 Nov 2019 22:04:10 +0300 Subject: [PATCH 3/4] rpc: implement getunspents method, fix #473 --- pkg/core/account_state.go | 6 ++-- pkg/rpc/neoScanBalanceGetter.go | 3 +- pkg/rpc/neoScanTypes.go | 6 ---- pkg/rpc/prometheus.go | 9 ++++++ pkg/rpc/server.go | 38 ++++++++++++++++------ pkg/rpc/server_helper_test.go | 20 ++++++++++++ pkg/rpc/server_test.go | 21 +++++++++++++ pkg/rpc/wrappers/unspents.go | 56 +++++++++++++++++++++++++++++++++ 8 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 pkg/rpc/wrappers/unspents.go diff --git a/pkg/core/account_state.go b/pkg/core/account_state.go index 78646c4fc..7c14151d7 100644 --- a/pkg/core/account_state.go +++ b/pkg/core/account_state.go @@ -72,9 +72,9 @@ func (a Accounts) commit(store storage.Store) error { // UnspentBalance contains input/output transactons that sum up into the // account balance for the given asset. type UnspentBalance struct { - Tx util.Uint256 - Index uint16 - Value util.Fixed8 + Tx util.Uint256 `json:"txid"` + Index uint16 `json:"n"` + Value util.Fixed8 `json:"value"` } // AccountState represents the state of a NEO account. diff --git a/pkg/rpc/neoScanBalanceGetter.go b/pkg/rpc/neoScanBalanceGetter.go index 5e7b6150d..c51fba304 100644 --- a/pkg/rpc/neoScanBalanceGetter.go +++ b/pkg/rpc/neoScanBalanceGetter.go @@ -7,6 +7,7 @@ import ( "sort" "github.com/CityOfZion/neo-go/pkg/core/transaction" + "github.com/CityOfZion/neo-go/pkg/rpc/wrappers" "github.com/CityOfZion/neo-go/pkg/util" errs "github.com/pkg/errors" ) @@ -59,7 +60,7 @@ func (s NeoScanServer) CalculateInputs(address string, assetIDUint util.Uint256, selected = util.Fixed8(0) us []*Unspent assetUnspent Unspent - assetID = GlobalAssets[assetIDUint.ReverseString()] + assetID = wrappers.GlobalAssets[assetIDUint.ReverseString()] ) if us, err = s.GetBalance(address); err != nil { return nil, util.Fixed8(0), errs.Wrapf(err, "Cannot get balance for address %v", address) diff --git a/pkg/rpc/neoScanTypes.go b/pkg/rpc/neoScanTypes.go index 58a8b9ce2..9ef6a81b1 100644 --- a/pkg/rpc/neoScanTypes.go +++ b/pkg/rpc/neoScanTypes.go @@ -39,12 +39,6 @@ type ( } ) -// GlobalAssets stores a map of asset IDs to user-friendly strings ("NEO"/"GAS"). -var GlobalAssets = map[string]string{ - "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b": "NEO", - "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7": "GAS", -} - // functions for sorting array of `Unspents` func (us Unspents) Len() int { return len(us) } func (us Unspents) Less(i, j int) bool { return us[i].Value < us[j].Value } diff --git a/pkg/rpc/prometheus.go b/pkg/rpc/prometheus.go index 7a58b8f19..51c865003 100644 --- a/pkg/rpc/prometheus.go +++ b/pkg/rpc/prometheus.go @@ -92,6 +92,14 @@ var ( }, ) + getunspentsCalled = prometheus.NewCounter( + prometheus.CounterOpts{ + Help: "Number of calls to getunspents rpc endpoint", + Name: "getunspents_called", + Namespace: "neogo", + }, + ) + sendrawtransactionCalled = prometheus.NewCounter( prometheus.CounterOpts{ Help: "Number of calls to sendrawtransaction rpc endpoint", @@ -113,6 +121,7 @@ func init() { validateaddressCalled, getassetstateCalled, getaccountstateCalled, + getunspentsCalled, getrawtransactionCalled, sendrawtransactionCalled, ) diff --git a/pkg/rpc/server.go b/pkg/rpc/server.go index 474e9103c..8b521ed36 100644 --- a/pkg/rpc/server.go +++ b/pkg/rpc/server.go @@ -233,20 +233,16 @@ Methods: case "getaccountstate": getaccountstateCalled.Inc() - param, err := reqParams.ValueWithType(0, "string") - if err != nil { - resultsErr = err - } else if scriptHash, err := crypto.Uint160DecodeAddress(param.StringVal); err != nil { - resultsErr = errInvalidParams - } else if as := s.chain.GetAccountState(scriptHash); as != nil { - results = wrappers.NewAccountState(as) - } else { - results = "Invalid public account address" - } + results, resultsErr = s.getAccountState(reqParams, false) + case "getrawtransaction": getrawtransactionCalled.Inc() results, resultsErr = s.getrawtransaction(reqParams) + case "getunspents": + getunspentsCalled.Inc() + results, resultsErr = s.getAccountState(reqParams, true) + case "invokescript": results, resultsErr = s.invokescript(reqParams) @@ -304,6 +300,28 @@ func (s *Server) getrawtransaction(reqParams Params) (interface{}, error) { return results, resultsErr } +// getAccountState returns account state either in short or full (unspents included) form. +func (s *Server) getAccountState(reqParams Params, unspents bool) (interface{}, error) { + var resultsErr error + var results interface{} + + param, err := reqParams.ValueWithType(0, "string") + if err != nil { + resultsErr = err + } else if scriptHash, err := crypto.Uint160DecodeAddress(param.StringVal); err != nil { + resultsErr = errInvalidParams + } else if as := s.chain.GetAccountState(scriptHash); as != nil { + if unspents { + results = wrappers.NewUnspents(as, s.chain, param.StringVal) + } else { + results = wrappers.NewAccountState(as) + } + } else { + results = "Invalid public account address" + } + return results, resultsErr +} + // invokescript implements the `invokescript` RPC call. func (s *Server) invokescript(reqParams Params) (interface{}, error) { hexScript, err := reqParams.ValueWithType(0, "string") diff --git a/pkg/rpc/server_helper_test.go b/pkg/rpc/server_helper_test.go index a2de55187..c57584bf5 100644 --- a/pkg/rpc/server_helper_test.go +++ b/pkg/rpc/server_helper_test.go @@ -138,6 +138,26 @@ type GetAccountStateResponse struct { ID int `json:"id"` } +// GetUnspents struct for testing. +type GetUnspents struct { + Jsonrpc string `json:"jsonrpc"` + Result struct { + Balance []struct { + Unspents []struct { + TxID string `json:"txid"` + Index int `json:"n"` + Value string `json:"value"` + } `json:"unspent"` + AssetHash string `json:"asset_hash"` + Asset string `json:"asset"` + AssetSymbol string `json:"asset_symbol"` + Amount string `json:"amount"` + } `json:"balance"` + Address string `json:"address"` + } `json:"result"` + ID int `json:"id"` +} + func initServerWithInMemoryChain(t *testing.T) (*core.Blockchain, http.HandlerFunc) { var nBlocks uint32 diff --git a/pkg/rpc/server_test.go b/pkg/rpc/server_test.go index 05d7eacf5..e9cb66cfe 100644 --- a/pkg/rpc/server_test.go +++ b/pkg/rpc/server_test.go @@ -146,6 +146,17 @@ func TestRPC(t *testing.T) { assert.Equal(t, false, res.Result.Frozen) }) + t.Run("getunspents_positive", func(t *testing.T) { + rpc := `{"jsonrpc": "2.0", "id": 1, "method": "getunspents", "params": ["AZ81H31DMWzbSnFDLFkzh9vHwaDLayV7fU"]}` + body := doRPCCall(rpc, handler, t) + checkErrResponse(t, body, false) + var res GetUnspents + err := json.Unmarshal(bytes.TrimSpace(body), &res) + assert.NoErrorf(t, err, "could not parse response: %s", body) + assert.Equal(t, 1, len(res.Result.Balance)) + assert.Equal(t, 1, len(res.Result.Balance[0].Unspents)) + }) + t.Run("getaccountstate_negative", func(t *testing.T) { rpc := `{"jsonrpc": "2.0", "id": 1, "method": "getaccountstate", "params": ["AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y"]}` body := doRPCCall(rpc, handler, t) @@ -156,6 +167,16 @@ func TestRPC(t *testing.T) { assert.Equal(t, "Invalid public account address", res.Result) }) + t.Run("getunspents_negative", func(t *testing.T) { + rpc := `{"jsonrpc": "2.0", "id": 1, "method": "getunspents", "params": ["AK2nJJpJr6o664CWJKi1QRXjqeic2zRp8y"]}` + body := doRPCCall(rpc, handler, t) + checkErrResponse(t, body, false) + var res StringResultResponse + err := json.Unmarshal(bytes.TrimSpace(body), &res) + assert.NoErrorf(t, err, "could not parse response: %s", body) + assert.Equal(t, "Invalid public account address", res.Result) + }) + t.Run("getrawtransaction", func(t *testing.T) { block, _ := chain.GetBlock(chain.GetHeaderHash(0)) TXHash := block.Transactions[1].Hash() diff --git a/pkg/rpc/wrappers/unspents.go b/pkg/rpc/wrappers/unspents.go new file mode 100644 index 000000000..b20307f14 --- /dev/null +++ b/pkg/rpc/wrappers/unspents.go @@ -0,0 +1,56 @@ +package wrappers + +import ( + "github.com/CityOfZion/neo-go/pkg/core" + "github.com/CityOfZion/neo-go/pkg/util" +) + +// UnspentBalanceInfo wrapper is used to represent single unspent asset entry +// in `getunspents` output. +type UnspentBalanceInfo struct { + Unspents []core.UnspentBalance `json:"unspent"` + AssetHash util.Uint256 `json:"asset_hash"` + Asset string `json:"asset"` + AssetSymbol string `json:"asset_symbol"` + Amount util.Fixed8 `json:"amount"` +} + +// Unspents wrapper is used to represent getunspents return result. +type Unspents struct { + Balance []UnspentBalanceInfo `json:"balance"` + Address string `json:"address"` +} + +// GlobalAssets stores a map of asset IDs to user-friendly strings ("NEO"/"GAS"). +var GlobalAssets = map[string]string{ + "c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b": "NEO", + "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7": "GAS", +} + +// NewUnspents creates a new AccountState wrapper using given Blockchainer. +func NewUnspents(a *core.AccountState, chain core.Blockchainer, addr string) Unspents { + res := Unspents{ + Address: addr, + Balance: make([]UnspentBalanceInfo, 0, len(a.Balances)), + } + balanceValues := a.GetBalanceValues() + for k, v := range a.Balances { + name, ok := GlobalAssets[k.ReverseString()] + if !ok { + as := chain.GetAssetState(k) + if as != nil { + name = as.Name + } + } + + res.Balance = append(res.Balance, UnspentBalanceInfo{ + Unspents: v, + AssetHash: k, + Asset: name, + AssetSymbol: name, + Amount: balanceValues[k], + }) + } + + return res +} From 7f24c6712c0103d81d9180ac3b836fb8ff767b57 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Sun, 17 Nov 2019 00:33:42 +0300 Subject: [PATCH 4/4] docs: bring rpc.md up to date, add issues links --- docs/rpc.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/rpc.md b/docs/rpc.md index 6d42ce123..baf060fff 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -36,25 +36,29 @@ which would yield the response: | Method | Implemented | | ------- | ------------| | `getaccountstate` | Yes | +| `getapplicationlog` | No (#500) | | `getassetstate` | Yes | | `getbestblockhash` | Yes | | `getblock` | Yes | | `getblockcount` | Yes | | `getblockhash` | Yes | -| `getblocksysfee` | No | +| `getblocksysfee` | No (#341) | | `getconnectioncount` | Yes | -| `getcontractstate` | No | +| `getcontractstate` | No (#342) | +| `getnep5balances` | No (#498) | +| `getnep5transfers` | No (#498) | | `getpeers` | Yes | -| `getrawmempool` | No | -| `getrawtransaction` | No | -| `getstorage` | No | -| `gettxout` | No | +| `getrawmempool` | No (#175) | +| `getrawtransaction` | Yes | +| `getstorage` | No (#343) | +| `gettxout` | No (#345) | +| `getunspents` | Yes | | `getversion` | Yes | -| `invoke` | No | -| `invokefunction` | No | -| `invokescript` | No | -| `sendrawtransaction` | No | -| `submitblock` | No | +| `invoke` | No (#346) | +| `invokefunction` | No (#347) | +| `invokescript` | Yes | +| `sendrawtransaction` | Yes | +| `submitblock` | No (#344) | | `validateaddress` | Yes | ## Reference