encrypted_runtime.cpp 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548
  1. #include <Windows.h>
  2. #include <algorithm>
  3. #include <array>
  4. #include <bit>
  5. #include <cstdio>
  6. #include "../../../core/logging/log.h"
  7. #include "../../../middleware/encoding/byte_order.h"
  8. #include "../../../middleware/secure_channel/runtime.h"
  9. #include "../../../state/activity/bubble_authority/runtime.h"
  10. #include "../../../state/runtime/runtime.h"
  11. #include "../../activity/host_runtime.h"
  12. #include "../../gameplay/peer/peer_transport.h"
  13. #include "../../gameplay/squad_entity_retirement.h"
  14. #include "../activity_authority_query_owner.h"
  15. #include "../activity_authority_reset_owner.h"
  16. #include "../internal.h"
  17. #include "activity_transaction/activity_transaction_notifications.h"
  18. #include "bap_connection_publication.h"
  19. #include "internal.h"
  20. #include "push/activity/activity_roster_push.h"
  21. #include "queuez/queuez_outcome_staging.h"
  22. #include "transactions/service_outcome_commit.h"
  23. namespace sunrise::server::bap::encrypted {
  24. namespace {
  25. /** Delay before the Family-4 copy of an artifact change, so its Family-5 refresh lands first. */
  26. constexpr std::uint64_t kArtifactFamily4RefreshDelayMs = 100;
  27. /** Traces one decoded service frame and the reply it produced. */
  28. void report_service_traffic(const middleware::bap::RequestFrame& frame,
  29. const ServiceRoute& route,
  30. std::size_t responseBodySize) noexcept {
  31. std::array<char, core::log::kLineCapacity> line{};
  32. const int written = std::snprintf(line.data(),
  33. line.size(),
  34. "ev=bap stage=service svc=%u task=%u request_bytes=%zu "
  35. "response_svc=%u response_bytes=%zu",
  36. static_cast<unsigned>(frame.serviceId),
  37. static_cast<unsigned>(frame.taskId),
  38. frame.body.size(),
  39. static_cast<unsigned>(route.response),
  40. responseBodySize);
  41. if (written > 0) {
  42. core::log::write(core::log::Channel::server,
  43. core::log::Level::debug,
  44. {line.data(), static_cast<std::size_t>(written)});
  45. }
  46. }
  47. /**
  48. * Wipes the part of one scratch buffer that may hold written bytes.
  49. * @param buffer Lock-owned scratch storage.
  50. * @param size Largest prefix that may hold transformed bytes.
  51. */
  52. void clear_prefix(std::span<std::byte> buffer, std::size_t size) noexcept {
  53. SecureZeroMemory(buffer.data(), (std::min)(buffer.size(), size));
  54. }
  55. /** Appends one join only after its exact ActivityClient binding has committed and published. */
  56. void record_committed_join(Session& session, const ConnectionFields& fields) noexcept {
  57. if (!fields.joinsActivity || !fields.joinIngress.prepared) {
  58. return;
  59. }
  60. server::activity::host::ClientMessageInput input{};
  61. input.binding = session.activity.session;
  62. input.sourceGeneration = session.activity.bindingGeneration;
  63. input.payloadFingerprint = fields.joinIngress.payloadFingerprint;
  64. input.messageType = 3;
  65. input.payloadBytes = fields.joinIngress.payloadBytes;
  66. input.peerHeardMask = fields.joinIngress.peerHeardMask;
  67. input.consumedBits = fields.joinIngress.consumedBits;
  68. input.hasPayloadFingerprint = fields.joinIngress.hasPayloadFingerprint;
  69. const std::uint64_t payloadBits =
  70. static_cast<std::uint64_t>(input.payloadBytes) * middleware::encoding::kBitsPerByte;
  71. input.status = payloadBits > input.consumedBits
  72. ? server::activity::host::ClientMessageStatus::prefixOnly
  73. : server::activity::host::ClientMessageStatus::decoded;
  74. static_cast<void>(server::activity::host::record_client_message(input));
  75. }
  76. /** Queues one safe msg-22 after-image only after State and connection publication commit. */
  77. void submit_committed_client_state(const activity_message::ActivityPlan& plan,
  78. const transactions::Publication& publication) noexcept {
  79. // A committed msg 22 that changed nothing material is the client's settle report, sent once
  80. // spawn-in completes. It carries no region, spawn or teleport delta, so the surface reads all
  81. // three as absent, but it still arrives: the script needs it to time the opening line.
  82. if (!plan.clientState.pending || !publication.clientState.committed) {
  83. return;
  84. }
  85. server::activity::host::ClientStateChangeInput input{};
  86. input.binding = plan.clientState.binding;
  87. input.state = publication.clientState;
  88. input.sourceGeneration = plan.clientState.sourceGeneration;
  89. input.clientMessageSequence = plan.clientState.clientMessageSequence;
  90. input.payloadBytes = plan.clientState.payloadBytes;
  91. if (!server::activity::host::submit_client_state_change(input)) {
  92. core::log::write(core::log::Channel::server,
  93. core::log::Level::warn,
  94. "ev=activity stage=client_state_ingress result=refused");
  95. }
  96. }
  97. /** Queues exact entity-slot demand only after its grant transaction commits. */
  98. void submit_committed_entity_slots_requested(const activity_message::ActivityPlan& plan) noexcept {
  99. if (!plan.entitySlotsRequested.pending) {
  100. return;
  101. }
  102. server::activity::host::EntitySlotsRequestedInput input{};
  103. input.binding = plan.entitySlotsRequested.binding;
  104. input.sourceGeneration = plan.entitySlotsRequested.sourceGeneration;
  105. input.clientMessageSequence = plan.entitySlotsRequested.clientMessageSequence;
  106. input.requestedCount = plan.entitySlotsRequested.requestedCount;
  107. if (!server::activity::host::submit_entity_slots_requested(input)) {
  108. core::log::write(core::log::Channel::server,
  109. core::log::Level::warn,
  110. "ev=activity stage=entity_slots_requested result=refused");
  111. }
  112. }
  113. /** Applies one query answer only after its authenticated service frame commits. */
  114. void submit_committed_authority_answer(Session& session,
  115. const activity_message::ActivityPlan& plan) noexcept {
  116. if (!plan.authorityQuery.pending
  117. || plan.mutationDomain != activity_message::MutationDomain::authorityQuery) {
  118. return;
  119. }
  120. const authority_query::AnswerStatus status =
  121. authority_query::submit(session.activityAuthorityQuery,
  122. plan.authorityQuery.sourceGeneration,
  123. GetTickCount64(),
  124. plan.authorityQuery.answer);
  125. if (status != authority_query::AnswerStatus::bubbleAccepted
  126. && status != authority_query::AnswerStatus::complete) {
  127. std::array<char, core::log::kLineCapacity> line{};
  128. const int count =
  129. std::snprintf(line.data(),
  130. line.size(),
  131. "ev=activity stage=authority_query result=refused status=%u",
  132. static_cast<unsigned>(status));
  133. if (count > 0) {
  134. core::log::write(core::log::Channel::server,
  135. core::log::Level::warn,
  136. {line.data(), static_cast<std::size_t>(count)});
  137. }
  138. }
  139. }
  140. /** An old ActivityClient cannot relinquish the replacement client's grant. */
  141. void submit_committed_authority_abdication(Session& session,
  142. const activity_message::ActivityPlan& plan) noexcept {
  143. if (plan.mutationDomain != activity_message::MutationDomain::authorityAbdication
  144. || !plan.authorityAbdication.pending
  145. || plan.authorityAbdication.sourceGeneration != session.activity.bindingGeneration
  146. || plan.sessionId != session.activity.session.sessionId) {
  147. return;
  148. }
  149. state::activity::bubble_authority::record_abdication(
  150. plan.sessionId, plan.authorityAbdication.bubble, &plan.authorityAbdication.entities);
  151. server::gameplay::squad_entity_retirement::observe_abdication(
  152. session.activity.session,
  153. session.activity.bindingGeneration,
  154. plan.authorityAbdication.bubble,
  155. plan.authorityAbdication.entities);
  156. }
  157. /** Applies one reset acknowledgement only after its authenticated service frame commits. */
  158. void submit_committed_authority_reset(Session& session,
  159. const activity_message::ActivityPlan& plan) noexcept {
  160. if (!plan.authorityReset.pending
  161. || plan.mutationDomain != activity_message::MutationDomain::authorityReset) {
  162. return;
  163. }
  164. const authority_reset::AcknowledgementStatus status =
  165. authority_reset::submit(session.activityAuthorityReset,
  166. plan.authorityReset.sourceGeneration,
  167. GetTickCount64(),
  168. plan.authorityReset.answer);
  169. if (status != authority_reset::AcknowledgementStatus::complete) {
  170. std::array<char, core::log::kLineCapacity> line{};
  171. const int count =
  172. std::snprintf(line.data(),
  173. line.size(),
  174. "ev=activity stage=authority_reset result=refused status=%u",
  175. static_cast<unsigned>(status));
  176. if (count > 0) {
  177. core::log::write(core::log::Channel::server,
  178. core::log::Level::warn,
  179. {line.data(), static_cast<std::size_t>(count)});
  180. }
  181. }
  182. }
  183. } // namespace
  184. /**
  185. * Authenticates and answers one supported encrypted post-bootstrap request.
  186. * @param session Connection-owned authentication and nonce state.
  187. * @param scratch Lock-owned transform buffers kept off the Client thread stack.
  188. * @param outer Validated encrypted outer frame.
  189. * @param response Caller-owned complete-frame storage.
  190. * @param written Receives encoded response bytes.
  191. * @return True when routing succeeds and any response fits, commits State, and publishes its nonce.
  192. */
  193. bool consume(Session& session,
  194. Scratch& scratch,
  195. const middleware::bap::OuterFrame& outer,
  196. std::span<std::byte> response,
  197. std::size_t& written) noexcept {
  198. written = 0;
  199. session.accountMutationPublished = false;
  200. if (!session.authenticated) {
  201. // Staying silent here looks the same as a decode fault, and both look like a dead link.
  202. core::log::write(core::log::Channel::server,
  203. core::log::Level::warn,
  204. "ev=bap stage=encrypted result=drop reason=unauthenticated");
  205. return false;
  206. }
  207. std::size_t plaintextSize = 0;
  208. if (!middleware::secure_channel::open_frame(session.sessionKey,
  209. session.receiveNonce,
  210. outer.payload,
  211. scratch.plaintext,
  212. plaintextSize)) {
  213. const std::size_t possiblePlaintextSize =
  214. outer.payload.size() >= middleware::secure_channel::kFrameTagSize
  215. ? outer.payload.size() - middleware::secure_channel::kFrameTagSize
  216. : 0;
  217. clear_prefix(scratch.plaintext, possiblePlaintextSize);
  218. // The service is unreadable while the frame is sealed, so this line names no service.
  219. core::log::write(core::log::Channel::server,
  220. core::log::Level::warn,
  221. "ev=bap svc=none stage=decrypt result=fail");
  222. return false;
  223. }
  224. // Authentication consumes the receive nonce even when the inner service is unsupported.
  225. middleware::secure_channel::advance_nonce(session.receiveNonce);
  226. middleware::bap::RequestFrame frame;
  227. ServiceRoute route;
  228. std::size_t responseBodySize = 0;
  229. std::size_t framedSize = 0;
  230. ServiceOutcome outcome{};
  231. transactions::Publication publication{};
  232. queuez::SessionState nextQueuez = session.queuez;
  233. bool publishesQueuez = false;
  234. bool handled =
  235. middleware::bap::parse_request_payload(std::span(scratch.plaintext).first(plaintextSize),
  236. middleware::bap::FrameType::encrypted,
  237. frame)
  238. && routing::resolve(frame.serviceId, route);
  239. if (!handled) {
  240. core::log::write(core::log::Channel::server,
  241. core::log::Level::warn,
  242. "ev=bap svc=none stage=parse result=fail");
  243. }
  244. const bool processesBody = handled && route.responseMode != ResponseMode::none;
  245. const bool sendsReply = handled && route.responseMode == ResponseMode::reply;
  246. bool staleWebAction = false;
  247. // An armed resync means this peer's published manifest predates the account it would name.
  248. if (processesBody && route.bodyCodec == BodyCodec::webService && session.accountResyncArmed
  249. && !web_service::encode_resident_dependent_refusal(
  250. frame.body, scratch.responseBody, responseBodySize, staleWebAction)) {
  251. handled = false;
  252. diagnostics::report_failure(frame.serviceId, "stale_refusal");
  253. }
  254. if (staleWebAction) {
  255. core::log::write(core::log::Channel::server,
  256. core::log::Level::warn,
  257. "ev=bap stage=web_service result=refuse reason=stale_manifest");
  258. }
  259. // Pure one-way services consume only the authenticated receive nonce.
  260. if (!staleWebAction && processesBody
  261. && !body::process(route,
  262. session.queuez,
  263. session.activity,
  264. session.activityRosterDecode,
  265. session.matchmakingContext,
  266. frame.body,
  267. scratch.responseBody,
  268. responseBodySize,
  269. outcome)) {
  270. diagnostics::report_failure(frame.serviceId, "body");
  271. // A reply-mode service answers with an empty body instead of not at all. The Client
  272. // matches only the head of its pending ring. One unanswered request jams that ring for
  273. // good, and every later reply is rejected, which is worse than a thin reply.
  274. clear_prefix(scratch.responseBody, responseBodySize);
  275. responseBodySize = 0;
  276. outcome = {};
  277. handled = sendsReply;
  278. }
  279. if (handled) {
  280. report_service_traffic(frame, route, responseBodySize);
  281. }
  282. if (handled && sendsReply) {
  283. handled = reply::encode(scratch,
  284. route,
  285. frame.taskId,
  286. session.sessionKey,
  287. session.sendNonce,
  288. std::span(scratch.responseBody).first(responseBodySize),
  289. framedSize);
  290. if (!handled) {
  291. diagnostics::report_failure(frame.serviceId, "encode");
  292. }
  293. }
  294. // Stage every requested frame and check for caller room before committing State or the nonce.
  295. auto nextSendNonce = session.sendNonce;
  296. if (handled && sendsReply) {
  297. middleware::secure_channel::advance_nonce(nextSendNonce);
  298. }
  299. queuez::StagedPublication queuezPublication{};
  300. if (handled) {
  301. const std::uint64_t now = GetTickCount64();
  302. if (now >= session.acquisitionPresentationUntilTick) {
  303. session.acquisitionPresentationRows = {};
  304. session.acquisitionPresentationRowCount = 0;
  305. }
  306. const auto acquisitionPresentationRows =
  307. std::span(session.acquisitionPresentationRows)
  308. .first(session.acquisitionPresentationRowCount);
  309. const bool preserveAcquisitionPresentation = now < session.acquisitionPresentationUntilTick;
  310. handled = queuez::stage_service_outcome(scratch,
  311. session.queuez,
  312. outcome,
  313. preserveAcquisitionPresentation,
  314. acquisitionPresentationRows,
  315. session.sessionKey,
  316. nextSendNonce,
  317. scratch.framed,
  318. framedSize,
  319. queuezPublication);
  320. if (handled && queuezPublication.hasState) {
  321. nextQueuez = queuezPublication.after;
  322. publishesQueuez = true;
  323. }
  324. if (!handled) {
  325. diagnostics::report_failure(frame.serviceId, "stage");
  326. }
  327. }
  328. const auto* activityPlan = transaction_if<activity_message::ActivityPlan>(outcome);
  329. const bool publishesFamily4 =
  330. publishesQueuez
  331. && (queuezPublication.after.family4Active != session.queuez.family4Active
  332. || queuezPublication.after.family4Version != session.queuez.family4Version);
  333. if (handled && activityPlan != nullptr) {
  334. handled = route.responseMode == ResponseMode::uncorrelatedPush;
  335. if (!handled) {
  336. diagnostics::report_failure(frame.serviceId, "route");
  337. } else if (!activity_transaction::stage_notifications(session,
  338. scratch,
  339. *activityPlan,
  340. session.sessionKey,
  341. nextSendNonce,
  342. scratch.framed,
  343. framedSize)) {
  344. // The transaction still commits. A push that cannot be built is one lost message, and
  345. // dropping the commit with it would strand the client's reported state for the session.
  346. diagnostics::report_failure(frame.serviceId, "notify");
  347. if (activityPlan->mutationDomain == activity_message::MutationDomain::authorityPurge) {
  348. handled = false;
  349. }
  350. }
  351. }
  352. const bool artifactPurchase = transaction_if<ArtifactPurchaseTransaction>(outcome) != nullptr;
  353. const bool mutatesAccount =
  354. outcome.hasSelectCharacter || outcome.hasRecordClaim || outcome.hasArtifactReset
  355. || transaction_if<EquipmentSwapTransaction>(outcome) != nullptr
  356. || transaction_if<SubclassSelectionTransaction>(outcome) != nullptr
  357. || transaction_if<SocketPlugTransaction>(outcome) != nullptr
  358. || transaction_if<ItemStateTransaction>(outcome) != nullptr || artifactPurchase
  359. || transaction_if<CurrentActivityTransaction>(outcome) != nullptr
  360. || transaction_if<ItemAcquisitionTransaction>(outcome) != nullptr
  361. || transaction_if<ProfileItemAcquisitionTransaction>(outcome) != nullptr
  362. || transaction_if<ItemDismantleTransaction>(outcome) != nullptr
  363. || transaction_if<RecordRewardGrantTransaction>(outcome) != nullptr
  364. || transaction_if<SeasonPassRewardTransaction>(outcome) != nullptr
  365. || transaction_if<state::PendingSettingsUpdate>(outcome) != nullptr;
  366. const bool presentsAcquisition =
  367. transaction_if<ItemAcquisitionTransaction>(outcome) != nullptr
  368. || transaction_if<ProfileItemAcquisitionTransaction>(outcome) != nullptr
  369. || transaction_if<RecordRewardGrantTransaction>(outcome) != nullptr
  370. || transaction_if<SeasonPassRewardTransaction>(outcome) != nullptr;
  371. const bool invalidatesAcquisitionPresentation =
  372. outcome.hasChangeCharacter || outcome.hasSelectCharacter || outcome.hasArtifactReset
  373. || transaction_if<ItemDismantleTransaction>(outcome) != nullptr;
  374. const bool hasPrecommittedAccountAction =
  375. outcome.hasRecordClaim || outcome.hasSelectCharacter || outcome.hasArtifactReset;
  376. // Commit consumes pending payloads, so retain the connection fields first.
  377. const ConnectionFields connection = connection_fields(outcome);
  378. if (handled && processesBody) {
  379. // State changes become visible only after every requested frame and caller byte fit.
  380. // A refused commit sends nothing, so the frame's own reason is the only record of why.
  381. const char* commitReason = "none";
  382. server::gameplay::entity_identities::PublicationLease entityLease;
  383. const bool retirementValid =
  384. push::activity::begin_staged_roster_publication(session, entityLease);
  385. const bool fits = framedSize <= response.size();
  386. if (!retirementValid) {
  387. commitReason = "entity_retirement_stale";
  388. }
  389. handled =
  390. fits && retirementValid && transactions::commit(outcome, publication, commitReason);
  391. if (!handled) {
  392. diagnostics::report_failure(
  393. frame.serviceId, "commit", fits ? commitReason : "frame_capacity");
  394. }
  395. if (handled) {
  396. std::copy_n(scratch.framed.begin(), framedSize, response.begin());
  397. written = framedSize;
  398. entityLease.release();
  399. // The caller copy finishes before connection fields are published.
  400. session.sendNonce = nextSendNonce;
  401. if (publishesQueuez) {
  402. session.queuez = nextQueuez;
  403. }
  404. arm_repushes(session, queuezPublication);
  405. if (invalidatesAcquisitionPresentation) {
  406. session.acquisitionPresentationRows = {};
  407. session.acquisitionPresentationRowCount = 0;
  408. session.acquisitionPresentationUntilTick = 0;
  409. } else if (queuezPublication.updatesAcquisitionPresentationRows) {
  410. session.acquisitionPresentationRows = queuezPublication.acquisitionPresentationRows;
  411. session.acquisitionPresentationRowCount =
  412. queuezPublication.acquisitionPresentationRowCount;
  413. if (session.acquisitionPresentationRowCount == 0) {
  414. session.acquisitionPresentationUntilTick = 0;
  415. }
  416. }
  417. if (presentsAcquisition && publishesQueuez) {
  418. bap::arm_acquisition_presentation_hold(session);
  419. }
  420. publish_connection_fields(session, publication, connection);
  421. record_committed_join(session, connection);
  422. // The caller copy is done, so what the staged roster body owes is settled here.
  423. push::activity::commit_staged_roster(session);
  424. commit_staged_advertisement(session);
  425. if (activityPlan != nullptr) {
  426. submit_committed_client_state(*activityPlan, publication);
  427. submit_committed_entity_slots_requested(*activityPlan);
  428. submit_committed_authority_reset(session, *activityPlan);
  429. submit_committed_authority_answer(session, *activityPlan);
  430. submit_committed_authority_abdication(session, *activityPlan);
  431. if (activityPlan->hasReturnedEntitySlots
  432. && activityPlan->sessionId == session.activity.session.sessionId) {
  433. server::gameplay::squad_entity_retirement::returned_slots(
  434. session.activity.session,
  435. session.activity.bindingGeneration,
  436. activityPlan->returnedEntitySlots);
  437. }
  438. if (activityPlan->mutationDomain
  439. == activity_message::MutationDomain::authorityPurge) {
  440. const auto previousEpoch = session.activity.replicationEpoch;
  441. session.activity.replicationEpoch = activityPlan->authorityPurge.body.epoch;
  442. const auto updatedViews = server::gameplay::peer::commit_replication_epoch(
  443. session.activity.session,
  444. session.activity.bindingGeneration,
  445. previousEpoch,
  446. session.activity.replicationEpoch);
  447. state::activity::bubble_authority::record_purge(
  448. activityPlan->sessionId, activityPlan->authorityPurge.body.slots);
  449. server::gameplay::squad_entity_retirement::returned_slots(
  450. session.activity.session,
  451. session.activity.bindingGeneration,
  452. activityPlan->authorityPurge.body.slots);
  453. if (session.activityReplicationEpoch.pending
  454. && session.activityReplicationEpoch.generation
  455. == session.activity.replicationEpoch) {
  456. session.activityReplicationEpoch.pending = false;
  457. }
  458. unsigned slots = 0;
  459. for (const std::byte byte : activityPlan->authorityPurge.body.slots) {
  460. slots +=
  461. static_cast<unsigned>(std::popcount(std::to_integer<unsigned>(byte)));
  462. }
  463. std::array<char, core::log::kLineCapacity> line{};
  464. const int count =
  465. std::snprintf(line.data(),
  466. line.size(),
  467. "ev=activity stage=purge result=published epoch=%u reason=%d "
  468. "slots=%u views=%zu",
  469. static_cast<unsigned>(session.activity.replicationEpoch),
  470. static_cast<int>(activityPlan->authorityPurge.body.reason),
  471. slots,
  472. updatedViews);
  473. if (count > 0) {
  474. core::log::write(core::log::Channel::server,
  475. core::log::Level::debug,
  476. {line.data(), static_cast<std::size_t>(count)});
  477. }
  478. }
  479. }
  480. // Any delivered activity notification resets the client's silence timer, so the
  481. // fallback keepalive is delayed. A roster-only answer is excluded: it owes neither the
  482. // global state nor the membership, and at once a second it would starve the keepalive.
  483. const bool defersKeepalive =
  484. activityPlan != nullptr && framedSize != 0
  485. && activityPlan->delivery != activity_message::Delivery::rosterNotification;
  486. if (defersKeepalive) {
  487. session.activityKeepaliveDueTick = GetTickCount64() + kActivityKeepaliveIntervalMs;
  488. }
  489. const bool resyncsCommittedAccount =
  490. hasPrecommittedAccountAction && !queuezPublication.hasState;
  491. if (resyncsCommittedAccount) {
  492. bap::arm_account_resync_everywhere();
  493. }
  494. if (artifactPurchase || outcome.hasArtifactReset) {
  495. // Artifact overrides live in Family 5, so they need their own refresh. A record
  496. // claim does not: its Family-4 replacement rearms the client rebuild.
  497. session.artifactRefreshArmed = true;
  498. session.artifactFamily4RefreshDueTick =
  499. GetTickCount64() + kArtifactFamily4RefreshDelayMs;
  500. session.artifactFamily4RefreshArmed = true;
  501. }
  502. if (outcome.hasArtifactReset) {
  503. session.artifactResetRefresh = outcome.artifactReset;
  504. session.artifactResetRefreshCursor = 0;
  505. }
  506. session.accountMutationPublished = mutatesAccount && !resyncsCommittedAccount;
  507. if (publishesFamily4) {
  508. // The Family-4 store is updated in place, so pointer identity cannot detect its
  509. // initial population or later revisions. Carry the exact committed publication
  510. // across to the next native lookup, after the client has consumed this frame.
  511. bap::notify_investment_publication();
  512. }
  513. }
  514. }
  515. if (!handled) {
  516. if (hasPrecommittedAccountAction) {
  517. bap::arm_account_resync_everywhere();
  518. }
  519. // The staged body is dropped, so its grant and its state byte go back for the next push.
  520. push::activity::discard_staged_roster(session);
  521. discard_staged_advertisement(session);
  522. }
  523. clear_prefix(scratch.plaintext, plaintextSize);
  524. clear_prefix(scratch.responseBody, responseBodySize);
  525. clear_prefix(scratch.framed, framedSize);
  526. outcome = {};
  527. SecureZeroMemory(&publication, sizeof publication);
  528. SecureZeroMemory(&queuezPublication, sizeof queuezPublication);
  529. return handled;
  530. }
  531. } // namespace sunrise::server::bap::encrypted