github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/rpcclient/waiter/waiter.go (about)

     1  package waiter
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/nspcc-dev/neo-go/pkg/core/block"
    11  	"github.com/nspcc-dev/neo-go/pkg/core/state"
    12  	"github.com/nspcc-dev/neo-go/pkg/neorpc"
    13  	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
    14  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
    15  	"github.com/nspcc-dev/neo-go/pkg/util"
    16  )
    17  
    18  // PollingBasedRetryCount is a threshold for a number of subsequent failed
    19  // attempts to get block count from the RPC server for PollingBased. If it fails
    20  // to retrieve block count PollingBasedRetryCount times in a raw then transaction
    21  // awaiting attempt considered to be failed and an error is returned.
    22  const PollingBasedRetryCount = 3
    23  
    24  var (
    25  	// ErrTxNotAccepted is returned when transaction wasn't accepted to the chain
    26  	// even after ValidUntilBlock block persist.
    27  	ErrTxNotAccepted = errors.New("transaction was not accepted to chain")
    28  	// ErrContextDone is returned when Waiter context has been done in the middle
    29  	// of transaction awaiting process and no result was received yet.
    30  	ErrContextDone = errors.New("waiter context done")
    31  	// ErrAwaitingNotSupported is returned from Wait method if Waiter instance
    32  	// doesn't support transaction awaiting.
    33  	ErrAwaitingNotSupported = errors.New("awaiting not supported")
    34  	// ErrMissedEvent is returned when RPCEventBased closes receiver channel
    35  	// which happens if missed event was received from the RPC server.
    36  	ErrMissedEvent = errors.New("some event was missed")
    37  )
    38  
    39  type (
    40  	// Waiter is an interface providing transaction awaiting functionality.
    41  	Waiter interface {
    42  		// Wait allows to wait until transaction will be accepted to the chain. It can be
    43  		// used as a wrapper for Send or SignAndSend and accepts transaction hash,
    44  		// ValidUntilBlock value and an error. It returns transaction execution result
    45  		// or an error if transaction wasn't accepted to the chain. Notice that "already
    46  		// exists" err value is not treated as an error by this routine because it
    47  		// means that the transactions given might be already accepted or soon going
    48  		// to be accepted. Such transaction can be waited for in a usual way, potentially
    49  		// with positive result, so that's what will happen.
    50  		Wait(h util.Uint256, vub uint32, err error) (*state.AppExecResult, error)
    51  		// WaitAny waits until at least one of the specified transactions will be accepted
    52  		// to the chain until vub (including). It returns execution result of this
    53  		// transaction or an error if none of the transactions was accepted to the chain.
    54  		// It uses underlying RPCPollingBased or RPCEventBased context to interrupt
    55  		// awaiting process, but additional ctx can be passed as an argument for the same
    56  		// purpose.
    57  		WaitAny(ctx context.Context, vub uint32, hashes ...util.Uint256) (*state.AppExecResult, error)
    58  	}
    59  	// RPCPollingBased is an interface that enables transaction awaiting functionality
    60  	// based on periodical BlockCount and ApplicationLog polls.
    61  	RPCPollingBased interface {
    62  		// Context should return the RPC client context to be able to gracefully
    63  		// shut down all running processes (if so).
    64  		Context() context.Context
    65  		GetVersion() (*result.Version, error)
    66  		GetBlockCount() (uint32, error)
    67  		GetApplicationLog(hash util.Uint256, trig *trigger.Type) (*result.ApplicationLog, error)
    68  	}
    69  	// RPCEventBased is an interface that enables improved transaction awaiting functionality
    70  	// based on web-socket Block and ApplicationLog notifications. RPCEventBased
    71  	// contains RPCPollingBased under the hood and falls back to polling when subscription-based
    72  	// awaiting fails.
    73  	RPCEventBased interface {
    74  		RPCPollingBased
    75  
    76  		ReceiveHeadersOfAddedBlocks(flt *neorpc.BlockFilter, rcvr chan<- *block.Header) (string, error)
    77  		ReceiveBlocks(flt *neorpc.BlockFilter, rcvr chan<- *block.Block) (string, error)
    78  		ReceiveExecutions(flt *neorpc.ExecutionFilter, rcvr chan<- *state.AppExecResult) (string, error)
    79  		Unsubscribe(id string) error
    80  	}
    81  )
    82  
    83  // Null is a Waiter stub that doesn't support transaction awaiting functionality.
    84  type Null struct{}
    85  
    86  // PollingBased is a polling-based Waiter.
    87  type PollingBased struct {
    88  	polling RPCPollingBased
    89  	version *result.Version
    90  }
    91  
    92  // EventBased is a websocket-based Waiter.
    93  type EventBased struct {
    94  	ws      RPCEventBased
    95  	polling Waiter
    96  }
    97  
    98  // errIsAlreadyExists is a temporary helper until we have #2248 solved. Both C#
    99  // and Go nodes return this string (possibly among other data).
   100  func errIsAlreadyExists(err error) bool {
   101  	return strings.Contains(strings.ToLower(err.Error()), "already exists")
   102  }
   103  
   104  // New creates Waiter instance. It can be either websocket-based or
   105  // polling-base, otherwise Waiter stub is returned. As a first argument
   106  // it accepts RPCEventBased implementation, RPCPollingBased implementation
   107  // or not an implementation of these two interfaces. It returns websocket-based
   108  // waiter, polling-based waiter or a stub correspondingly.
   109  func New(base any, v *result.Version) Waiter {
   110  	if eventW, ok := base.(RPCEventBased); ok {
   111  		return &EventBased{
   112  			ws: eventW,
   113  			polling: &PollingBased{
   114  				polling: eventW,
   115  				version: v,
   116  			},
   117  		}
   118  	}
   119  	if pollW, ok := base.(RPCPollingBased); ok {
   120  		return &PollingBased{
   121  			polling: pollW,
   122  			version: v,
   123  		}
   124  	}
   125  	return NewNull()
   126  }
   127  
   128  // NewNull creates an instance of Waiter stub.
   129  func NewNull() Null {
   130  	return Null{}
   131  }
   132  
   133  // Wait implements Waiter interface.
   134  func (Null) Wait(h util.Uint256, vub uint32, err error) (*state.AppExecResult, error) {
   135  	return nil, ErrAwaitingNotSupported
   136  }
   137  
   138  // WaitAny implements Waiter interface.
   139  func (Null) WaitAny(ctx context.Context, vub uint32, hashes ...util.Uint256) (*state.AppExecResult, error) {
   140  	return nil, ErrAwaitingNotSupported
   141  }
   142  
   143  // NewPollingBased creates an instance of Waiter supporting poll-based transaction awaiting.
   144  func NewPollingBased(waiter RPCPollingBased) (*PollingBased, error) {
   145  	v, err := waiter.GetVersion()
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  	return &PollingBased{
   150  		polling: waiter,
   151  		version: v,
   152  	}, nil
   153  }
   154  
   155  // Wait implements Waiter interface.
   156  func (w *PollingBased) Wait(h util.Uint256, vub uint32, err error) (*state.AppExecResult, error) {
   157  	if err != nil && !errIsAlreadyExists(err) {
   158  		return nil, err
   159  	}
   160  	return w.WaitAny(context.TODO(), vub, h)
   161  }
   162  
   163  // WaitAny implements Waiter interface.
   164  func (w *PollingBased) WaitAny(ctx context.Context, vub uint32, hashes ...util.Uint256) (*state.AppExecResult, error) {
   165  	var (
   166  		currentHeight uint32
   167  		failedAttempt int
   168  		pollTime      = time.Millisecond * time.Duration(w.version.Protocol.MillisecondsPerBlock) / 2
   169  	)
   170  	if pollTime == 0 {
   171  		pollTime = time.Second
   172  	}
   173  	timer := time.NewTicker(pollTime)
   174  	defer timer.Stop()
   175  	for {
   176  		select {
   177  		case <-timer.C:
   178  			blockCount, err := w.polling.GetBlockCount()
   179  			if err != nil {
   180  				failedAttempt++
   181  				if failedAttempt > PollingBasedRetryCount {
   182  					return nil, fmt.Errorf("failed to retrieve block count: %w", err)
   183  				}
   184  				continue
   185  			}
   186  			failedAttempt = 0
   187  			if blockCount-1 > currentHeight {
   188  				currentHeight = blockCount - 1
   189  			}
   190  			t := trigger.Application
   191  			for _, h := range hashes {
   192  				res, err := w.polling.GetApplicationLog(h, &t)
   193  				if err == nil {
   194  					return &state.AppExecResult{
   195  						Container: res.Container,
   196  						Execution: res.Executions[0],
   197  					}, nil
   198  				}
   199  			}
   200  			if currentHeight >= vub {
   201  				return nil, ErrTxNotAccepted
   202  			}
   203  		case <-w.polling.Context().Done():
   204  			return nil, fmt.Errorf("%w: %w", ErrContextDone, w.polling.Context().Err())
   205  		case <-ctx.Done():
   206  			return nil, fmt.Errorf("%w: %w", ErrContextDone, ctx.Err())
   207  		}
   208  	}
   209  }
   210  
   211  // NewEventBased creates an instance of Waiter supporting websocket event-based transaction awaiting.
   212  // EventBased contains PollingBased under the hood and falls back to polling when subscription-based
   213  // awaiting fails.
   214  func NewEventBased(waiter RPCEventBased) (*EventBased, error) {
   215  	polling, err := NewPollingBased(waiter)
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  	return &EventBased{
   220  		ws:      waiter,
   221  		polling: polling,
   222  	}, nil
   223  }
   224  
   225  // Wait implements Waiter interface.
   226  func (w *EventBased) Wait(h util.Uint256, vub uint32, err error) (res *state.AppExecResult, waitErr error) {
   227  	if err != nil && !errIsAlreadyExists(err) {
   228  		return nil, err
   229  	}
   230  	return w.WaitAny(context.TODO(), vub, h)
   231  }
   232  
   233  // WaitAny implements Waiter interface.
   234  func (w *EventBased) WaitAny(ctx context.Context, vub uint32, hashes ...util.Uint256) (res *state.AppExecResult, waitErr error) {
   235  	var (
   236  		wsWaitErr     error
   237  		waitersActive int
   238  		hRcvr         = make(chan *block.Header, 2)
   239  		bRcvr         = make(chan *block.Block, 2)
   240  		aerRcvr       = make(chan *state.AppExecResult, len(hashes))
   241  		unsubErrs     = make(chan error)
   242  		exit          = make(chan struct{})
   243  	)
   244  
   245  	// Execution event preceded the block event, thus wait until the VUB-th block to be sure.
   246  	since := vub
   247  	blocksID, err := w.ws.ReceiveHeadersOfAddedBlocks(&neorpc.BlockFilter{Since: &since}, hRcvr)
   248  	if err != nil {
   249  		// Falling back to block-based subscription.
   250  		if errors.Is(err, neorpc.ErrInvalidParams) {
   251  			blocksID, err = w.ws.ReceiveBlocks(&neorpc.BlockFilter{Since: &since}, bRcvr)
   252  		}
   253  	}
   254  	if err != nil {
   255  		wsWaitErr = fmt.Errorf("failed to subscribe for new blocks/headers: %w", err)
   256  	} else {
   257  		waitersActive++
   258  		go func() {
   259  			<-exit
   260  			err = w.ws.Unsubscribe(blocksID)
   261  			if err != nil {
   262  				unsubErrs <- fmt.Errorf("failed to unsubscribe from blocks/headers (id: %s): %w", blocksID, err)
   263  				return
   264  			}
   265  			unsubErrs <- nil
   266  		}()
   267  	}
   268  	if wsWaitErr == nil {
   269  		trig := trigger.Application
   270  		for _, h := range hashes {
   271  			txsID, err := w.ws.ReceiveExecutions(&neorpc.ExecutionFilter{Container: &h}, aerRcvr)
   272  			if err != nil {
   273  				wsWaitErr = fmt.Errorf("failed to subscribe for execution results: %w", err)
   274  				break
   275  			}
   276  			waitersActive++
   277  			go func() {
   278  				<-exit
   279  				err = w.ws.Unsubscribe(txsID)
   280  				if err != nil {
   281  					unsubErrs <- fmt.Errorf("failed to unsubscribe from transactions (id: %s): %w", txsID, err)
   282  					return
   283  				}
   284  				unsubErrs <- nil
   285  			}()
   286  			// There is a potential race between subscription and acceptance, so
   287  			// do a polling check once _after_ the subscription.
   288  			appLog, err := w.ws.GetApplicationLog(h, &trig)
   289  			if err == nil {
   290  				res = &state.AppExecResult{
   291  					Container: appLog.Container,
   292  					Execution: appLog.Executions[0],
   293  				}
   294  				break // We have the result, no need for other subscriptions.
   295  			}
   296  		}
   297  	}
   298  
   299  	if wsWaitErr == nil && res == nil {
   300  		select {
   301  		case _, ok := <-hRcvr:
   302  			if !ok {
   303  				// We're toast, retry with non-ws client.
   304  				hRcvr = nil
   305  				bRcvr = nil
   306  				aerRcvr = nil
   307  				wsWaitErr = ErrMissedEvent
   308  				break
   309  			}
   310  			waitErr = ErrTxNotAccepted
   311  		case _, ok := <-bRcvr:
   312  			if !ok {
   313  				// We're toast, retry with non-ws client.
   314  				hRcvr = nil
   315  				bRcvr = nil
   316  				aerRcvr = nil
   317  				wsWaitErr = ErrMissedEvent
   318  				break
   319  			}
   320  			waitErr = ErrTxNotAccepted
   321  		case aer, ok := <-aerRcvr:
   322  			if !ok {
   323  				// We're toast, retry with non-ws client.
   324  				hRcvr = nil
   325  				bRcvr = nil
   326  				aerRcvr = nil
   327  				wsWaitErr = ErrMissedEvent
   328  				break
   329  			}
   330  			res = aer
   331  		case <-w.ws.Context().Done():
   332  			waitErr = fmt.Errorf("%w: %w", ErrContextDone, w.ws.Context().Err())
   333  		case <-ctx.Done():
   334  			waitErr = fmt.Errorf("%w: %w", ErrContextDone, ctx.Err())
   335  		}
   336  	}
   337  	close(exit)
   338  
   339  	if waitersActive > 0 {
   340  		// Drain receivers to avoid other notification receivers blocking.
   341  	drainLoop:
   342  		for {
   343  			select {
   344  			case _, ok := <-hRcvr:
   345  				if !ok { // Missed event means both channels are closed.
   346  					hRcvr = nil
   347  					bRcvr = nil
   348  					aerRcvr = nil
   349  				}
   350  			case _, ok := <-bRcvr:
   351  				if !ok { // Missed event means both channels are closed.
   352  					hRcvr = nil
   353  					bRcvr = nil
   354  					aerRcvr = nil
   355  				}
   356  			case _, ok := <-aerRcvr:
   357  				if !ok { // Missed event means both channels are closed.
   358  					hRcvr = nil
   359  					bRcvr = nil
   360  					aerRcvr = nil
   361  				}
   362  			case unsubErr := <-unsubErrs:
   363  				if unsubErr != nil {
   364  					errFmt := "unsubscription error: %w"
   365  					errArgs := []any{unsubErr}
   366  					if waitErr != nil {
   367  						errFmt = "%w; " + errFmt
   368  						errArgs = append([]any{waitErr}, errArgs...)
   369  					}
   370  					waitErr = fmt.Errorf(errFmt, errArgs...)
   371  				}
   372  				waitersActive--
   373  				// Wait until all receiver channels finish their work.
   374  				if waitersActive == 0 {
   375  					break drainLoop
   376  				}
   377  			}
   378  		}
   379  	}
   380  	if hRcvr != nil {
   381  		close(hRcvr)
   382  	}
   383  	if bRcvr != nil {
   384  		close(bRcvr)
   385  	}
   386  	if aerRcvr != nil {
   387  		close(aerRcvr)
   388  	}
   389  	close(unsubErrs)
   390  
   391  	// Rollback to a poll-based waiter if needed.
   392  	if wsWaitErr != nil && waitErr == nil {
   393  		res, waitErr = w.polling.WaitAny(ctx, vub, hashes...)
   394  		if waitErr != nil {
   395  			// Wrap the poll-based error, it's more important.
   396  			waitErr = fmt.Errorf("event-based error: %w; poll-based waiter error: %w", wsWaitErr, waitErr)
   397  		}
   398  	}
   399  	return
   400  }