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 }