From 6dbae7edc497e89fea99ca7fe182aec233daa3a4 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 16 Nov 2022 12:35:26 +0300 Subject: [PATCH] rpcclient: fix WS-client unsubscription process Do not block subscribers until the unsubscription request to RPC server is completed. Otherwise, another notification may be received from the RPC server which will block the unsubscription process. At the same time, fix event-based waiter. We must not block the receiver channel during unsubscription because there's a chance that subsequent event will be sent by the server. We need to read this event in order not to block the WSClient's readloop. --- internal/basicchain/basic.go | 6 ++ pkg/core/statesync/neotest_test.go | 1 - pkg/neotest/basic.go | 3 +- pkg/rpcclient/actor/waiter.go | 72 +++++++++++------ pkg/rpcclient/wsclient.go | 67 +++++++++++----- pkg/services/rpcsrv/client_test.go | 72 +++++++++++++++++ pkg/services/rpcsrv/server_test.go | 84 ++++++++++++-------- pkg/services/rpcsrv/subscription_test.go | 6 +- pkg/services/rpcsrv/testdata/testblocks.acc | Bin 35080 -> 35787 bytes 9 files changed, 225 insertions(+), 86 deletions(-) diff --git a/internal/basicchain/basic.go b/internal/basicchain/basic.go index 5dbc6844f..ef9a3d9a4 100644 --- a/internal/basicchain/basic.go +++ b/internal/basicchain/basic.go @@ -16,6 +16,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/neotest" "github.com/nspcc-dev/neo-go/pkg/rpcclient/nns" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" @@ -273,6 +274,11 @@ func Init(t *testing.T, rootpath string, e *neotest.Executor) { storageCfg := filepath.Join(testDataPrefix, "storage", "storage_contract.yml") _, _, _ = deployContractFromPriv0(t, storagePath, "Storage", storageCfg, StorageContractID) + // Block #23: add FAULTed transaction to check WSClient waitloops. + faultedInvoker := e.NewInvoker(cHash, acc0) + faultedH := faultedInvoker.InvokeScriptCheckFAULT(t, []byte{byte(opcode.ABORT)}, []neotest.Signer{acc0}, "at instruction 0 (ABORT): ABORT") + t.Logf("FAULTed transaction:\n\thash LE: %s\n\tblock index: %d", faultedH.StringLE(), e.Chain.BlockHeight()) + // Compile contract to test `invokescript` RPC call invokePath := filepath.Join(testDataPrefix, "invoke", "invokescript_contract.go") invokeCfg := filepath.Join(testDataPrefix, "invoke", "invoke.yml") diff --git a/pkg/core/statesync/neotest_test.go b/pkg/core/statesync/neotest_test.go index af6bd5d90..d799d9afa 100644 --- a/pkg/core/statesync/neotest_test.go +++ b/pkg/core/statesync/neotest_test.go @@ -307,7 +307,6 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) { e.AddNewBlock(t) e.AddNewBlock(t) // This block is stateSyncPoint-th block. e.AddNewBlock(t) - e.AddNewBlock(t) require.Equal(t, stateSyncPoint+2, int(bcSpout.BlockHeight())) boltCfg := func(c *config.ProtocolConfiguration) { diff --git a/pkg/neotest/basic.go b/pkg/neotest/basic.go index 3ee1904ab..b0653c940 100644 --- a/pkg/neotest/basic.go +++ b/pkg/neotest/basic.go @@ -200,9 +200,10 @@ func (e *Executor) InvokeScriptCheckHALT(t testing.TB, script []byte, signers [] // InvokeScriptCheckFAULT adds a transaction with the specified script to the // chain and checks if it's FAULTed with the specified error. -func (e *Executor) InvokeScriptCheckFAULT(t testing.TB, script []byte, signers []Signer, errMessage string) { +func (e *Executor) InvokeScriptCheckFAULT(t testing.TB, script []byte, signers []Signer, errMessage string) util.Uint256 { hash := e.InvokeScript(t, script, signers) e.CheckFault(t, hash, errMessage) + return hash } // CheckHalt checks that the transaction is persisted with HALT state. diff --git a/pkg/rpcclient/actor/waiter.go b/pkg/rpcclient/actor/waiter.go index 65bbc625d..e2365a437 100644 --- a/pkg/rpcclient/actor/waiter.go +++ b/pkg/rpcclient/actor/waiter.go @@ -217,26 +217,47 @@ func (w *EventWaiter) Wait(h util.Uint256, vub uint32, err error) (res *state.Ap // WaitAny implements Waiter interface. func (w *EventWaiter) WaitAny(ctx context.Context, vub uint32, hashes ...util.Uint256) (res *state.AppExecResult, waitErr error) { - var wsWaitErr error + var ( + wsWaitErr error + bRcvr = make(chan *block.Block) + aerRcvr = make(chan *state.AppExecResult) + unsubErrs = make(chan error) + waitersActive int + ) + + // Rollback to a poll-based waiter if needed. defer func() { if wsWaitErr != nil { res, waitErr = w.polling.WaitAny(ctx, vub, hashes...) if waitErr != nil { - waitErr = fmt.Errorf("WS waiter error: %w; simple waiter error: %v", wsWaitErr, waitErr) + // Wrap the poll-based error, it's more important. + waitErr = fmt.Errorf("event-based error: %v; poll-based waiter error: %w", wsWaitErr, waitErr) } } }() - bRcvr := make(chan *block.Block) - aerRcvr := make(chan *state.AppExecResult) + + // Drain receivers to avoid other notification receivers blocking. defer func() { drainLoop: - // Drain receivers to avoid other notification receivers blocking. for { select { case <-bRcvr: case <-aerRcvr: - default: - break drainLoop + case unsubErr := <-unsubErrs: + if unsubErr != nil { + errFmt := "unsubscription error: %v" + errArgs := []interface{}{unsubErr} + if waitErr != nil { + errFmt = "%w; " + errFmt + errArgs = append([]interface{}{waitErr}, errArgs...) + } + waitErr = fmt.Errorf(errFmt, errArgs...) + } + waitersActive-- + // Wait until all receiver channels finish their work. + if waitersActive == 0 { + break drainLoop + } } } if wsWaitErr == nil || !errors.Is(wsWaitErr, ErrMissedEvent) { @@ -244,24 +265,24 @@ func (w *EventWaiter) WaitAny(ctx context.Context, vub uint32, hashes ...util.Ui close(aerRcvr) } }() - // Execution event precedes the block event, thus wait until the VUB-th block to be sure. + + // Execution event preceded the block event, thus wait until the VUB-th block to be sure. since := vub blocksID, err := w.ws.ReceiveBlocks(&neorpc.BlockFilter{Since: &since}, bRcvr) if err != nil { wsWaitErr = fmt.Errorf("failed to subscribe for new blocks: %w", err) return } + waitersActive++ defer func() { - err = w.ws.Unsubscribe(blocksID) - if err != nil { - errFmt := "failed to unsubscribe from blocks (id: %s): %v" - errArgs := []interface{}{blocksID, err} - if waitErr != nil { - errFmt += "; wait error: %w" - errArgs = append(errArgs, waitErr) + go func() { + err = w.ws.Unsubscribe(blocksID) + if err != nil { + unsubErrs <- fmt.Errorf("failed to unsubscribe from blocks (id: %s): %w", blocksID, err) + return } - waitErr = fmt.Errorf(errFmt, errArgs...) - } + unsubErrs <- nil + }() }() for _, h := range hashes { txsID, err := w.ws.ReceiveExecutions(&neorpc.ExecutionFilter{Container: &h}, aerRcvr) @@ -269,17 +290,16 @@ func (w *EventWaiter) WaitAny(ctx context.Context, vub uint32, hashes ...util.Ui wsWaitErr = fmt.Errorf("failed to subscribe for execution results: %w", err) return } + waitersActive++ defer func() { - err = w.ws.Unsubscribe(txsID) - if err != nil { - errFmt := "failed to unsubscribe from transactions (id: %s): %v" - errArgs := []interface{}{txsID, err} - if waitErr != nil { - errFmt += "; wait error: %w" - errArgs = append(errArgs, waitErr) + go func() { + err = w.ws.Unsubscribe(txsID) + if err != nil { + unsubErrs <- fmt.Errorf("failed to unsubscribe from transactions (id: %s): %w", txsID, err) + return } - waitErr = fmt.Errorf(errFmt, errArgs...) - } + unsubErrs <- nil + }() }() } diff --git a/pkg/rpcclient/wsclient.go b/pkg/rpcclient/wsclient.go index 1a4c7cd3a..3e15eff8d 100644 --- a/pkg/rpcclient/wsclient.go +++ b/pkg/rpcclient/wsclient.go @@ -625,16 +625,6 @@ func (c *WSClient) performSubscription(params []interface{}, rcvr notificationRe return resp, nil } -func (c *WSClient) performUnsubscription(id string) error { - c.subscriptionsLock.Lock() - defer c.subscriptionsLock.Unlock() - - if _, ok := c.subscriptions[id]; !ok { - return errors.New("no subscription with this ID") - } - return c.removeSubscription(id) -} - // SubscribeForNewBlocks adds subscription for new block events to this instance // of the client. It can be filtered by primary consensus node index, nil value doesn't // add any filters. @@ -876,29 +866,55 @@ func (c *WSClient) ReceiveNotaryRequests(flt *neorpc.TxFilter, rcvr chan<- *resu return c.performSubscription(params, r) } -// Unsubscribe removes subscription for the given event stream. +// Unsubscribe removes subscription for the given event stream. It will return an +// error in case if there's no subscription with the provided ID. Call to Unsubscribe +// doesn't block notifications receive process for given subscriber, thus, ensure +// that subscriber channel is properly drained while unsubscription is being +// performed. You may probably need to run unsubscription process in a separate +// routine (in parallel with notification receiver routine) to avoid Client's +// notification dispatcher blocking. func (c *WSClient) Unsubscribe(id string) error { return c.performUnsubscription(id) } -// UnsubscribeAll removes all active subscriptions of the current client. +// UnsubscribeAll removes all active subscriptions of the current client. It copies +// the list of subscribers in order not to hold the lock for the whole execution +// time and tries to unsubscribe from us many feeds as possible returning the +// chunk of unsubscription errors afterwards. Call to UnsubscribeAll doesn't block +// notifications receive process for given subscribers, thus, ensure that subscribers +// channels are properly drained while unsubscription is being performed. You may +// probably need to run unsubscription process in a separate routine (in parallel +// with notification receiver routines) to avoid Client's notification dispatcher +// blocking. func (c *WSClient) UnsubscribeAll() error { c.subscriptionsLock.Lock() - defer c.subscriptionsLock.Unlock() - + subs := make([]string, 0, len(c.subscriptions)) for id := range c.subscriptions { - err := c.removeSubscription(id) + subs = append(subs, id) + } + c.subscriptionsLock.Unlock() + + var resErr error + for _, id := range subs { + err := c.performUnsubscription(id) if err != nil { - return err + errFmt := "failed to unsubscribe from feed %d: %v" + errArgs := []interface{}{err} + if resErr != nil { + errFmt = "%w; " + errFmt + errArgs = append([]interface{}{resErr}, errArgs...) + } + resErr = fmt.Errorf(errFmt, errArgs...) } } - return nil + return resErr } -// removeSubscription is internal method that removes subscription with the given -// ID from the list of subscriptions and receivers. It must be performed under -// subscriptions lock. -func (c *WSClient) removeSubscription(id string) error { +// performUnsubscription is internal method that removes subscription with the given +// ID from the list of subscriptions and receivers. It takes the subscriptions lock +// after WS RPC unsubscription request is completed. Until then the subscriber channel +// may still receive WS notifications. +func (c *WSClient) performUnsubscription(id string) error { var resp bool if err := c.performRequest("unsubscribe", []interface{}{id}, &resp); err != nil { return err @@ -906,7 +922,14 @@ func (c *WSClient) removeSubscription(id string) error { if !resp { return errors.New("unsubscribe method returned false result") } - rcvr := c.subscriptions[id] + + c.subscriptionsLock.Lock() + defer c.subscriptionsLock.Unlock() + + rcvr, ok := c.subscriptions[id] + if !ok { + return errors.New("no subscription with this ID") + } ch := rcvr.Receiver() ids := c.receivers[ch] for i, rcvrID := range ids { diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index a2d1d5d95..102e1e93e 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -19,8 +19,10 @@ import ( "github.com/nspcc-dev/neo-go/internal/testchain" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core" + "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/fee" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -1419,6 +1421,7 @@ func TestClient_NEP11_ND(t *testing.T) { expected := stackitem.NewMap() expected.Add(stackitem.Make([]byte("name")), stackitem.Make([]byte("neo.com"))) expected.Add(stackitem.Make([]byte("expiration")), stackitem.Make(blockRegisterDomain.Timestamp+365*24*3600*1000)) // expiration formula + expected.Add(stackitem.Make([]byte("admin")), stackitem.Null{}) require.EqualValues(t, expected, p) }) t.Run("Transfer", func(t *testing.T) { @@ -2001,3 +2004,72 @@ func TestClient_Wait(t *testing.T) { // Wait for transaction that hasn't been persisted and VUB block has been persisted. check(t, util.Uint256{1, 2, 3}, chain.BlockHeight()-1, true) } + +func TestWSClient_Wait(t *testing.T) { + chain, rpcSrv, httpSrv := initClearServerWithServices(t, false, false, true) + defer chain.Close() + defer rpcSrv.Shutdown() + + url := "ws" + strings.TrimPrefix(httpSrv.URL, "http") + "/ws" + c, err := rpcclient.NewWS(context.Background(), url, rpcclient.Options{}) + require.NoError(t, err) + require.NoError(t, c.Init()) + acc, err := wallet.NewAccount() + require.NoError(t, err) + act, err := actor.New(c, []actor.SignerAccount{ + { + Signer: transaction.Signer{ + Account: acc.ScriptHash(), + }, + Account: acc, + }, + }) + require.NoError(t, err) + + rcvr := make(chan *state.AppExecResult) + check := func(t *testing.T, b *block.Block, h util.Uint256, vub uint32) { + go func() { + aer, err := act.Wait(h, vub, nil) + require.NoError(t, err, b.Index) + rcvr <- aer + }() + go func() { + require.Eventually(t, func() bool { + rpcSrv.subsLock.Lock() + defer rpcSrv.subsLock.Unlock() + return len(rpcSrv.subscribers) == 1 + }, time.Second, 100*time.Millisecond) + require.NoError(t, chain.AddBlock(b)) + }() + waitloop: + for { + select { + case aer := <-rcvr: + require.Equal(t, h, aer.Container) + require.Equal(t, trigger.Application, aer.Trigger) + if h.StringLE() == faultedTxHashLE { + require.Equal(t, vmstate.Fault, aer.VMState) + } else { + require.Equal(t, vmstate.Halt, aer.VMState) + } + break waitloop + case <-time.NewTimer(time.Duration(chain.GetConfig().SecondsPerBlock) * time.Second).C: + t.Fatalf("transaction from block %d failed to be awaited: deadline exceeded", b.Index) + } + } + } + + var faultedChecked bool + for _, b := range getTestBlocks(t) { + if len(b.Transactions) > 0 { + tx := b.Transactions[0] + check(t, b, tx.Hash(), tx.ValidUntilBlock) + if tx.Hash().StringLE() == faultedTxHashLE { + faultedChecked = true + } + } else { + require.NoError(t, chain.AddBlock(b)) + } + } + require.True(t, faultedChecked, "FAULTed transaction wasn't checked") +} diff --git a/pkg/services/rpcsrv/server_test.go b/pkg/services/rpcsrv/server_test.go index d61629d51..ec6270e86 100644 --- a/pkg/services/rpcsrv/server_test.go +++ b/pkg/services/rpcsrv/server_test.go @@ -67,20 +67,22 @@ type rpcTestCase struct { } const genesisBlockHash = "0f8fb4e17d2ab9f3097af75ca7fd16064160fb8043db94909e00dd4e257b9dc4" -const testContractHash = "2db7d679c538ace5f00495c9e9d8ea95f1e0f5a5" -const deploymentTxHash = "496bccb5cb0a008ef9b7a32c459e508ef24fbb0830f82bac9162afa4ca804839" +const testContractHash = "565cff9508ebc75aadd7fe59f38dac610ab6093c" +const deploymentTxHash = "a14390941cc3a1d87393eff720a722e9cd350bd6ed233c5fe2001326c80eb68e" const ( - verifyContractHash = "06ed5314c2e4cb103029a60b86d46afa2fb8f67c" - verifyContractAVM = "VwIAQS1RCDBwDBTunqIsJ+NL0BSPxBCOCPdOj1BIskrZMCQE2zBxaBPOStkoJATbKGlK2SgkBNsol0A=" - verifyWithArgsContractHash = "0dce75f52adb1a4c5c6eaa6a34eb26db2e5b3781" - nnsContractHash = "bdbfe1a280a0e23ca5b569c8f5845169bd93cb06" - nnsToken1ID = "6e656f2e636f6d" - nfsoContractHash = "0e15ca0df00669a2cd5dcb03bfd3e2b3849c2969" - nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486" - invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" - block20StateRootLE = "f1380226a217b5e35ea968d42c50e20b9af7ab83b91416c8fb85536c61004332" - storageContractHash = "ebc0c16a76c808cd4dde6bcc063f09e45e331ec7" + verifyContractHash = "06ed5314c2e4cb103029a60b86d46afa2fb8f67c" + verifyContractAVM = "VwIAQS1RCDBwDBTunqIsJ+NL0BSPxBCOCPdOj1BIskrZMCQE2zBxaBPOStkoJATbKGlK2SgkBNsol0A=" + verifyWithArgsContractHash = "4dc916254efd2947c93b11207e8ffc0bb56161c5" + nnsContractHash = "892429fcd47c30f8451781acc627e8b20e0d64f3" + nnsToken1ID = "6e656f2e636f6d" + nfsoContractHash = "730ebe719ab8e3b69d11dafc95cdb9bf409db179" + nfsoToken1ID = "7e244ffd6aa85fb1579d2ed22e9b761ab62e3486" + storageContractHash = "ebc0c16a76c808cd4dde6bcc063f09e45e331ec7" + faultedTxHashLE = "82279bfe9bada282ca0f8cb8e0bb124b921af36f00c69a518320322c6f4fef60" + faultedTxBlock uint32 = 23 + invokescriptContractAVM = "VwIADBQBDAMOBQYMDQIODw0DDgcJAAAAAErZMCQE2zBwaEH4J+yMqiYEEUAMFA0PAwIJAAIBAwcDBAUCAQAOBgwJStkwJATbMHFpQfgn7IyqJgQSQBNA" + block20StateRootLE = "b49a045246bf3bb90248ed538dd21e67d782a9242c52f31dfdef3da65ecd87c1" ) var ( @@ -287,6 +289,7 @@ var rpcTestCases = map[string][]rpcTestCase{ return &map[string]interface{}{ "name": "neo.com", "expiration": "lhbLRl0B", + "admin": nil, } }, }, @@ -817,7 +820,7 @@ var rpcTestCases = map[string][]rpcTestCase{ require.True(t, ok) expected := result.UnclaimedGas{ Address: testchain.MultisigScriptHash(), - Unclaimed: *big.NewInt(11000), + Unclaimed: *big.NewInt(11500), } assert.Equal(t, expected, *actual) }, @@ -905,7 +908,7 @@ var rpcTestCases = map[string][]rpcTestCase{ script = append(script, 0x41, 0x62, 0x7d, 0x5b, 0x52) return &result.Invoke{ State: "HALT", - GasConsumed: 32414250, + GasConsumed: 31922970, Script: script, Stack: []stackitem.Item{stackitem.Make(true)}, Notifications: []state.NotificationEvent{{ @@ -935,19 +938,19 @@ var rpcTestCases = map[string][]rpcTestCase{ chg := []dboper.Operation{{ State: "Changed", Key: []byte{0xfa, 0xff, 0xff, 0xff, 0xb}, - Value: []byte{0xf6, 0x8b, 0x4e, 0x9d, 0x51, 0x79, 0x12}, + Value: []byte{0x54, 0xb2, 0xd2, 0xa3, 0x51, 0x79, 0x12}, }, { State: "Added", Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xd6, 0x24, 0x87, 0x12, 0xff, 0x97, 0x22, 0x80, 0xa0, 0xae, 0xf5, 0x24, 0x1c, 0x96, 0x4d, 0x63, 0x78, 0x29, 0xcd, 0xb}, - Value: []byte{0x41, 0x03, 0x21, 0x01, 0x01, 0x21, 0x01, 0x17, 0}, + Value: []byte{0x41, 0x03, 0x21, 0x01, 0x01, 0x21, 0x01, 0x18, 0}, }, { State: "Changed", Key: []byte{0xfb, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}, - Value: []byte{0x41, 0x03, 0x21, 0x04, 0x2f, 0xd9, 0xf5, 0x05, 0x21, 0x01, 0x17, 0}, + Value: []byte{0x41, 0x03, 0x21, 0x04, 0x2f, 0xd9, 0xf5, 0x05, 0x21, 0x01, 0x18, 0}, }, { State: "Changed", Key: []byte{0xfa, 0xff, 0xff, 0xff, 0x14, 0xee, 0x9e, 0xa2, 0x2c, 0x27, 0xe3, 0x4b, 0xd0, 0x14, 0x8f, 0xc4, 0x10, 0x8e, 0x8, 0xf7, 0x4e, 0x8f, 0x50, 0x48, 0xb2}, - Value: []byte{0x41, 0x01, 0x21, 0x05, 0xe4, 0x74, 0xef, 0xdb, 0x08}, + Value: []byte{0x41, 0x01, 0x21, 0x05, 0x0c, 0x76, 0x4f, 0xdf, 0x08}, }} // Can be returned in any order. assert.ElementsMatch(t, chg, res.Diagnostics.Changes) @@ -963,7 +966,7 @@ var rpcTestCases = map[string][]rpcTestCase{ cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib) return &result.Invoke{ State: "HALT", - GasConsumed: 15928320, + GasConsumed: 13970250, Script: script, Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, Notifications: []state.NotificationEvent{}, @@ -1052,7 +1055,7 @@ var rpcTestCases = map[string][]rpcTestCase{ script = append(script, 0x41, 0x62, 0x7d, 0x5b, 0x52) return &result.Invoke{ State: "HALT", - GasConsumed: 32414250, + GasConsumed: 31922970, Script: script, Stack: []stackitem.Item{stackitem.Make(true)}, Notifications: []state.NotificationEvent{{ @@ -1078,7 +1081,7 @@ var rpcTestCases = map[string][]rpcTestCase{ cryptoHash, _ := e.chain.GetNativeContractScriptHash(nativenames.CryptoLib) return &result.Invoke{ State: "HALT", - GasConsumed: 15928320, + GasConsumed: 13970250, Script: script, Stack: []stackitem.Item{stackitem.Make("1.2.3.4")}, Notifications: []state.NotificationEvent{}, @@ -1605,12 +1608,12 @@ var rpcTestCases = map[string][]rpcTestCase{ "sendrawtransaction": { { name: "positive", - params: `["ABwAAACWP5gAAAAAAEDaEgAAAAAAFwAAAAHunqIsJ+NL0BSPxBCOCPdOj1BIsoAAXgsDAOh2SBcAAAAMFBEmW7QXJQBBvgTo+iQOOPV8HlabDBTunqIsJ+NL0BSPxBCOCPdOj1BIshTAHwwIdHJhbnNmZXIMFPVj6kC8KD1NDgXEjqMFs/Kgc0DvQWJ9W1IBQgxAEh2U53FB2sU+eeLwTAUqMM5518nsDGil4Oi5IoBiMM7hvl6lKGoYIEaVkf7cS6x4MX1RmSHcoOabKFTyuEXI3SgMIQKzYiv0AXvf4xfFiu1fTHU/IGt9uJYEb6fXdLvEv3+NwkFW57Mn"]`, + params: `["AB0AAACWP5gAAAAAAEDaEgAAAAAAGAAAAAHunqIsJ+NL0BSPxBCOCPdOj1BIsoAAXgsDAOh2SBcAAAAMFBEmW7QXJQBBvgTo+iQOOPV8HlabDBTunqIsJ+NL0BSPxBCOCPdOj1BIshTAHwwIdHJhbnNmZXIMFPVj6kC8KD1NDgXEjqMFs/Kgc0DvQWJ9W1IBQgxAJ6norhWoZxp+Hj1JFhi+Z3qI9DUkLSbfsbaLSaJIqxTfdmPbNFDVK1G+oa+LWmpRp/bj9+QZM7yC+S6HXUI7rigMIQKzYiv0AXvf4xfFiu1fTHU/IGt9uJYEb6fXdLvEv3+NwkFW57Mn"]`, result: func(e *executor) interface{} { return &result.RelayResult{} }, check: func(t *testing.T, e *executor, inv interface{}) { res, ok := inv.(*result.RelayResult) require.True(t, ok) - expectedHash := "e4418a8bdad8cdf401aabb277c7bec279d0b0113812c09607039c4ad87204d90" + expectedHash := "c11861dec1dd0f188608b725095041fcfc90abe51eea044993f122f22472753e" assert.Equal(t, expectedHash, res.Hash.StringLE()) }, }, @@ -2232,7 +2235,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] require.NoErrorf(t, err, "could not parse response: %s", txOut) assert.Equal(t, *block.Transactions[0], actual.Transaction) - assert.Equal(t, 23, actual.Confirmations) + assert.Equal(t, 24, actual.Confirmations) assert.Equal(t, TXHash, actual.Transaction.Hash()) }) @@ -2345,12 +2348,12 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] require.NoError(t, json.Unmarshal(res, actual)) checkNep17TransfersAux(t, e, actual, sent, rcvd) } - t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{18, 19, 20, 21}, []int{3, 4}) }) + t.Run("time frame only", func(t *testing.T) { testNEP17T(t, 4, 5, 0, 0, []int{19, 20, 21, 22}, []int{3, 4}) }) t.Run("no res", func(t *testing.T) { testNEP17T(t, 100, 100, 0, 0, []int{}, []int{}) }) - t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{15, 16}, []int{2}) }) - t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{18}, []int{3}) }) - t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{17, 18}, []int{3}) }) - t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{19, 20}, []int{4}) }) + t.Run("limit", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 0, []int{16, 17}, []int{2}) }) + t.Run("limit 2", func(t *testing.T) { testNEP17T(t, 4, 5, 2, 0, []int{19}, []int{3}) }) + t.Run("limit with page", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 1, []int{18, 19}, []int{3}) }) + t.Run("limit with page 2", func(t *testing.T) { testNEP17T(t, 1, 7, 3, 2, []int{20, 21}, []int{4}) }) }) prepareIteratorSession := func(t *testing.T) (uuid.UUID, uuid.UUID) { @@ -2557,7 +2560,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] t.Run("contract-based verification with parameters", func(t *testing.T) { verAcc, err := util.Uint160DecodeStringLE(verifyWithArgsContractHash) require.NoError(t, err) - checkContract(t, verAcc, []byte{}, 490890) // No C# match, but we believe it's OK and it differs from the one above. + checkContract(t, verAcc, []byte{}, 245250) // No C# match, but we believe it's OK and it differs from the one above. }) t.Run("contract-based verification with invocation script", func(t *testing.T) { verAcc, err := util.Uint160DecodeStringLE(verifyWithArgsContractHash) @@ -2567,7 +2570,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) [] emit.Int(invocWriter.BinWriter, 5) emit.String(invocWriter.BinWriter, "") invocScript := invocWriter.Bytes() - checkContract(t, verAcc, invocScript, 393720) // No C# match, but we believe it's OK and it has a specific invocation script overriding anything server-side. + checkContract(t, verAcc, invocScript, 148080) // No C# match, but we believe it's OK and it has a specific invocation script overriding anything server-side. }) }) } @@ -2719,8 +2722,8 @@ func checkNep17Balances(t *testing.T, e *executor, acc interface{}) { }, { Asset: e.chain.UtilityTokenHash(), - Amount: "37099660700", - LastUpdated: 22, + Amount: "37106285100", + LastUpdated: 23, Decimals: 8, Name: "GasToken", Symbol: "GAS", @@ -2834,7 +2837,7 @@ func checkNep11TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc } func checkNep17Transfers(t *testing.T, e *executor, acc interface{}) { - checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}) + checkNep17TransfersAux(t, e, acc, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24}, []int{0, 1, 2, 3, 4, 5, 6, 7, 8}) } func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rcvd []int) { @@ -2843,6 +2846,11 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc rublesHash, err := util.Uint160DecodeStringLE(testContractHash) require.NoError(t, err) + blockWithFAULTedTx, err := e.chain.GetBlock(e.chain.GetHeaderHash(int(faultedTxBlock))) // Transaction with ABORT inside. + require.NoError(t, err) + require.Equal(t, 1, len(blockWithFAULTedTx.Transactions)) + txFAULTed := blockWithFAULTedTx.Transactions[0] + blockDeploy6, err := e.chain.GetBlock(e.chain.GetHeaderHash(22)) // deploy Storage contract (storage_contract.go) require.NoError(t, err) require.Equal(t, 1, len(blockDeploy6.Transactions)) @@ -2947,6 +2955,14 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc // duplicate the Server method. expected := result.NEP17Transfers{ Sent: []result.NEP17Transfer{ + { + Timestamp: blockWithFAULTedTx.Timestamp, + Asset: e.chain.UtilityTokenHash(), + Address: "", // burn + Amount: big.NewInt(txFAULTed.SystemFee + txFAULTed.NetworkFee).String(), + Index: 23, + TxHash: blockWithFAULTedTx.Hash(), + }, { Timestamp: blockDeploy6.Timestamp, Asset: e.chain.UtilityTokenHash(), diff --git a/pkg/services/rpcsrv/subscription_test.go b/pkg/services/rpcsrv/subscription_test.go index d7f2a2100..60496feba 100644 --- a/pkg/services/rpcsrv/subscription_test.go +++ b/pkg/services/rpcsrv/subscription_test.go @@ -12,6 +12,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/neorpc" + "github.com/nspcc-dev/neo-go/pkg/util" "github.com/stretchr/testify/require" "go.uber.org/atomic" ) @@ -270,8 +271,9 @@ func TestFilteredSubscriptions(t *testing.T) { }, }, "execution non-matching": { - params: `["transaction_executed", {"state":"FAULT"}]`, - check: func(t *testing.T, _ *neorpc.Notification) { + // We have single FAULTed transaction in chain, this, use the wrong hash for this test instead of FAULT state. + params: `["transaction_executed", {"container":"0x` + util.Uint256{}.StringLE() + `"}]`, + check: func(t *testing.T, n *neorpc.Notification) { t.Fatal("unexpected match for faulted execution") }, }, diff --git a/pkg/services/rpcsrv/testdata/testblocks.acc b/pkg/services/rpcsrv/testdata/testblocks.acc index 965230d6a41c18d15ae5096cba629b1e7f2bda78..173d18a8ce8bfe5999e3e205b0cb30e1e021a1eb 100644 GIT binary patch delta 10947 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

`x6bCFIj;su%{BOWttoyYxtc7vzu6lwMq z8vkqF%lvl)vX;2o7c4TOB`&v~op0)gW1h0|D)ykqS6%M6M!PJdhWcHwFq6;4W?$|Z zRFv4$ODrUt9A^gHjTp+C;~bfV++#XA82&PnW@o(Z{>B;`@ys5M0ooptQ&#CpH3i9z zxCU&ur@mMWi!o5Ui(_cH#E1%zg5?H(t&KXoD=V5pdAt)MWtEZ|IDNMN#bbMo_w@6N zQ-uA5nvoEbKRgX27$|b&fy%W$c4UM8b$=c01d=Dto#54UH+U_*F2wI zW>W&J&a422k`g$7+LXY|D1lrH3($R97QlGu!K4&VZpFb{?pAgcNhq-C84PoyAaE-{ zqt_2&A@YkbppKfWb-;TFDh`4a!r|H(guWo!KSAYe?!VcM6KMYDDDDS>3Z8kO$M-k1 z!GS{*!Gn6Bo2L=_3)l$41$+OC3-SKLg?Rt}T!^R}4(|n$1%2RX@O+OmLH`6XwOc6O z%M{)CTNY}99sy-IP!`5uCPyGM%wfP~KX>SLVA$^p6b_j9-xnnSm(Xw{^eGUO__^VX z&=0_df0+AkEKFcQNb!Ft8bCRK_0r#LjI3K|co=9w7T7-!#9&lz;h@GIFu!j2aFBSx z|CS4Za=ZyT8o2b{z)a~O;xOrF+*3~2lg^QWbwha8Nz4kRZ0Ce_*v(6DM$^eJ{b}u^DoENp zsn?V^R%ITr_m8FL#~KEEWv`x>8H@V*)XgHRTg>OBssMBfco-rc{rcV%V|`Mm zg!pm(p1k_6Z!O(D`0!uD@~%nhxQT}??&bQc4{?$Fh@+Z2_&Xtda2sv4HfKyRC&h&r zMLQC=R5*q{b@Yp?Hos49YUmk*9K`VZo$^ch0d+0Erk#t1!|V$}_Z+2I5IGOygG|C_DViYex6>Y3#-6L0mq~$s@heLrsx z(OZdDk}LN)8@j9V!bHxPw5CDEakAn-dP!x{jCo4YS_VB~mmp&TLy~a19nixE)8m_O zVIJKdy>U2=yw(1xqrk75IsnMz)xeZTN>-nfK7Vy@dDd4wytL`Zne2;&{Ew%mge<;g zKhj`}K}}HJi60?d)=JSc@~31tt~K}TiFmkQsLM6^p50QGkN_Yij2v^m`SD9T zf*ubjx_us-e-aWO^;q`}Z=GS8dYC?t>{5Yukv1y-K@$$x{3^dmLKN7*v!jwW!RqzQ zcj;grcp4@i9be=%;s#&QCr?2`=;ivtD$G`Dv=+ zzH>v#W?WOmo^^1${`d^seXD|ppY=24)jT799-l*CeK=|0m@!+DKAU{*zAe_A_wxnc z&cK^usO9U=|7zE+tEUsD_B{ZUNV}Ek zp-i6&LsX;Am_aa328fL`h+Y^jd=hn;Z~S8Yhf|k;JiE&6oXs)~M;;)N^~2(-pS&P% z-4-p+6@B^_mggFHHoj*ZFi|g}Ya(WiQ0o0A!@}T^t}B5-F2J$E-{gl_o2|y4Bm2~A zlBt+1)lfOl=cle`H$ulu;RQ9a4>gQ3n)D-0B%*2Zj$fV=lBaX2hGDZ{QgBR@xoqe` z$aY5)9eDA!#%^c)Ebwt|v9xZ_VKZv~np8;zd-Xiq2jguNB}A910LxhM=zigBa&be4+YkrZ%seLu+Yg6D z>Xi_#2AjLLcNWK>IL^N}ORoaetJ%#&WM0mFSzO)d9DW9_c!N;Yr(z@3pZfW;K zGAw*O4GS7??iA=((z;bY4?JhO4;>siDL7WJ;4aY41B-DH|6Oo1yY!!4?a(v6eB$|+U1cd-JtWx)No8%A8Qkx+M2Do^90Zk&@C>JZzc?O_OFo-T{IvQ3foKG%Q zj>4QL!zEG0ZUv5C$hh6NP~K`Y!F$|uXFMC@x1*KT8P}Y_-p?j-$ZR*~W#kmJc)uhU zi0p)8g0I|d%9>UKypOw$Bsq5kqee*E+^pv-jH14wgD%T@z3iCT-KTjrs5w^4vHeRf zVzPuT!y|m;{Ob|DH>TJ@<}x^@qPAh#Yf>{IQkZ+z&q2L=kNFzv$as62^&aCw&4T&q z>aYBxH@2r2&v}LCkzdMK<8!j)lYM7;h5c!WJ${X9|D@pVCmIr2;J{x+!ed$KBx=-h zL8~Z4(6rLxGpPZ^6`ia75*ZI`_cDD4Rv0g@8~TXJpyG`S1|UA)S+n(MAIx>6(hS9w z)))RN6pSlcW?46b{+^5Q$pmb&vPYjj6u5qj_I{x*tGJtBOyxS2s2CID+n0cQTHME3 z_q@ieC|T|aJyGC|rJ^GNSxMTqNA$CYN3AnbU#RbE&c5t}V@{Q@}vi-G+g*C4yz$dhkO;4LDn7F3b&Pi-BEH+W8^aZ=~y?P6Dm zl-~51qb}qOojNk445DqYJ50H2-O5GxUY0vb&!{GWOHib1ty%Kp_mJU%R{}iz{VgXq Gxc>)6GspP= delta 10473 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