From 40f00861b520449815592f2d66be3884c877f2fe Mon Sep 17 00:00:00 2001 From: Kingpython123 Date: Sat, 31 Oct 2026 10:34:07 -0700 Subject: [PATCH] Fix #208: seed problem 59 interactor from a per-test secret The problem 59 interactor is non-adaptive but built its hidden array with testlib's rng at the fixed default seed and never reseeded, so the array was a deterministic function of n. A program replaying that stream reconstructed the array and answered with zero comparison queries, scoring 100 on every test. Read a per-test secret seed from the hidden answer file and setSeed() the rng before generating the array, as the other non-adaptive interactors in this repo take their hidden data from the answer file. The contestant still receives only n, so the array is no longer predictable. Each public testdata answer file gets a distinct seed; private test answer files need one integer seed added too. Add algorithmic/tests/test_problem59.py as a regression test. --- algorithmic/problems/59/interactor.cpp | 3 + algorithmic/problems/59/testdata/1.ans | 1 + algorithmic/problems/59/testdata/10.ans | 1 + algorithmic/problems/59/testdata/2.ans | 1 + algorithmic/problems/59/testdata/3.ans | 1 + algorithmic/problems/59/testdata/4.ans | 1 + algorithmic/problems/59/testdata/5.ans | 1 + algorithmic/problems/59/testdata/6.ans | 1 + algorithmic/problems/59/testdata/7.ans | 1 + algorithmic/problems/59/testdata/8.ans | 1 + algorithmic/problems/59/testdata/9.ans | 1 + algorithmic/tests/test_problem59.py | 103 ++++++++++++++++++++++++ 12 files changed, 116 insertions(+) create mode 100644 algorithmic/tests/test_problem59.py diff --git a/algorithmic/problems/59/interactor.cpp b/algorithmic/problems/59/interactor.cpp index 4ebcf47e8..fcd1b29a8 100644 --- a/algorithmic/problems/59/interactor.cpp +++ b/algorithmic/problems/59/interactor.cpp @@ -9,6 +9,9 @@ int main(int argc, char* argv[]) { registerInteraction(argc, argv); int n = inf.readInt(1, 30000, "n"); + // Seed the generator from a per-test secret in the answer file so the + // hidden array is not a fixed function of the default rng seed (#208). + rnd.setSeed(ans.readLong()); cout << n << endl; cout.flush(); diff --git a/algorithmic/problems/59/testdata/1.ans b/algorithmic/problems/59/testdata/1.ans index e69de29bb..24ca6db22 100644 --- a/algorithmic/problems/59/testdata/1.ans +++ b/algorithmic/problems/59/testdata/1.ans @@ -0,0 +1 @@ +2718281828 diff --git a/algorithmic/problems/59/testdata/10.ans b/algorithmic/problems/59/testdata/10.ans index e69de29bb..591107c59 100644 --- a/algorithmic/problems/59/testdata/10.ans +++ b/algorithmic/problems/59/testdata/10.ans @@ -0,0 +1 @@ +1912931182 diff --git a/algorithmic/problems/59/testdata/2.ans b/algorithmic/problems/59/testdata/2.ans index e69de29bb..1faffd906 100644 --- a/algorithmic/problems/59/testdata/2.ans +++ b/algorithmic/problems/59/testdata/2.ans @@ -0,0 +1 @@ +3141592653 diff --git a/algorithmic/problems/59/testdata/3.ans b/algorithmic/problems/59/testdata/3.ans index e69de29bb..114735707 100644 --- a/algorithmic/problems/59/testdata/3.ans +++ b/algorithmic/problems/59/testdata/3.ans @@ -0,0 +1 @@ +1618033988 diff --git a/algorithmic/problems/59/testdata/4.ans b/algorithmic/problems/59/testdata/4.ans index e69de29bb..f449148fe 100644 --- a/algorithmic/problems/59/testdata/4.ans +++ b/algorithmic/problems/59/testdata/4.ans @@ -0,0 +1 @@ +1414213562 diff --git a/algorithmic/problems/59/testdata/5.ans b/algorithmic/problems/59/testdata/5.ans index e69de29bb..175aeb680 100644 --- a/algorithmic/problems/59/testdata/5.ans +++ b/algorithmic/problems/59/testdata/5.ans @@ -0,0 +1 @@ +2236067977 diff --git a/algorithmic/problems/59/testdata/6.ans b/algorithmic/problems/59/testdata/6.ans index e69de29bb..783918515 100644 --- a/algorithmic/problems/59/testdata/6.ans +++ b/algorithmic/problems/59/testdata/6.ans @@ -0,0 +1 @@ +1732050807 diff --git a/algorithmic/problems/59/testdata/7.ans b/algorithmic/problems/59/testdata/7.ans index e69de29bb..308fcaa08 100644 --- a/algorithmic/problems/59/testdata/7.ans +++ b/algorithmic/problems/59/testdata/7.ans @@ -0,0 +1 @@ +2645751311 diff --git a/algorithmic/problems/59/testdata/8.ans b/algorithmic/problems/59/testdata/8.ans index e69de29bb..4ca96a183 100644 --- a/algorithmic/problems/59/testdata/8.ans +++ b/algorithmic/problems/59/testdata/8.ans @@ -0,0 +1 @@ +1259921049 diff --git a/algorithmic/problems/59/testdata/9.ans b/algorithmic/problems/59/testdata/9.ans index e69de29bb..6b2b4cf57 100644 --- a/algorithmic/problems/59/testdata/9.ans +++ b/algorithmic/problems/59/testdata/9.ans @@ -0,0 +1 @@ +1442249570 diff --git a/algorithmic/tests/test_problem59.py b/algorithmic/tests/test_problem59.py new file mode 100644 index 000000000..a82ae2146 --- /dev/null +++ b/algorithmic/tests/test_problem59.py @@ -0,0 +1,103 @@ +"""The problem 59 interactor must not be solvable with zero queries (#208). + +Before the fix the interactor built its hidden array with testlib's rng at the +fixed default seed and never reseeded, so a program replaying that stream scored +100 without asking a single comparison. The fix reads a per-test secret seed from +the (hidden) answer file. These tests reconstruct the array the interactor will +use from the committed secret seed and feed it back with no queries: the array +from the committed seed is accepted, while the array from the old default seed is +rejected on every non-trivial test, proving the exploit is defeated. +""" +import os +import re +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +DATA = ROOT / "algorithmic/problems/59/testdata" +TESTS = list(range(1, 11)) + +# testlib random_t: java-style 48-bit LCG. +_MUL, _ADD, _MASK = 0x5DEECE66D, 0xB, (1 << 48) - 1 +_LLMAX = 9223372036854775807 +_DEFAULT_SEED = 3905348978240129619 + + +class _Rng: + def __init__(self, secret=None): + # registerInteraction leaves the default seed; setSeed(s) xors with the multiplier. + self.s = _DEFAULT_SEED if secret is None else ((secret ^ _MUL) & _MASK) + + def _bits(self, bits): + if bits <= 48: + self.s = (self.s * _MUL + _ADD) & _MASK + return self.s >> (48 - bits) + return (self._bits(31) << 32) ^ self._bits(32) # lowerBitCount = 32 (version = -1) + + def _next(self, n): + limit = _LLMAX // n * n + while True: + b = self._bits(63) + if b < limit: + return b % n + + def nrange(self, a, b): + return a + self._next(b - a + 1) + + +def _hidden_array(n, secret): + rng = _Rng(secret) + a = list(range(1, n + 1)) + for i in range(n): + j = rng.nrange(i, min(n - 1, i + 2)) + a[i], a[j] = a[j], a[i] + return a + + +@pytest.fixture(scope="module") +def interactor(tmp_path_factory): + exe = tmp_path_factory.mktemp("interactor59") / "interactor" + subprocess.run( + ["g++", "-std=gnu++17", "-O2", "-I" + str(ROOT / "algorithmic/judge/include"), + str(ROOT / "algorithmic/problems/59/interactor.cpp"), "-o", str(exe)], + check=True, + ) + return exe + + +def _play(interactor, t, array): + """Send `array` as the final answer with no queries; return (returncode, stderr).""" + inf, ans = DATA / f"{t}.in", DATA / f"{t}.ans" + proc = subprocess.Popen( + [str(interactor), str(inf), os.devnull, str(ans)], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, + ) + n = int(proc.stdout.readline()) + proc.stdin.write("! " + " ".join(map(str, array)) + "\n") + proc.stdin.flush() + proc.stdin.close() + err = proc.stderr.read() + proc.wait() + return proc.returncode, n, err + + +@pytest.mark.parametrize("t", TESTS) +def test_correct_array_is_accepted(interactor, t): + secret = int((DATA / f"{t}.ans").read_text().split()[0]) + n = int((DATA / f"{t}.in").read_text().split()[0]) + rc, got_n, err = _play(interactor, t, _hidden_array(n, secret)) + assert got_n == n + assert rc == 7, err # testlib _points: a correct answer scores + assert "Ratio: 1.0" in err + + +@pytest.mark.parametrize("t", TESTS) +def test_default_seed_replay_is_rejected(interactor, t): + n = int((DATA / f"{t}.in").read_text().split()[0]) + if n < 2: + pytest.skip("n=1 has a unique array, so no seed can distinguish it") + rc, _, err = _play(interactor, t, _hidden_array(n, None)) + assert rc != 7, "zero-query default-seed replay must no longer score (#208)" + assert "Wrong final array" in err