activity_transaction_notifications.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. #include "activity_transaction_notifications.h"
  2. #include <bit>
  3. #include <algorithm>
  4. #include "../../../../core/logging/log.h"
  5. #include "../../../../middleware/secure_channel/runtime.h"
  6. #include "../../../gameplay/gameplay_advertisement.h"
  7. #include "../push/activity/activity_arrival.h"
  8. #include "../push/activity/activity_global_state_push.h"
  9. #include "../push/activity/activity_membership_push.h"
  10. #include "../push/activity/activity_message_push.h"
  11. #include "../push/activity/activity_notification_frame.h"
  12. #include "../push/activity/activity_roster_push.h"
  13. #include "../push/activity/activity_world_globals_push.h"
  14. #include "../push/activity/internal.h"
  15. namespace sunrise::server::bap::encrypted::activity_transaction {
  16. namespace {
  17. /** Publishes the exact abandoned mask with the next entity-slot epoch. */
  18. [[nodiscard]] bool stage_purge(Session& session, Scratch& scratch,
  19. const activity_message::ActivityPlan& activity,
  20. std::span<const std::byte, state::kAesKeySize> key,
  21. std::array<std::byte, state::kBapNonceSize>& nonce,
  22. std::span<std::byte> response, std::size_t& written) noexcept {
  23. namespace control = middleware::bap::activity_message::host_control;
  24. if (activity.authorityPurgeGeneration != session.activity.bindingGeneration
  25. || activity.sessionId != session.activity.session.sessionId
  26. || activity.authorityPurge.epoch != unsigned(session.activity.authorityEpoch) + 1U
  27. || written > response.size()) return false;
  28. std::array<std::byte, control::kPurgeAuthorityByteCount> body{};
  29. std::size_t bodySize{};
  30. if (!control::encode_purge_authority(activity.authorityPurge, body, bodySize)) return false;
  31. const auto initialWritten = written;
  32. const auto initialNonce = nonce;
  33. if (!push::activity::append_notification_frame(scratch, activity.sessionId,
  34. control::kPurgeAuthorityMessageType, std::span(body).first(bodySize),
  35. key, nonce, response, written)) {
  36. if (written > initialWritten)
  37. std::fill(response.begin() + initialWritten, response.begin() + written, std::byte{});
  38. written = initialWritten;
  39. nonce = initialNonce;
  40. return false;
  41. }
  42. middleware::secure_channel::advance_nonce(nonce);
  43. unsigned slots = 0;
  44. for (const auto byte : activity.authorityPurge.slots)
  45. slots += std::popcount(std::to_integer<unsigned>(byte));
  46. core::log::writef(core::log::Channel::server, core::log::Level::info,
  47. "ev=authority_cleanup stage=purge_staged session=%llu epoch=%u reason=%d slots=%u",
  48. static_cast<unsigned long long>(activity.sessionId),
  49. static_cast<unsigned>(activity.authorityPurge.epoch),
  50. static_cast<int>(activity.authorityPurge.reason), slots);
  51. return true;
  52. }
  53. /**
  54. * Reports whether the citizen advertisement this membership body would carry is still coming.
  55. * One membership update lands per revision, so a body sent before the region's host session exists
  56. * spends that revision on a record no later push can fill. Holding costs one keepalive.
  57. * @param activity Prepared activity transaction, whose region this body publishes.
  58. * @return True when the push has to wait.
  59. */
  60. [[nodiscard]] bool advertisement_pending(const Session& session,
  61. const activity_message::ActivityPlan& activity) noexcept {
  62. if (session.activity.role != ActivityClientRole::privateCurrent
  63. || !state::activity::binding_matches(session.activity.source)) {
  64. return false;
  65. }
  66. // Take the delta's region, not the committed one. Staging runs before the commit, so the
  67. // committed value still names the region the player has left.
  68. const push::activity::EffectiveRegion region = push::activity::private_planned_region(
  69. activity.membershipMutation, session.activity.source);
  70. const server::gameplay::AdvertisementState state =
  71. push::activity::region_advertisement(session, region.index);
  72. if (state != server::gameplay::AdvertisementState::pending) {
  73. return false;
  74. }
  75. return true;
  76. }
  77. /**
  78. * Stages the whole host snapshot the client's state-refresh request asks for.
  79. * The order matches the join burst: the global state and its clock enable, then membership, then
  80. * the roster. The roster's participation key binds to the player the membership publishes.
  81. * @param session Connection-owned roster counters, advanced only by a staged roster.
  82. * @param scratch Lock-owned transform buffers.
  83. * @param activity Prepared activity transaction carrying the membership snapshot.
  84. * @param key Active AES-GCM session key.
  85. * @param nonce Local send nonce advanced only by complete staged notifications.
  86. * @param response Lock-owned complete-frame staging storage.
  87. * @param written Existing staged byte count, updated only by complete notifications.
  88. * @return True when at least one of the three notifications was staged.
  89. */
  90. [[nodiscard]] bool stage_refresh(Session& session,
  91. Scratch& scratch,
  92. const activity_message::ActivityPlan& activity,
  93. std::span<const std::byte, state::kAesKeySize> key,
  94. std::array<std::byte, state::kBapNonceSize>& nonce,
  95. std::span<std::byte> response,
  96. std::size_t& written,
  97. bool allowEntityRetirement) noexcept {
  98. bool staged = push::activity::append_global_state_notification(
  99. scratch, session.activity.session, key, nonce, response, written)
  100. && push::activity::append_world_globals_notification(
  101. scratch, session.activity.session.sessionId, key, nonce, response, written);
  102. bool stagedMembership = false;
  103. if (session.activity.role == ActivityClientRole::privateCurrent
  104. && activity.membershipMutation.hasSnapshot && !advertisement_pending(session, activity)) {
  105. stagedMembership = push::activity::append_membership_notification(
  106. scratch, session, activity, key, nonce, response, written);
  107. staged = stagedMembership || staged;
  108. }
  109. // Message 18 asks for the whole host snapshot, so its roster is solicited and is never
  110. // suppressed as a repeat. The client sends it when its own mirror is stale, which the host's
  111. // delivered-body record cannot see. Its bubble field names the slice set the client holds.
  112. const push::activity::RefreshReport refresh{activity.membershipMutation.bubbleIndex,
  113. activity.membershipMutation.requestedRevision};
  114. return push::activity::append_roster_notification(session,
  115. scratch,
  116. key,
  117. nonce,
  118. response,
  119. written,
  120. nullptr,
  121. nullptr,
  122. true,
  123. &refresh,
  124. allowEntityRetirement)
  125. || staged;
  126. }
  127. /**
  128. * Stages what one client-reported, host-committed delta owes: membership, the roster, or both.
  129. *
  130. * The roster follows membership, because its participation key binds to the player membership
  131. * publishes.
  132. * @param session Connection-owned roster counters, advanced only by a staged roster.
  133. * @param scratch Lock-owned transform buffers.
  134. * @param activity Prepared activity transaction and its region-move flag.
  135. * @param key Active AES-GCM session key.
  136. * @param nonce Local send nonce advanced only by complete staged notifications.
  137. * @param response Lock-owned complete-frame staging storage.
  138. * @param written Existing staged byte count, updated only by complete notifications.
  139. * @return True when every notification the delta owed was staged.
  140. */
  141. [[nodiscard]] bool stage_authoritative(Session& session,
  142. Scratch& scratch,
  143. const activity_message::ActivityPlan& activity,
  144. std::span<const std::byte, state::kAesKeySize> key,
  145. std::array<std::byte, state::kBapNonceSize>& nonce,
  146. std::span<std::byte> response,
  147. std::size_t& written,
  148. bool allowEntityRetirement) noexcept {
  149. bool staged = false;
  150. bool stagedMembership = false;
  151. bool held = false;
  152. bool owed = false;
  153. bool suppressed = false;
  154. if (session.activity.role == ActivityClientRole::privateCurrent
  155. && activity.membershipMutation.hasSnapshot) {
  156. owed = true;
  157. held = advertisement_pending(session, activity);
  158. if (!held) {
  159. // A delta that changed the published fields never matches the delivered body, so
  160. // only a true repeat is skipped here.
  161. stagedMembership = push::activity::append_membership_notification(
  162. scratch, session, activity, key, nonce, response, written, true, &suppressed);
  163. staged = stagedMembership;
  164. }
  165. }
  166. if (session.activity.role == ActivityClientRole::privateCurrent && activity.regionMoved) {
  167. owed = true;
  168. // This notification is encoded before the membership transaction commits. Its roster
  169. // must use the prepared move, never the old committed msg-22 region.
  170. const push::activity::EffectiveRegion region =
  171. push::activity::planned_region(activity.membershipMutation, session.activity.source);
  172. // Staged before the commit, so readiness reads the incoming leg too. Without it the
  173. // body repeats the loading lifetime this very report says is over.
  174. push::activity::RefreshReport report{};
  175. report.currentRegion = activity.membershipMutation.authoritativeInput.currentRegion.index;
  176. report.hasCurrentRegion = activity.membershipMutation.authoritativeInput.hasCurrentRegion;
  177. // The client reports the region it now holds once its slice set is instantiated, and the
  178. // roster is that report's answer. It is solicited, so it is never skipped as a repeat,
  179. // including while the slice set is still instantiating.
  180. staged = push::activity::append_roster_notification(session,
  181. scratch,
  182. key,
  183. nonce,
  184. response,
  185. written,
  186. nullptr,
  187. &region,
  188. true,
  189. &report,
  190. allowEntityRetirement)
  191. || staged;
  192. }
  193. // A delta that owed nothing is not a failure, and a public link never owes the block above.
  194. // Held and already-delivered bodies are not failures either: a false here would drop the
  195. // very commit the body reflects.
  196. return !owed || staged || held || suppressed;
  197. }
  198. } // namespace
  199. /**
  200. * Stages the notifications one activity transaction requests.
  201. * @param session Connection-owned roster counters, advanced only by a staged roster.
  202. * @param scratch Lock-owned transform buffers.
  203. * @param activity Prepared activity transaction and delivery selection.
  204. * @param key Active AES-GCM session key.
  205. * @param nonce Local send nonce advanced only by complete staged notifications.
  206. * @param response Lock-owned complete-frame staging storage.
  207. * @param written Existing staged byte count, updated only by complete notifications.
  208. * @return True when every requested notification is staged.
  209. */
  210. bool stage_notifications(Session& session,
  211. Scratch& scratch,
  212. const activity_message::ActivityPlan& activity,
  213. std::span<const std::byte, state::kAesKeySize> key,
  214. std::array<std::byte, state::kBapNonceSize>& nonce,
  215. std::span<std::byte> response,
  216. std::size_t& written,
  217. bool allowEntityRetirement) noexcept {
  218. // Each encoder refuses an absent session itself, so a plan that delivers nothing needs no
  219. // session at all. Message type 52 is the one that arrives on an unallocated link.
  220. if (activity.delivery == activity_message::Delivery::authorityPurgeNotification) {
  221. return stage_purge(session, scratch, activity, key, nonce, response, written);
  222. }
  223. if (activity.delivery == activity_message::Delivery::joinNotifications) {
  224. return push::activity::append_join_notifications(
  225. scratch, session, activity, key, nonce, response, written);
  226. }
  227. if (activity.delivery == activity_message::Delivery::purgeNotification) {
  228. namespace control = middleware::bap::activity_message::host_control;
  229. const auto& purge = activity.authorityPurge;
  230. if (!purge.pending || purge.sourceGeneration != session.activity.bindingGeneration
  231. || activity.sessionId != session.activity.session.sessionId
  232. || activity_link_count_locked(session.activity.session) != 1
  233. || purge.body.epoch
  234. != static_cast<std::uint8_t>(session.activity.replicationEpoch + 1U)) {
  235. return false;
  236. }
  237. std::array<std::byte, control::kPurgeAuthorityByteCount> body{};
  238. std::size_t bodySize = 0;
  239. if (!control::encode_purge_authority(purge.body, body, bodySize)
  240. || !push::activity::append_notification_frame(scratch,
  241. activity.sessionId,
  242. control::kPurgeAuthorityMessageType,
  243. std::span(body).first(bodySize),
  244. key,
  245. nonce,
  246. response,
  247. written)) {
  248. return false;
  249. }
  250. middleware::secure_channel::advance_nonce(nonce);
  251. return true;
  252. }
  253. if (activity.delivery == activity_message::Delivery::rosterNotification) {
  254. // The client's inbound dispatch table has no entry for a type-52 response, so that
  255. // message is a one-way report and owes nothing on its own.
  256. if (!session.activityRosterOwedForEpoch) {
  257. return true;
  258. }
  259. // The epoch it reports is what an earlier answer was missing, so that answer goes now. The
  260. // epoch comes from this message; the connection's own copy is published after this runs.
  261. return push::activity::append_roster_notification(session,
  262. scratch,
  263. key,
  264. nonce,
  265. response,
  266. written,
  267. &activity.patchEpoch,
  268. nullptr,
  269. true,
  270. nullptr,
  271. allowEntityRetirement);
  272. }
  273. if (activity.delivery == activity_message::Delivery::entitySlotNotification) {
  274. return push::activity::append_entity_slot_notification(scratch,
  275. activity.sessionId,
  276. activity.entitySlotMutation.mask,
  277. key,
  278. nonce,
  279. response,
  280. written);
  281. }
  282. if (activity.delivery == activity_message::Delivery::membershipNotification) {
  283. // A public target gets exactly one membership body, in its join burst. A second one here
  284. // would land mid-transition and would set no one-shot latch.
  285. if (session.activity.role == ActivityClientRole::publicTarget) {
  286. return true;
  287. }
  288. return push::activity::append_membership_notification(
  289. scratch, session, activity, key, nonce, response, written);
  290. }
  291. if (activity.delivery == activity_message::Delivery::refreshNotifications) {
  292. return stage_refresh(
  293. session, scratch, activity, key, nonce, response, written, allowEntityRetirement);
  294. }
  295. if (activity.delivery == activity_message::Delivery::authoritativeNotifications) {
  296. return stage_authoritative(
  297. session, scratch, activity, key, nonce, response, written, allowEntityRetirement);
  298. }
  299. return activity.delivery == activity_message::Delivery::none;
  300. }
  301. } // namespace sunrise::server::bap::encrypted::activity_transaction