10. 델타 스냅샷 동기화 + 리플레이 방어 (종합)

난이도 최상 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 델타 스냅샷 동기화 + 리플레이 방어 (종합/최상)
// ----------------------------------------------------------------------------
// 시나리오(현실적):
//   - 서버는 각 클라에 월드 상태를 "델타 스냅샷"으로 보낸다:
//       서버는 tick 마다 스냅샷을 만들고, 클라가 ack 한 baseline 스냅샷과의
//       차분(delta)만 전송한다. 클라는 baseline 에 델타를 적용해 현재 상태를 복원한다.
//       와이어: [uint snapTick][uint baseTick][delta...]
//   - 반대 방향(클라→서버)의 명령 패킷은 리플레이/변조를 막아야 한다:
//       와이어: [ulong nonce][uint clientTimeMs][cmdPayload...][mac(생략)]
//     서버는 nonce 중복과 시간 창(window)으로 리플레이를 거른다.
//   - UDP 기반이라 패킷은 순서뒤바뀜/유실/중복/지연되어 도착한다.
//   - 클라는 신뢰 불가(치터/리플레이/프록시). 멀티스레드 서버다.
//
// 요구사항:
//   - 델타는 클라가 "실제로 가진 baseline"에만 적용되어야 한다. 어긋나면 상태가 발산한다.
//   - 같은 명령이 두 번 처리되면(리플레이) 경제/전투 사고가 난다.
//   - 어떤 입력/타이밍에도 서버가 죽거나 무한히 메모리를 쓰면 안 된다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 발생·악용되는지 입체적으로 설명하고,
//   수정안과 더 나은 설계(델타 동기화 안전성 / 리플레이 방어 / 버전·상태 합의)를 제시하라.
// ============================================================================

using System;
using System.Collections.Generic;

// ---------------------------------------------------------------------------
// 서버 → 클라: 델타 스냅샷 송신
// ---------------------------------------------------------------------------
public class Snapshot
{
    public uint tick;
    public byte[] state;
}

public class DeltaSender
{
    // 클라가 마지막으로 ack 한 baseline tick
    public uint ackedTick = 0;

    // 최근 스냅샷 히스토리 (tick -> snapshot)
    public Dictionary<uint, Snapshot> history = new Dictionary<uint, Snapshot>();

    // 이번 tick 스냅샷을 만들고 델타를 보낸다
    public byte[] BuildDelta(Snapshot cur)
    {
        // (A) baseline 으로 acked 스냅샷을 찾는다
        Snapshot baseSnap = history[ackedTick];

        byte[] outBuf = new byte[8];
        BitConverter.GetBytes(cur.tick).CopyTo(outBuf, 0);
        BitConverter.GetBytes(ackedTick).CopyTo(outBuf, 4);

        // (B) cur 와 base 의 차분을 계산해 뒤에 붙인다
        AppendDiff(outBuf, baseSnap.state, cur.state);

        // 히스토리에 보관
        history[cur.tick] = cur;
        return outBuf;
    }

    private void AppendDiff(byte[] outBuf, byte[] baseState, byte[] cur) { /* 차분 인코딩 */ }
}

// ---------------------------------------------------------------------------
// 클라 → 서버: 명령 패킷 리플레이 방어
// ---------------------------------------------------------------------------
public class ReplayGuard
{
    // 본 nonce 들을 기억한다
    public HashSet<ulong> seenNonces = new HashSet<ulong>();

    public const uint kWindowMs = 5000;   // 허용 시간 창

    // true = 정상 처리, false = 리플레이/만료로 폐기
    public bool Accept(byte[] buf, int len, uint serverNowMs)
    {
        // 헤더: [ulong nonce][uint clientTimeMs]
        ulong nonce        = BitConverter.ToUInt64(buf, 0);
        uint  clientTimeMs = BitConverter.ToUInt32(buf, 8);

        // (C) 시간 창 검사: 너무 오래된 명령은 버린다
        if (serverNowMs - clientTimeMs > kWindowMs)
            return false;

        // (D) nonce 중복 검사
        if (seenNonces.Contains(nonce))
            return false;
        seenNonces.Add(nonce);

        return true;   // 통과 → 명령 처리
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.