diff --git a/pkg/rpcclient/actor/waiter.go b/pkg/rpcclient/actor/waiter.go index e2365a437..003b7546e 100644 --- a/pkg/rpcclient/actor/waiter.go +++ b/pkg/rpcclient/actor/waiter.go @@ -304,13 +304,17 @@ func (w *EventWaiter) WaitAny(ctx context.Context, vub uint32, hashes ...util.Ui } select { - case _, ok := <-bRcvr: + case b, ok := <-bRcvr: if !ok { // We're toast, retry with non-ws client. wsWaitErr = ErrMissedEvent return } - waitErr = ErrTxNotAccepted + // We can easily end up in a situation when subscription was performed too late and + // the desired transaction and VUB-th block have already got accepted before the + // subscription happened. Thus, always retry with non-ws client, it will perform + // AER requests and make sure. + wsWaitErr = fmt.Errorf("block #%d was received by EventWaiter", b.Index) case aer, ok := <-aerRcvr: if !ok { // We're toast, retry with non-ws client. diff --git a/pkg/rpcclient/actor/waiter_test.go b/pkg/rpcclient/actor/waiter_test.go index 881be6a7f..a3fcac31f 100644 --- a/pkg/rpcclient/actor/waiter_test.go +++ b/pkg/rpcclient/actor/waiter_test.go @@ -166,6 +166,7 @@ func TestWSWaiter_Wait(t *testing.T) { }) // Missing AER after VUB. + c.RPCClient.appLog = nil go func() { _, err = w.Wait(h, bCount-2, nil) require.ErrorIs(t, err, ErrTxNotAccepted) diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index 102e1e93e..0da3964dc 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -2073,3 +2073,58 @@ func TestWSClient_Wait(t *testing.T) { } require.True(t, faultedChecked, "FAULTed transaction wasn't checked") } + +func TestWSClient_WaitWithLateSubscription(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) + + // Firstly, accept the block. + blocks := getTestBlocks(t) + b1 := blocks[0] + b2 := blocks[1] + tx := b1.Transactions[0] + require.NoError(t, chain.AddBlock(b1)) + + // After that, subscribe for AERs/blocks and wait. + rcvr := make(chan *state.AppExecResult) + go func() { + aer, err := act.Wait(tx.Hash(), tx.ValidUntilBlock, nil) + require.NoError(t, err) + rcvr <- aer + }() + + // Accept the next block to trigger event-based waiter loop exit and rollback to + // poll-based waiter. + require.NoError(t, chain.AddBlock(b2)) + + // Wait for the result. +waitloop: + for { + select { + case aer := <-rcvr: + require.Equal(t, tx.Hash(), aer.Container) + require.Equal(t, trigger.Application, aer.Trigger) + require.Equal(t, vmstate.Halt, aer.VMState) + break waitloop + case <-time.NewTimer(time.Duration(chain.GetConfig().SecondsPerBlock) * time.Second).C: + t.Fatal("transaction failed to be awaited") + } + } +}