Independent tick: server trusts client frame delta with a time budget that has no cap, allowing speed bursts

Summary

With PreferredTickingPolicy=Independent, the server simulates an autonomous proxy using the delta time sent by the client. The delta is serialized as an unsigned integer with no validation, and is only clamped to 1-200 ms per input. The server also limits each frame to 6 steps and 200 ms in total.

An input is simulated only if the real time accumulated by the server for that pawn (UnspentTimeMS) covers it. That budget grows with the server’s real time, has no cap, and is reset only when the server configures the instance (at spawn or on a role change), never during play. A modified client can therefore stay idle and later spend the stored time faster than real time.

By our calculation (not measured): 200 ms per server frame at about 55 fps is roughly 11x real time. While the stored time is being spent, the budget keeps growing by 1 s per second, so a 60 s reserve shrinks by about 10 s per second and lasts about 6 s. After 60 s idle, a client could thus move at roughly 11x speed for about 6 s on the authoritative server.

A related issue: when the budget is insufficient, or when the 200 ms per-frame total is reached, the input is skipped without being simulated. A client running slightly fast therefore loses inputs silently. We expect these to come back to the client as corrections, but we have not measured it.

What Type of Bug are you experiencing?

Networking

Steps to Reproduce

Note: this was found by reading the source; we have not yet reproduced it at runtime. Steps 1-4 show the uncapped budget; step 5 requires a modified client.

  1. Project with a Mover character replicated through Network Prediction, Config/DefaultNetworkPrediction.ini: PreferredTickingPolicy=Independent.
  2. PIE, Run Under One Process, Play As Client, dedicated server, 1 client.
  3. On the server, log once per second UnspentTimeMS of the client’s autonomous proxy (TServerRecvData_Independent::UnspentTimeMS), for example with a temporary log line in TRemoteIndependentTickService::Tick or a debugger watch.
  4. Stop the client from sending inputs for 60 s (for example with a breakpoint in the client’s input send path, or by simulating a network stall), then resume. UnspentTimeMS grows by about 1000 ms per second for the whole 60 s, with no cap.
  5. With a modified client that sends DeltaTimeMS = 200 for every input after the pause, the server simulates up to 200 ms per server frame until the stored budget is spent: by our calculation about 6 s at roughly 11x real time.

Expected Result

The accumulated time budget is capped (for example to a few hundred milliseconds), so a client cannot store time and simulate faster than real time. Inputs that cannot be simulated in the current frame are deferred rather than skipped.

Observed Result

The budget grows without limit during play while the client sends no inputs, which by the source would allow bursts of accelerated movement on the authoritative server. Inputs are dropped silently when the budget or the per-frame total is exceeded.

Affects Versions

5.8

Platform(s)

Windows

Additional Notes

Relevant code (paths relative to Engine/Plugins/Runtime/NetworkPrediction/Source/NetworkPrediction/)

  • Client delta read with no validation: Public/NetworkPredictionSerialization.h, TIndependentTickReplicator_Server::NetRecv, lines 253-310 (DeltaTimeMS read at line 295; SerializeDeltaMS, lines 98-101)
  • Budget field: Public/Services/NetworkPredictionInstanceData.h, TServerRecvData_Independent::UnspentTimeMS, line 148
  • Budget reset only at instance configuration: Public/NetworkPredictionWorldManager.h, UNetworkPredictionWorldManager::BindServerNetRecv_Independent, lines 460-477 (reset at line 475)
  • In Public/Services/NetworkPredictionService_Ticking.inl, TRemoteIndependentTickService::Tick:
    • budget growth with server real time: line 254
    • LastConsumedFrame incremented before the checks: line 266
    • per-input clamp 1-200 ms: line 275
    • input simulated only if the budget covers it, otherwise break: lines 277-280
    • 200 ms per-frame total, break after the increment at line 266, so that input is skipped: lines 283-286
    • budget consumption, the only other write during play: line 291
    • 6 steps per frame: lines 316-319
    • MaxRemoteClientStepMS = 100 is declared but not used: line 223

Plugins: NetworkPrediction, Mover